@intentius/chant-lexicon-gitlab 0.124.31 → 0.124.32

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 (133) hide show
  1. package/dist/codegen/docs-cli.js +2 -0
  2. package/dist/codegen/docs.js +157 -0
  3. package/dist/codegen/fetch.js +93 -0
  4. package/dist/codegen/generate-cli.js +30 -0
  5. package/dist/codegen/generate-lexicon.js +36 -0
  6. package/dist/codegen/generate-typescript.js +105 -0
  7. package/dist/codegen/generate.js +125 -0
  8. package/dist/codegen/naming.js +48 -0
  9. package/dist/codegen/package.js +41 -0
  10. package/dist/codegen/parse.js +539 -0
  11. package/dist/codegen/patches.js +84 -0
  12. package/dist/codegen/typecheck.js +4 -0
  13. package/dist/components/ci-jobs.js +16 -0
  14. package/dist/components/generate-op-pipeline.js +288 -0
  15. package/dist/components/generate-op-waves-pipeline.js +125 -0
  16. package/dist/components/generate-pipeline.d.ts.map +1 -1
  17. package/dist/components/generate-pipeline.js +369 -0
  18. package/dist/composites/catalog.js +418 -0
  19. package/dist/composites/docker-build.js +38 -0
  20. package/dist/composites/index.js +5 -0
  21. package/dist/composites/mr-plan-report.js +42 -0
  22. package/dist/composites/node-pipeline.js +67 -0
  23. package/dist/composites/python-pipeline.js +45 -0
  24. package/dist/composites/review-app.js +32 -0
  25. package/dist/coverage.js +35 -0
  26. package/dist/generated/index.js +25 -0
  27. package/dist/generated/runtime.js +4 -0
  28. package/dist/graph-edges.js +202 -0
  29. package/dist/import/generator.js +196 -0
  30. package/dist/import/parser.js +111 -0
  31. package/dist/index.js +26 -0
  32. package/dist/integrity.json +3 -3
  33. package/dist/intrinsics.js +33 -0
  34. package/dist/lint/audit-catalog.js +53 -0
  35. package/dist/lint/audit-lineage.js +64 -0
  36. package/dist/lint/post-synth/index.js +84 -0
  37. package/dist/lint/post-synth/wgl010.js +34 -0
  38. package/dist/lint/post-synth/wgl011.js +48 -0
  39. package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
  40. package/dist/lint/post-synth/wgl012.js +68 -0
  41. package/dist/lint/post-synth/wgl013.js +57 -0
  42. package/dist/lint/post-synth/wgl014.js +43 -0
  43. package/dist/lint/post-synth/wgl015.js +70 -0
  44. package/dist/lint/post-synth/wgl016.js +70 -0
  45. package/dist/lint/post-synth/wgl017.js +38 -0
  46. package/dist/lint/post-synth/wgl018.js +34 -0
  47. package/dist/lint/post-synth/wgl019.js +43 -0
  48. package/dist/lint/post-synth/wgl020.js +47 -0
  49. package/dist/lint/post-synth/wgl021.js +52 -0
  50. package/dist/lint/post-synth/wgl022.js +38 -0
  51. package/dist/lint/post-synth/wgl023.js +44 -0
  52. package/dist/lint/post-synth/wgl024.js +41 -0
  53. package/dist/lint/post-synth/wgl025.js +42 -0
  54. package/dist/lint/post-synth/wgl026.js +58 -0
  55. package/dist/lint/post-synth/wgl027.js +49 -0
  56. package/dist/lint/post-synth/wgl028.js +58 -0
  57. package/dist/lint/post-synth/wgl029.js +55 -0
  58. package/dist/lint/post-synth/wgl030.js +43 -0
  59. package/dist/lint/post-synth/wgl031.js +33 -0
  60. package/dist/lint/post-synth/wgl032.js +64 -0
  61. package/dist/lint/post-synth/wgl033.js +32 -0
  62. package/dist/lint/post-synth/wgl034.js +37 -0
  63. package/dist/lint/post-synth/wgl035.js +40 -0
  64. package/dist/lint/post-synth/wgl036.js +38 -0
  65. package/dist/lint/post-synth/wgl037.js +41 -0
  66. package/dist/lint/post-synth/wgl038.js +43 -0
  67. package/dist/lint/post-synth/wgl039.js +34 -0
  68. package/dist/lint/post-synth/wgl040.js +35 -0
  69. package/dist/lint/post-synth/wgl041.js +53 -0
  70. package/dist/lint/post-synth/wgl042.js +41 -0
  71. package/dist/lint/post-synth/wgl043.js +39 -0
  72. package/dist/lint/post-synth/wgl044.js +35 -0
  73. package/dist/lint/post-synth/wgl045.js +54 -0
  74. package/dist/lint/post-synth/wgl046.js +40 -0
  75. package/dist/lint/post-synth/wgl047.js +34 -0
  76. package/dist/lint/post-synth/wgl048.js +32 -0
  77. package/dist/lint/post-synth/wgl049.js +53 -0
  78. package/dist/lint/post-synth/wgl050.js +42 -0
  79. package/dist/lint/post-synth/yaml-helpers.js +432 -0
  80. package/dist/lint/rules/artifact-no-expiry.js +53 -0
  81. package/dist/lint/rules/data/known-include-sources.js +44 -0
  82. package/dist/lint/rules/data/untrusted-variables.js +30 -0
  83. package/dist/lint/rules/deprecated-only-except.js +46 -0
  84. package/dist/lint/rules/import-source.js +61 -0
  85. package/dist/lint/rules/index.js +7 -0
  86. package/dist/lint/rules/missing-script.js +67 -0
  87. package/dist/lint/rules/missing-stage.js +60 -0
  88. package/dist/lsp/completions.d.ts.map +1 -1
  89. package/dist/lsp/completions.js +18 -0
  90. package/dist/lsp/hover.d.ts.map +1 -1
  91. package/dist/lsp/hover.js +33 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/mcp/context-tools.js +262 -0
  94. package/dist/migrate/from-github/actions/index.js +17 -0
  95. package/dist/migrate/from-github/actions/registry.js +61 -0
  96. package/dist/migrate/from-github/actions/tier-1.js +298 -0
  97. package/dist/migrate/from-github/actions/tier-2.js +241 -0
  98. package/dist/migrate/from-github/actions/tier-3.js +96 -0
  99. package/dist/migrate/from-github/composites/patterns.js +151 -0
  100. package/dist/migrate/from-github/composites/rewriter.js +22 -0
  101. package/dist/migrate/from-github/diagnostics.js +53 -0
  102. package/dist/migrate/from-github/emit-yaml.js +121 -0
  103. package/dist/migrate/from-github/expressions.js +275 -0
  104. package/dist/migrate/from-github/index.js +96 -0
  105. package/dist/migrate/from-github/provenance.js +35 -0
  106. package/dist/migrate/from-github/rules.js +74 -0
  107. package/dist/migrate/from-github/security.js +189 -0
  108. package/dist/migrate/from-github/stages.js +156 -0
  109. package/dist/migrate/from-github/transformer.js +633 -0
  110. package/dist/op/activities/gitlab.js +38 -0
  111. package/dist/op/activities/index.js +10 -0
  112. package/dist/op/builders.js +26 -0
  113. package/dist/package-cli.js +21 -0
  114. package/dist/plugin.js +536 -0
  115. package/dist/rules/wgl012.ts +2 -2
  116. package/dist/serializer.js +197 -0
  117. package/dist/testdata/create-fixture.js +40 -0
  118. package/dist/testdata/load-fixtures.js +20 -0
  119. package/dist/validate-cli.js +16 -0
  120. package/dist/validate.js +29 -0
  121. package/dist/variables.js +30 -0
  122. package/package.json +8 -8
  123. package/src/codegen/fetch.ts +2 -2
  124. package/src/codegen/package.ts +2 -2
  125. package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
  126. package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
  127. package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
  128. package/src/components/generate-pipeline.ts +60 -27
  129. package/src/components/generate-pr-pipeline.test.ts +14 -9
  130. package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
  131. package/src/lint/post-synth/wgl012.ts +2 -2
  132. package/src/lsp/completions.ts +2 -1
  133. package/src/lsp/hover.ts +2 -1
