@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.
Files changed (59) hide show
  1. package/dist/codegen/naming.d.ts.map +1 -1
  2. package/dist/components/generate-pipeline.d.ts.map +1 -1
  3. package/dist/composites/node-pipeline.d.ts +16 -16
  4. package/dist/composites/node-pipeline.d.ts.map +1 -1
  5. package/dist/generated/index.d.ts +19 -19
  6. package/dist/generated/index.d.ts.map +1 -1
  7. package/dist/import/generator.d.ts.map +1 -1
  8. package/dist/import/parser.d.ts.map +1 -1
  9. package/dist/integrity.json +10 -10
  10. package/dist/intrinsics.d.ts.map +1 -1
  11. package/dist/lint/post-synth/wgl013.d.ts +3 -1
  12. package/dist/lint/post-synth/wgl013.d.ts.map +1 -1
  13. package/dist/lint/post-synth/wgl017.d.ts.map +1 -1
  14. package/dist/lint/post-synth/wgl020.d.ts.map +1 -1
  15. package/dist/lint/post-synth/wgl021.d.ts.map +1 -1
  16. package/dist/lint/post-synth/wgl050.d.ts.map +1 -1
  17. package/dist/lint/post-synth/yaml-helpers.d.ts +57 -8
  18. package/dist/lint/post-synth/yaml-helpers.d.ts.map +1 -1
  19. package/dist/manifest.json +1 -1
  20. package/dist/migrate/from-github/provenance.d.ts.map +1 -1
  21. package/dist/op/builders.d.ts.map +1 -1
  22. package/dist/rules/wgl013.ts +4 -2
  23. package/dist/rules/wgl017.ts +3 -11
  24. package/dist/rules/wgl020.ts +4 -5
  25. package/dist/rules/wgl021.ts +4 -5
  26. package/dist/rules/wgl048.ts +2 -2
  27. package/dist/rules/wgl049.ts +4 -4
  28. package/dist/rules/wgl050.ts +5 -8
  29. package/dist/rules/yaml-helpers.ts +176 -113
  30. package/dist/variables.d.ts.map +1 -1
  31. package/package.json +4 -4
  32. package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +43 -0
  33. package/src/components/generate-op-pipeline.ts +4 -4
  34. package/src/components/generate-pipeline.test.ts +33 -0
  35. package/src/components/generate-pipeline.ts +117 -11
  36. package/src/components/generate-pr-pipeline.test.ts +70 -0
  37. package/src/lint/post-synth/post-synth.test.ts +29 -0
  38. package/src/lint/post-synth/wgl013.test.ts +53 -0
  39. package/src/lint/post-synth/wgl013.ts +4 -2
  40. package/src/lint/post-synth/wgl014.test.ts +23 -0
  41. package/src/lint/post-synth/wgl015.test.ts +35 -0
  42. package/src/lint/post-synth/wgl017.test.ts +14 -0
  43. package/src/lint/post-synth/wgl017.ts +3 -11
  44. package/src/lint/post-synth/wgl020.test.ts +26 -0
  45. package/src/lint/post-synth/wgl020.ts +4 -5
  46. package/src/lint/post-synth/wgl021.test.ts +29 -0
  47. package/src/lint/post-synth/wgl021.ts +4 -5
  48. package/src/lint/post-synth/wgl028.test.ts +27 -0
  49. package/src/lint/post-synth/wgl042.test.ts +14 -0
  50. package/src/lint/post-synth/wgl048.test.ts +14 -0
  51. package/src/lint/post-synth/wgl048.ts +2 -2
  52. package/src/lint/post-synth/wgl049.test.ts +25 -0
  53. package/src/lint/post-synth/wgl049.ts +4 -4
  54. package/src/lint/post-synth/wgl050.test.ts +16 -0
  55. package/src/lint/post-synth/wgl050.ts +5 -8
  56. package/src/lint/post-synth/yaml-helpers.test.ts +249 -0
  57. package/src/lint/post-synth/yaml-helpers.ts +176 -113
  58. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +3 -1
  59. 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.split("\n\n");
20
- if (sections.some((s) => /^cache:/.test(s))) return true;
21
- const defaultSection = sections.find((s) => /^default:/.test(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 section of yaml.split("\n\n")) {
28
- const top = section.split("\n")[0]?.match(/^(\.?[a-z][a-z0-9_.-]*):/i);
29
- if (!top) continue;
30
- const jobName = top[1];
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
+ });