@intentius/chant-lexicon-gitlab 0.101.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/composites/node-pipeline.d.ts +16 -16
- package/dist/composites/node-pipeline.d.ts.map +1 -1
- package/dist/generated/index.d.ts +19 -19
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/integrity.json +10 -10
- package/dist/intrinsics.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl013.d.ts +3 -1
- package/dist/lint/post-synth/wgl013.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl017.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl020.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl021.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl050.d.ts.map +1 -1
- package/dist/lint/post-synth/yaml-helpers.d.ts +57 -8
- package/dist/lint/post-synth/yaml-helpers.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrate/from-github/provenance.d.ts.map +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/rules/wgl013.ts +4 -2
- package/dist/rules/wgl017.ts +3 -11
- package/dist/rules/wgl020.ts +4 -5
- package/dist/rules/wgl021.ts +4 -5
- package/dist/rules/wgl048.ts +2 -2
- package/dist/rules/wgl049.ts +4 -4
- package/dist/rules/wgl050.ts +5 -8
- package/dist/rules/yaml-helpers.ts +176 -113
- package/dist/variables.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +43 -0
- package/src/components/generate-op-pipeline.ts +4 -4
- package/src/components/generate-pipeline.test.ts +33 -0
- package/src/components/generate-pipeline.ts +117 -11
- package/src/components/generate-pr-pipeline.test.ts +70 -0
- package/src/lint/post-synth/post-synth.test.ts +29 -0
- package/src/lint/post-synth/wgl013.test.ts +53 -0
- package/src/lint/post-synth/wgl013.ts +4 -2
- package/src/lint/post-synth/wgl014.test.ts +23 -0
- package/src/lint/post-synth/wgl015.test.ts +35 -0
- package/src/lint/post-synth/wgl017.test.ts +14 -0
- package/src/lint/post-synth/wgl017.ts +3 -11
- package/src/lint/post-synth/wgl020.test.ts +26 -0
- package/src/lint/post-synth/wgl020.ts +4 -5
- package/src/lint/post-synth/wgl021.test.ts +29 -0
- package/src/lint/post-synth/wgl021.ts +4 -5
- package/src/lint/post-synth/wgl028.test.ts +27 -0
- package/src/lint/post-synth/wgl042.test.ts +14 -0
- package/src/lint/post-synth/wgl048.test.ts +14 -0
- package/src/lint/post-synth/wgl048.ts +2 -2
- package/src/lint/post-synth/wgl049.test.ts +25 -0
- package/src/lint/post-synth/wgl049.ts +4 -4
- package/src/lint/post-synth/wgl050.test.ts +16 -0
- package/src/lint/post-synth/wgl050.ts +5 -8
- package/src/lint/post-synth/yaml-helpers.test.ts +249 -0
- package/src/lint/post-synth/yaml-helpers.ts +176 -113
- package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +3 -1
- package/src/serializer.ts +5 -5
|
@@ -69,3 +69,28 @@ build:
|
|
|
69
69
|
expect(wgl049.check(makeCtx(yaml))).toHaveLength(0);
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
describe("WGL049: job ids and section boundaries (#3256)", () => {
|
|
74
|
+
test("flags a job whose id starts with a capital", () => {
|
|
75
|
+
const yaml = `Build_App:
|
|
76
|
+
script:
|
|
77
|
+
- npm ci
|
|
78
|
+
`;
|
|
79
|
+
const diags = wgl049.check(makeCtx(yaml));
|
|
80
|
+
expect(diags).toHaveLength(1);
|
|
81
|
+
expect(diags[0].entity).toBe("Build_App");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a cache: in the next job is not read as this job's cache", () => {
|
|
85
|
+
const yaml = `build:
|
|
86
|
+
script:
|
|
87
|
+
- npm ci
|
|
88
|
+
other:
|
|
89
|
+
cache:
|
|
90
|
+
paths: [node_modules/]
|
|
91
|
+
script: echo
|
|
92
|
+
`;
|
|
93
|
+
const diags = wgl049.check(makeCtx(yaml));
|
|
94
|
+
expect(diags.map((d) => d.entity)).toEqual(["build"]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
|
-
import { getPrimaryOutput, extractJobs, extractJobSection } from "./yaml-helpers";
|
|
14
|
+
import { getPrimaryOutput, extractJobs, extractJobSection, topLevelSections } from "./yaml-helpers";
|
|
15
15
|
|
|
16
16
|
const DEP_INSTALL_RE = /\b(npm (install|ci)|yarn install|pnpm install|pip install|pip3 install|bundle install|composer install|go mod download|mvn (install|dependency:resolve))\b/i;
|
|
17
17
|
|
|
18
18
|
function hasPipelineWideCache(yaml: string): boolean {
|
|
19
|
-
const sections = yaml
|
|
20
|
-
if (sections.some((s) =>
|
|
21
|
-
const defaultSection = sections.find((s) =>
|
|
19
|
+
const sections = topLevelSections(yaml);
|
|
20
|
+
if (sections.some((s) => s.key === "cache")) return true;
|
|
21
|
+
const defaultSection = sections.find((s) => s.key === "default")?.text;
|
|
22
22
|
return !!defaultSection && /\n\s+cache:/.test(defaultSection);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -55,3 +55,19 @@ describe("WGL050: merge-request job missing interruptible", () => {
|
|
|
55
55
|
expect(wgl050.check(makeCtx(yaml))).toHaveLength(0);
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
describe("WGL050: job ids and section boundaries (#3256)", () => {
|
|
60
|
+
test("flags a job whose id has a colon, with no blank line before it", () => {
|
|
61
|
+
const yaml = `build:
|
|
62
|
+
interruptible: true
|
|
63
|
+
script: make
|
|
64
|
+
test:unit:
|
|
65
|
+
rules:
|
|
66
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
67
|
+
script:
|
|
68
|
+
- npm test
|
|
69
|
+
`;
|
|
70
|
+
const diags = wgl050.check(makeCtx(yaml));
|
|
71
|
+
expect(diags.map((d) => d.entity)).toEqual(["test:unit"]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
13
|
-
import { getPrimaryOutput, isMergeRequestReachable } from "./yaml-helpers";
|
|
14
|
-
|
|
15
|
-
const RESERVED_TOP_LEVEL_KEYS = new Set(["stages", "default", "workflow", "variables", "include", "cache"]);
|
|
13
|
+
import { getPrimaryOutput, isMergeRequestReachable, extractJobs, extractJobSection } from "./yaml-helpers";
|
|
16
14
|
|
|
17
15
|
export const wgl050: PostSynthCheck = {
|
|
18
16
|
id: "WGL050",
|
|
@@ -24,11 +22,10 @@ export const wgl050: PostSynthCheck = {
|
|
|
24
22
|
for (const [, output] of ctx.outputs) {
|
|
25
23
|
const yaml = getPrimaryOutput(output);
|
|
26
24
|
|
|
27
|
-
for (const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (jobName.startsWith(".") || RESERVED_TOP_LEVEL_KEYS.has(jobName)) continue;
|
|
25
|
+
for (const [jobName] of extractJobs(yaml)) {
|
|
26
|
+
if (jobName.startsWith(".")) continue;
|
|
27
|
+
const section = extractJobSection(yaml, jobName);
|
|
28
|
+
if (!section) continue;
|
|
32
29
|
if (/deploy/i.test(jobName)) continue; // cancelling mid-deploy is a hazard, not a win
|
|
33
30
|
|
|
34
31
|
if (!isMergeRequestReachable(section)) continue;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
extractJobs,
|
|
4
|
+
extractStages,
|
|
5
|
+
extractGlobalVariables,
|
|
6
|
+
extractJobSection,
|
|
7
|
+
extractScriptCommands,
|
|
8
|
+
topLevelSections,
|
|
9
|
+
topLevelKey,
|
|
10
|
+
} from "./yaml-helpers";
|
|
11
|
+
|
|
12
|
+
// The reproduction from #3256.
|
|
13
|
+
const ISSUE_3256 = `stages:
|
|
14
|
+
- build
|
|
15
|
+
- deploy
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
stage: build
|
|
19
|
+
script:
|
|
20
|
+
- make
|
|
21
|
+
|
|
22
|
+
Build_Docs:
|
|
23
|
+
stage: build
|
|
24
|
+
script:
|
|
25
|
+
- make docs
|
|
26
|
+
|
|
27
|
+
deploy:
|
|
28
|
+
stage: deploy
|
|
29
|
+
needs:
|
|
30
|
+
- job: build
|
|
31
|
+
artifacts: true
|
|
32
|
+
- job: Build_Docs
|
|
33
|
+
script:
|
|
34
|
+
- ./deploy.sh
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
describe("extractJobs", () => {
|
|
38
|
+
test("reads a job id that starts with a capital and needs after an artifacts: line (#3256)", () => {
|
|
39
|
+
const jobs = extractJobs(ISSUE_3256);
|
|
40
|
+
expect([...jobs.keys()]).toEqual(["build", "Build_Docs", "deploy"]);
|
|
41
|
+
expect(jobs.get("Build_Docs")?.stage).toBe("build");
|
|
42
|
+
expect(jobs.get("deploy")?.needs).toEqual(["build", "Build_Docs"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("skips global keywords, including the deprecated global forms", () => {
|
|
46
|
+
const yaml = `stages: [build]
|
|
47
|
+
variables:
|
|
48
|
+
FOO: bar
|
|
49
|
+
default:
|
|
50
|
+
image: node:20
|
|
51
|
+
include:
|
|
52
|
+
- local: ci/other.yml
|
|
53
|
+
workflow:
|
|
54
|
+
name: main
|
|
55
|
+
image: node:20
|
|
56
|
+
services:
|
|
57
|
+
- docker:dind
|
|
58
|
+
cache:
|
|
59
|
+
paths: [node_modules]
|
|
60
|
+
before_script:
|
|
61
|
+
- echo hi
|
|
62
|
+
after_script:
|
|
63
|
+
- echo bye
|
|
64
|
+
build:
|
|
65
|
+
stage: build
|
|
66
|
+
script: make
|
|
67
|
+
`;
|
|
68
|
+
expect([...extractJobs(yaml).keys()]).toEqual(["build"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("keeps hidden jobs and reads extends: as a string or a list", () => {
|
|
72
|
+
const yaml = `.base:
|
|
73
|
+
image: node:20
|
|
74
|
+
|
|
75
|
+
.lint-base:
|
|
76
|
+
stage: test
|
|
77
|
+
|
|
78
|
+
lint:
|
|
79
|
+
extends: .base
|
|
80
|
+
script: npm run lint
|
|
81
|
+
|
|
82
|
+
test:
|
|
83
|
+
extends:
|
|
84
|
+
- .base
|
|
85
|
+
- .lint-base
|
|
86
|
+
script: npm test
|
|
87
|
+
`;
|
|
88
|
+
const jobs = extractJobs(yaml);
|
|
89
|
+
expect(jobs.has(".base")).toBe(true);
|
|
90
|
+
expect(jobs.get("lint")?.extends).toEqual([".base"]);
|
|
91
|
+
expect(jobs.get("test")?.extends).toEqual([".base", ".lint-base"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("reads every needs: form", () => {
|
|
95
|
+
const yaml = `a:
|
|
96
|
+
script: x
|
|
97
|
+
b:
|
|
98
|
+
script: x
|
|
99
|
+
c:
|
|
100
|
+
script: x
|
|
101
|
+
plain:
|
|
102
|
+
needs: [a, 'b']
|
|
103
|
+
script: x
|
|
104
|
+
maps:
|
|
105
|
+
needs:
|
|
106
|
+
- job: a
|
|
107
|
+
artifacts: true
|
|
108
|
+
- job: b
|
|
109
|
+
optional: true
|
|
110
|
+
- job: c
|
|
111
|
+
parallel:
|
|
112
|
+
matrix:
|
|
113
|
+
- PROVIDER: aws
|
|
114
|
+
script: x
|
|
115
|
+
cross:
|
|
116
|
+
needs:
|
|
117
|
+
- pipeline: $PARENT_PIPELINE_ID
|
|
118
|
+
job: generate
|
|
119
|
+
- project: group/other
|
|
120
|
+
job: build
|
|
121
|
+
ref: main
|
|
122
|
+
artifacts: true
|
|
123
|
+
- a
|
|
124
|
+
script: x
|
|
125
|
+
none:
|
|
126
|
+
needs: []
|
|
127
|
+
script: x
|
|
128
|
+
scalar:
|
|
129
|
+
needs: a
|
|
130
|
+
script: x
|
|
131
|
+
`;
|
|
132
|
+
const jobs = extractJobs(yaml);
|
|
133
|
+
expect(jobs.get("plain")?.needs).toEqual(["a", "b"]);
|
|
134
|
+
expect(jobs.get("maps")?.needs).toEqual(["a", "b", "c"]);
|
|
135
|
+
expect(jobs.get("maps")?.optionalNeeds).toEqual(["b"]);
|
|
136
|
+
expect(jobs.get("cross")?.needs).toEqual(["a"]);
|
|
137
|
+
expect(jobs.get("none")?.needs).toEqual([]);
|
|
138
|
+
expect(jobs.get("scalar")?.needs).toEqual(["a"]);
|
|
139
|
+
expect(jobs.get("a")?.needs).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("reads job ids with colons, spaces and quotes", () => {
|
|
143
|
+
const yaml = `build:linux:
|
|
144
|
+
stage: build
|
|
145
|
+
script: make
|
|
146
|
+
"deploy prod":
|
|
147
|
+
needs: ["build:linux"]
|
|
148
|
+
script: ./deploy
|
|
149
|
+
'it''s':
|
|
150
|
+
script: x
|
|
151
|
+
`;
|
|
152
|
+
const jobs = extractJobs(yaml);
|
|
153
|
+
expect([...jobs.keys()]).toEqual(["build:linux", "deploy prod", "it's"]);
|
|
154
|
+
expect(jobs.get("deploy prod")?.needs).toEqual(["build:linux"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("does not depend on blank lines between jobs", () => {
|
|
158
|
+
const yaml = `build:
|
|
159
|
+
stage: build
|
|
160
|
+
script: make
|
|
161
|
+
test:
|
|
162
|
+
stage: test
|
|
163
|
+
needs: [build]
|
|
164
|
+
script: make test
|
|
165
|
+
`;
|
|
166
|
+
const jobs = extractJobs(yaml);
|
|
167
|
+
expect(jobs.get("test")?.needs).toEqual(["build"]);
|
|
168
|
+
expect(jobs.get("test")?.stage).toBe("test");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("unparseable YAML gives no jobs", () => {
|
|
172
|
+
expect(extractJobs("build:\n script: [unclosed\n").size).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("extractStages", () => {
|
|
177
|
+
test("reads block and flow lists", () => {
|
|
178
|
+
expect(extractStages("stages:\n - build\n - 'test'\n")).toEqual(["build", "test"]);
|
|
179
|
+
expect(extractStages("stages: [build, deploy]\n")).toEqual(["build", "deploy"]);
|
|
180
|
+
expect(extractStages("build:\n script: x\n")).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("extractGlobalVariables", () => {
|
|
185
|
+
test("reads the expanded value:/description: form without treating its keys as variables", () => {
|
|
186
|
+
const vars = extractGlobalVariables(`variables:
|
|
187
|
+
PLAIN: one
|
|
188
|
+
DEPLOY_ENV:
|
|
189
|
+
value: staging
|
|
190
|
+
description: Target environment
|
|
191
|
+
`);
|
|
192
|
+
expect([...vars.entries()]).toEqual([
|
|
193
|
+
["PLAIN", "one"],
|
|
194
|
+
["DEPLOY_ENV", "staging"],
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("topLevelSections / extractJobSection", () => {
|
|
200
|
+
test("topLevelKey unquotes and keeps colons inside the key", () => {
|
|
201
|
+
expect(topLevelKey("build:linux:")).toBe("build:linux");
|
|
202
|
+
expect(topLevelKey('"deploy prod":')).toBe("deploy prod");
|
|
203
|
+
expect(topLevelKey("Build_Docs:")).toBe("Build_Docs");
|
|
204
|
+
expect(topLevelKey(" script:")).toBeUndefined();
|
|
205
|
+
expect(topLevelKey("# build:")).toBeUndefined();
|
|
206
|
+
expect(topLevelKey("---")).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("cuts at top-level keys, not blank lines", () => {
|
|
210
|
+
const yaml = `build:
|
|
211
|
+
script:
|
|
212
|
+
- make
|
|
213
|
+
|
|
214
|
+
- make install
|
|
215
|
+
test:
|
|
216
|
+
script: make test
|
|
217
|
+
`;
|
|
218
|
+
expect(topLevelSections(yaml).map((s) => s.key)).toEqual(["build", "test"]);
|
|
219
|
+
expect(extractJobSection(yaml, "build")).toContain("make install");
|
|
220
|
+
expect(extractJobSection(yaml, "build")).not.toContain("make test");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("matches the exact key", () => {
|
|
224
|
+
const yaml = `build:linux:
|
|
225
|
+
script: linux
|
|
226
|
+
|
|
227
|
+
build:
|
|
228
|
+
script: plain
|
|
229
|
+
`;
|
|
230
|
+
expect(extractJobSection(yaml, "build")).toBe("build:\n script: plain");
|
|
231
|
+
expect(extractJobSection(yaml, "Build_Docs")).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a job preceded by a comment is still found", () => {
|
|
235
|
+
const yaml = `build:
|
|
236
|
+
script: make
|
|
237
|
+
|
|
238
|
+
# the docs job
|
|
239
|
+
Build_Docs:
|
|
240
|
+
script:
|
|
241
|
+
- make docs
|
|
242
|
+
`;
|
|
243
|
+
expect(extractJobSection(yaml, "build")).toBe("build:\n script: make");
|
|
244
|
+
expect(extractScriptCommands(yaml)).toEqual([
|
|
245
|
+
{ job: "build", command: "make" },
|
|
246
|
+
{ job: "Build_Docs", command: "make docs" },
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
});
|