@@ -44,7 +44,17 @@ import {
44
44
  import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
45
45
  import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
46
46
  import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
47
- import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prLoopJobs, prLoopSetup } from "@intentius/chant/components/pr-pipeline";
47
+ import {
48
+ PR_APPLY_RECORD,
49
+ PR_LOOP_REPORT_DIR,
50
+ prApplyGroup,
51
+ prApplyRecordKey,
52
+ prApplyWaveJobName,
53
+ prLoopJobs,
54
+ prLoopSetup,
55
+ prLoopWaveJobs,
56
+ prLoopWithWaves,
57
+ } from "@intentius/chant/components/pr-pipeline";
48
58
  import { memberGitlabChanges, memberRepoPath, memberShellDir } from "@intentius/chant/lexicon";
49
59
  import type {
50
60
  ComponentPipelineJob as GeneratedJob,
@@ -79,10 +89,10 @@ export function generateGitlabPipeline(
79
89
  const byName = new Map(components.map((c) => [c.name, c]));
80
90
 
81
91
  if (options.prLoop) {
82
- if (options.gatedWaves || options.promoteTo !== undefined) {
83
- throw new Error("a pull-request pipeline has no wave or promote jobs; drop --wave-gate and --promote-to, or --pr-loop");
84
- }
85
- return prLoopGitlabPipeline(env, image, beforeScript, extraScript, options);
92
+ if (options.promoteTo !== undefined) throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
93
+ // #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
94
+ const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
95
+ return prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, { ...options, prLoop });
86
96
  }
87
97
 
88
98
  if (options.gatedWaves) {
@@ -282,8 +292,15 @@ function gatedWaveGitlabPipeline(
282
292
  * apply passes it to `pr-apply --resume` (#3543). GitLab keeps protected
283
293
  * branches' caches apart from other branches' by default, so a merge request
284
294
  * pipeline cannot write the record the target branch's apply reads.
295
+ *
296
+ * With a gate per wave (`waveGate`, #4003) the apply job becomes
297
+ * `apply-wave-1` to `apply-wave-<n>`, one stage each, each needing the one
298
+ * before. They share the commit's cache key, so wave k pulls the record wave
299
+ * k-1 pushed, and a retry of a wave that stopped at its gate pulls the one it
300
+ * pushed itself.
285
301
  */
286
302
  function prLoopGitlabPipeline(
303
+ components: DriverComponent[],
287
304
  env: string,
288
305
  image: string,
289
306
  beforeScript: string[],
@@ -296,14 +313,19 @@ function prLoopGitlabPipeline(
296
313
  const [plan, apply] = prLoopJobs("gitlab", env, loop, member?.name);
297
314
  const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
298
315
  const planJob = member ? `${member.name}-plan` : "plan";
299
- const applyJob = member ? `${member.name}-apply` : "apply";
316
+ const named = (job: string) => (member ? `${member.name}-${job}` : job);
317
+ const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
318
+ const applies = loop.waveGate
319
+ ? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave!)), stage: prApplyWaveJobName(job.wave!) }))
320
+ : [{ job: apply, name: named("apply"), stage: "apply" }];
300
321
  const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
301
322
  if (loop.branch !== undefined) assertGitlabBranch("The merge-request pipeline", loop.branch);
302
323
  const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
303
324
  // A member's jobs start in its directory, after the image's install lines.
304
325
  const enter = rooted ? [] : [`cd ${memberShellDir(member!)}`];
305
- const script = (job: typeof plan): string[] => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.setup ?? []), ...(job.build ?? []), job.command, ...extraScript];
306
- const stages = ["plan", "apply"];
326
+ // The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY variable is set.
327
+ const script = (job: typeof plan): string[] => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.setup ?? []), ...(job.build ?? []), job.run.command, ...extraScript];
328
+ const stages = ["plan", ...applies.map((a) => a.stage)];
307
329
  const doc: Record<string, unknown> = {
308
330
  // Included files merge their top-level keys, so a member's file carries
309
331
  // the rules every member shares and no name: two names would collide.
@@ -314,32 +336,43 @@ function prLoopGitlabPipeline(
314
336
  stage: "plan",
315
337
  image,
316
338
  rules: [{ if: onMergeRequest }],
317
- variables: plan.env,
339
+ variables: plan.run.env,
318
340
  script: script(plan),
319
341
  artifacts: { when: "always", paths: [reportPath] },
320
342
  },
321
- [applyJob]: {
322
- stage: "apply",
323
- image,
324
- resource_group: prApplyGroup(env, member?.name),
325
- rules: [{ if: onMerge }],
326
- variables: apply.env,
327
- script: script(apply),
328
- // The attempt record (#3543), kept under the pushed commit even when
329
- // the job fails, so a retry finishes the apply under the same approval.
330
- cache: {
331
- key: prApplyRecordKey(env, "$CI_COMMIT_SHA", member?.name),
332
- paths: [member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD],
333
- when: "always",
334
- },
335
- artifacts: { when: "always", paths: [reportPath] },
336
- },
343
+ ...Object.fromEntries(
344
+ applies.map(({ job, name, stage }, i) => [
345
+ name,
346
+ {
347
+ stage,
348
+ image,
349
+ ...(i > 0 ? { needs: [applies[i - 1]!.name] } : {}),
350
+ resource_group: prApplyGroup(env, member?.name),
351
+ rules: [{ if: onMerge }],
352
+ variables: job.run.env,
353
+ script: script(job),
354
+ // The attempt record (#3543), kept under the pushed commit even when
355
+ // the job fails, so a retry finishes the apply under the same approval.
356
+ cache: {
357
+ key: prApplyRecordKey(env, "$CI_COMMIT_SHA", member?.name),
358
+ paths: [member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD],
359
+ when: "always",
360
+ },
361
+ artifacts: { when: "always", paths: [reportPath] },
362
+ },
363
+ ]),
364
+ ),
337
365
  };
