Skip to content

Commit 7f7ae91

Browse files
authored
fix: use regex instead of globby for include:local wildcard file paths implementation (#1548)
* fix: use regex instead of globby for include:local wildcard file paths implementation * refactor: fix sonarqube Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
1 parent 4347267 commit 7f7ae91

File tree

9 files changed

+140
-13
lines changed

9 files changed

+140
-13
lines changed

src/job.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {GitlabRunnerCPUsPresetValue, GitlabRunnerMemoryPresetValue, GitlabRunner
1515
import {handler} from "./handler.js";
1616
import * as yaml from "js-yaml";
1717
import {Parser} from "./parser.js";
18-
import globby from "globby";
19-
import {validateIncludeLocal} from "./parser-includes.js";
18+
import {resolveIncludeLocal, validateIncludeLocal} from "./parser-includes.js";
2019
import terminalLink from "terminal-link";
2120

2221
const GCL_SHELL_PROMPT_PLACEHOLDER = "<gclShellPromptPlaceholder>";
@@ -1499,13 +1498,13 @@ export class Job {
14991498
if (include["local"]) {
15001499
const expandedInclude = Utils.expandText(include["local"], this._variables);
15011500
validateIncludeLocal(expandedInclude);
1502-
const files = await globby(expandedInclude.replace(/^\//, ""), {dot: true, cwd});
1501+
const files = resolveIncludeLocal(expandedInclude, cwd);
15031502
if (files.length == 0) {
15041503
throw new AssertionError({message: `Local include file \`${include["local"]}\` specified in \`.${this.name}\` cannot be found!`});
15051504
}
15061505

15071506
for (const file of files) {
1508-
const content = await Parser.loadYaml(`${cwd}/${file}`, {});
1507+
const content = await Parser.loadYaml(file, {});
15091508
contents = {
15101509
...contents,
15111510
...content,

src/parser-includes.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import axios from "axios";
1010
import globby from "globby";
1111
import path from "path";
1212
import semver from "semver";
13+
import RE2 from "re2";
1314

1415
type ParserIncludesInitOptions = {
1516
argv: Argv;
@@ -66,13 +67,7 @@ export class ParserIncludes {
6667
continue;
6768
}
6869
}
69-
if (value["local"]) {
70-
validateIncludeLocal(value["local"]);
71-
const files = await globby(value["local"].replace(/^\//, ""), {dot: true, cwd});
72-
if (files.length == 0) {
73-
throw new AssertionError({message: `Local include file cannot be found ${value["local"]}`});
74-
}
75-
} else if (value["file"]) {
70+
if (value["file"]) {
7671
for (const fileValue of Array.isArray(value["file"]) ? value["file"] : [value["file"]]) {
7772
promises.push(this.downloadIncludeProjectFile(cwd, stateDir, value["project"], value["ref"] || "HEAD", fileValue, gitData, fetchIncludes));
7873
}
@@ -97,9 +92,13 @@ export class ParserIncludes {
9792
}
9893
}
9994
if (value["local"]) {
100-
const files = await globby([value["local"].replace(/^\//, "")], {dot: true, cwd});
95+
validateIncludeLocal(value["local"]);
96+
const files = resolveIncludeLocal(value["local"], cwd);
97+
if (files.length == 0) {
98+
throw new AssertionError({message: `Local include file cannot be found ${value["local"]}`});
99+
}
101100
for (const localFile of files) {
102-
const content = await Parser.loadYaml(`${cwd}/${localFile}`, {inputs: value.inputs || {}}, expandVariables);
101+
const content = await Parser.loadYaml(localFile, {inputs: value.inputs ?? {}}, expandVariables);
103102
includeDatas = includeDatas.concat(await this.init(content, opts));
104103
}
105104
} else if (value["project"]) {
@@ -321,6 +320,18 @@ export class ParserIncludes {
321320
throw new AssertionError({message: `Project include could not be fetched { project: ${project}, ref: ${ref}, file: ${normalizedFile} }\n${e}`});
322321
}
323322
}
323+
324+
static readonly memoLocalRepoFiles = (() => {
325+
const cache = new Map<string, string[]>();
326+
return (path: string) => {
327+
let result = cache.get(path);
328+
if (typeof result !== "undefined") return result;
329+
330+
result = globby.sync(path, {dot: true, gitignore: true});
331+
cache.set(path, result);
332+
return result;
333+
};
334+
})();
324335
}
325336

326337
export function validateIncludeLocal (filePath: string) {
@@ -345,3 +356,24 @@ export function resolveSemanticVersionRange (range: string, gitTags: string[]) {
345356
});
346357
return found;
347358
}
359+
360+
export function resolveIncludeLocal (pattern: string, cwd: string) {
361+
const repoFiles = ParserIncludes.memoLocalRepoFiles(cwd);
362+
363+
if (!pattern.startsWith("/")) pattern = `/${pattern}`; // Ensure pattern starts with `/`
364+
pattern = `${cwd}${pattern}`;
365+
366+
// escape all special regex metacharacters
367+
pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
368+
369+
// `**` matches anything
370+
const anything = ".*?";
371+
pattern = pattern.replace(/\\\*\\\*/g, anything);
372+
373+
// `*` matches anything except for `/`
374+
const anything_but_not_slash = "([^/])*?";
375+
pattern = pattern.replace(/\\\*/g, anything_but_not_slash);
376+
377+
const re2 = new RE2(`^${pattern}`);
378+
return repoFiles.filter((f: any) => re2.test(f));
379+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
# This matches all `.yml` files in `configs` and any subfolder in it.
3+
include: 'configs/**.yml'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
# This matches all `.yml` files only in subfolders of `configs`.
3+
include: 'configs/**/*.yml'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
# This matches all `.yml` files only in `configs`.
3+
include: 'configs/*.yml'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
configs/.gitlab-ci.yml:
3+
script:
4+
- echo hello world
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
configs/subfolder/.gitlab-ci.yml:
3+
script:
4+
- echo hello world
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
configs/subfolder/subfolder/.gitlab-ci.yml:
3+
script:
4+
- echo hello world

tests/test-cases/include-local-wildcard/integration.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,78 @@ test("include-local-wildcard <build-job>", async () => {
1818
expect(output).toContain("cache-repo executed!");
1919
expect(output).toContain("docs executed!");
2020
});
21+
22+
test("expect `configs/**.yml` to match all `.yml` files in `configs` and any subfolder in it.", async () => {
23+
const writeStreams = new WriteStreamsMock();
24+
await handler({
25+
cwd: "tests/test-cases/include-local-wildcard",
26+
file: ".gitlab-ci-1.yml",
27+
preview: true,
28+
noColor: true,
29+
}, writeStreams);
30+
expect(writeStreams.stdoutLines.join()).toEqual(`
31+
---
32+
stages:
33+
- .pre
34+
- build
35+
- test
36+
- deploy
37+
- .post
38+
configs/.gitlab-ci.yml:
39+
script:
40+
- echo hello world
41+
configs/subfolder/.gitlab-ci.yml:
42+
script:
43+
- echo hello world
44+
configs/subfolder/subfolder/.gitlab-ci.yml:
45+
script:
46+
- echo hello world
47+
`.trim());
48+
});
49+
50+
test("expect `configs/**/*.yml` to match files only in subfolders of `configs`", async () => {
51+
const writeStreams = new WriteStreamsMock();
52+
await handler({
53+
cwd: "tests/test-cases/include-local-wildcard",
54+
file: ".gitlab-ci-2.yml",
55+
preview: true,
56+
noColor: true,
57+
}, writeStreams);
58+
expect(writeStreams.stdoutLines.join()).toEqual(`
59+
---
60+
stages:
61+
- .pre
62+
- build
63+
- test
64+
- deploy
65+
- .post
66+
configs/subfolder/.gitlab-ci.yml:
67+
script:
68+
- echo hello world
69+
configs/subfolder/subfolder/.gitlab-ci.yml:
70+
script:
71+
- echo hello world
72+
`.trim());
73+
});
74+
75+
test("expect `configs/*.yml` to match only `.yml` files in `configs`.", async () => {
76+
const writeStreams = new WriteStreamsMock();
77+
await handler({
78+
cwd: "tests/test-cases/include-local-wildcard",
79+
file: ".gitlab-ci-3.yml",
80+
preview: true,
81+
noColor: true,
82+
}, writeStreams);
83+
expect(writeStreams.stdoutLines.join()).toEqual(`
84+
---
85+
stages:
86+
- .pre
87+
- build
88+
- test
89+
- deploy
90+
- .post
91+
configs/.gitlab-ci.yml:
92+
script:
93+
- echo hello world
94+
`.trim());
95+
});

0 commit comments

Comments
 (0)