338
366
  const jobs: GeneratedJob[] = [
339
367
  { jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
340
- { jobName: applyJob, component: "merge request apply", stage: "apply", needs: [] },
368
+ ...applies.map(({ job, name, stage }, i) => ({
369
+ jobName: name,
370
+ component: job.wave ? `merge request apply, wave ${job.wave}` : "merge request apply",
371
+ stage,
372
+ needs: i > 0 ? [applies[i - 1]!.name] : [],
373
+ })),
341
374
  ];
342
- const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, applyJob].map((key) => [key, doc[key]] as const));
375
+ const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, ...applies.map((a) => a.name)].map((key) => [key, doc[key]] as const));
343
376
  return { yaml, stages, jobs, env };
344
377
  }
345
378
 
@@ -12,7 +12,7 @@ import { describe, expect, test } from "vitest";
12
12
  import { parseYAML } from "@intentius/chant/yaml";
13
13
  import type { DriverComponent } from "@intentius/chant/components/driver";
14
14
  import { generateGitlabPipeline } from "./generate-pipeline";
15
- import { PR_LOOP_SETUP } from "@intentius/chant/components/pr-pipeline";
15
+ import { PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
16
16
 
17
17
  const ESTATE: DriverComponent[] = [
18
18
  { name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
@@ -54,10 +54,12 @@ describe("the GitLab merge-request pipeline", () => {
54
54
  expect(doc.plan.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
55
55
  expect(doc.plan.variables).toEqual({ BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" });
56
56
  expect(doc.plan.script).toEqual([
57
- ...PR_LOOP_SETUP,'chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge gitlab']);
57
+ ...PR_LOOP_SETUP,'chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge gitlab']);
58
58
  expect(doc.apply.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
59
59
  expect(doc.apply.variables).toEqual({ BASE_SHA: "$CI_COMMIT_BEFORE_SHA" });
60
- expect(doc.apply.script.at(-1)).toContain("--require-review");
60
+ // #4156: ci run builds pr-apply with --require-review, and GitLab hands CHANT_RUNNER_KEY to the job as a CI/CD variable when it has one.
61
+ expect(doc.apply.script.at(-1)).toBe('chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab');
62
+ expect(result.yaml).not.toContain("CHANT_RUNNER_KEY");
61
63
  expect(doc.apply.resource_group).toBe("chant-apply-prod");
62
64
  expect(doc.variables.GIT_DEPTH).toBe("0");
63
65
  expect(doc.apply.artifacts).toEqual({ when: "always", paths: [".chant/pr"] });
@@ -95,15 +97,17 @@ describe("the GitLab merge-request pipeline", () => {
95
97
  });
96
98
 
97
99
  test("the apply resumes from a record the job cache keeps for the pushed commit, failed or not (#3543)", () => {
98
- expect(doc.apply.script.at(-1)).toContain("--resume .chant/pr-resume/pr-apply.json");
100
+ expect(doc.apply.script.at(-1)).toContain("--in-place");
101
+ expect(prLoopJobs("gitlab", "prod")[1]!.command).toContain("--resume .chant/pr-resume/pr-apply.json");
99
102
  expect(doc.apply.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
100
103
  // The plan runs the merge request's code: no cache, no record.
101
104
  expect(doc.plan.cache).toBeUndefined();
102
105
  expect(doc.plan.script.at(-1)).not.toContain("--resume");
103
106
  });
104
107
 
105
- test("refuses a wave or promote job beside it", () => {
106
- expect(() => generateGitlabPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g" } })).toThrow(/pull-request pipeline has no wave or promote jobs/);
108
+ test("refuses a promote job beside it, and a canary beside --wave-gate (#4003)", () => {
109
+ expect(() => generateGitlabPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
110
+ expect(() => generateGitlabPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
107
111
  });
108
112
 
109
113
  test("inside a workspace member: member-named jobs that cd into the member, with no changes rule (#3465)", () => {
@@ -118,9 +122,10 @@ describe("the GitLab merge-request pipeline", () => {
118
122
  const plan = mdoc["network-plan"] as Job;
119
123
  const apply = mdoc["network-apply"] as Job;
120
124
  expect(plan.script).toContain("cd infra/network");
121
- expect(plan.script.indexOf("cd infra/network")).toBeLessThan(plan.script.findIndex((l) => l.includes("pr-plan")));
122
- expect(plan.script.find((l) => l.includes("pr-plan"))).toContain("--member network");
123
- expect(apply.script.find((l) => l.includes("pr-apply"))).toContain("--member network");
125
+ // ci run, started in the member's directory, finds the member and passes --member to pr-plan and pr-apply.
126
+ expect(plan.script.indexOf("cd infra/network")).toBeLessThan(plan.script.findIndex((l) => l.includes("ci run plan")));
127
+ expect(apply.script.indexOf("cd infra/network")).toBeLessThan(apply.script.findIndex((l) => l.includes("ci run apply")));
128
+ expect(prLoopJobs("gitlab", "prod", {}, "network")[0]!.command).toContain("--member network");
124
129
  // A change outside the member can reach it, so the jobs keep only their pipeline-source rules.
125
130
  expect(plan.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
126
131
  expect(apply.resource_group).toBe("chant-apply.network.prod");
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The GitLab merge-request pipeline with a gate per wave on merge (#4003):
3
+ * the plan job in each merge request pipeline, and one apply job per wave of
4
+ * the component graph, one stage each, sharing the commit's record in the
5
+ * job cache. The whole file has a golden, and it validates against the GitLab
6
+ * CI schema this lexicon vendors.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { createRequire } from "node:module";
11
+ import { join } from "node:path";
12
+ import { describe, expect, test } from "vitest";
13
+ import { parseYAML } from "@intentius/chant/yaml";
14
+ import type { DriverComponent } from "@intentius/chant/components/driver";
15
+ import { generateGitlabPipeline } from "./generate-pipeline";
16
+
17
+ const root = (name: string, dependsOn: string[] = []): DriverComponent => ({
18
+ name,
19
+ dependsOn,
20
+ deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }],
21
+ });
22
+ const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"]), root("web", ["app"])];
23
+
24
+ const GOLDEN = join(import.meta.dirname, "__fixtures__", "pr-loop-waves.gitlab.golden.yml");
25
+ const SCHEMA = join(import.meta.dirname, "..", "codegen", "vendored", "gitlab-ci-schema.json");
26
+
27
+ interface Job {
28
+ stage: string;
29
+ needs?: string[];
30
+ rules: Array<{ if: string }>;
31
+ script: string[];
32
+ resource_group?: string;
33
+ cache?: Record<string, unknown>;
34
+ }
35
+
36
+ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () => {
37
+ const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } });
38
+ const doc = parseYAML(result.yaml) as Record<string, unknown> & { stages: string[] };
39
+
40
+ test("matches its golden", () => {
41
+ if (process.env.UPDATE_GOLDEN) writeFileSync(GOLDEN, result.yaml);
42
+ expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
43
+ });
44
+
45
+ test("validates against the vendored GitLab CI schema", () => {
46
+ const req = createRequire(import.meta.url);
47
+ const mod = req("ajv") as { default?: unknown };
48
+ const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } };
49
+ const validate = new Ajv({ strict: false, allErrors: true }).compile(JSON.parse(readFileSync(SCHEMA, "utf-8")));
50
+ expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
51
+ });
52
+
53
+ test("one apply job and stage per wave, each needing the one before, on the merge, one at a time", () => {
54
+ expect(doc.stages).toEqual(["plan", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
55
+ expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
56
+ ["plan", []],
57
+ ["apply-wave-1", []],
58
+ ["apply-wave-2", ["apply-wave-1"]],
59
+ ["apply-wave-3", ["apply-wave-2"]],
60
+ ]);
61
+ for (const k of [1, 2, 3]) {
62
+ const job = doc[`apply-wave-${k}`] as Job;
63
+ expect(job.stage).toBe(`apply-wave-${k}`);
64
+ expect(job.needs).toEqual(k > 1 ? [`apply-wave-${k - 1}`] : undefined);
65
+ expect(job.resource_group).toBe("chant-apply-prod");
66
+ // #4156: through chant ci run, which runs pr-apply --require-review --wave-gate release --resume ... --wave <k>.
67
+ expect(job.script.at(-1)).toBe(`chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab --wave-gate release --wave ${k}`);
68
+ // Every wave shares the commit's record, so wave k pulls what wave k-1 pushed.
69
+ expect(job.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
70
+ }
71
+ expect((doc.plan as Job).script.at(-1)).toContain("--forge gitlab --wave-gate release");
72
+ expect((doc.plan as Job).script.at(-1)).not.toContain("--wave ");
73
+ });
74
+
75
+ test("inside a workspace member the wave jobs carry the member's name", () => {
76
+ const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
77
+ const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" }, member });
78
+ expect(scoped.jobs.map((j) => [j.jobName, j.needs])).toEqual([
79
+ ["network-plan", []],
80
+ ["network-apply-wave-1", []],
81
+ ["network-apply-wave-2", ["network-apply-wave-1"]],
82
+ ["network-apply-wave-3", ["network-apply-wave-2"]],
83
+ ]);
84
+ // ci run, started in the member's directory, passes --member network to pr-apply.
85
+ expect(scoped.yaml).toContain("cd infra/network");
86
+ expect(scoped.yaml).toContain("chant ci run apply --in-place --base \"$BASE_SHA\" --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 3");
87
+ });
88
+ });
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { readFileSync } from "fs";
9
9
  import { join } from "path";
10
+ import { moduleSourceDir } from "@intentius/chant/module-source";
10
11
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
12
  import { isPropertyDeclarable } from "@intentius/chant/declarable";
12
13
 
@@ -23,8 +24,7 @@ interface LexiconEntry {
23
24
  function loadDeprecatedProperties(): Map<string, Set<string>> {
24
25
  const map = new Map<string, Set<string>>();
25
26
  try {
26
- const pkgDir = join(__dirname, "..", "..", "..");
27
- const lexiconPath = join(pkgDir, "src", "generated", "lexicon-gitlab.json");
27
+ const lexiconPath = join(moduleSourceDir(import.meta.url), "..", "..", "generated", "lexicon-gitlab.json");
28
28
  const content = readFileSync(lexiconPath, "utf-8");
29
29
  const data = JSON.parse(content) as Record<string, LexiconEntry>;
30
30
 
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
- const require = createRequire(import.meta.url);
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
6
 
6
7
  let cachedIndex: LexiconIndex | null = null;
7
8
 
package/src/lsp/hover.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
- const require = createRequire(import.meta.url);
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
6
 
6
7
  let cachedIndex: LexiconIndex | null = null;
7
8