@intentius/chant-lexicon-gitlab 0.124.31 → 0.124.33

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 (137) 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.d.ts.map +1 -1
  15. package/dist/components/generate-op-pipeline.js +294 -0
  16. package/dist/components/generate-op-waves-pipeline.js +125 -0
  17. package/dist/components/generate-pipeline.d.ts.map +1 -1
  18. package/dist/components/generate-pipeline.js +376 -0
  19. package/dist/composites/catalog.js +418 -0
  20. package/dist/composites/docker-build.js +38 -0
  21. package/dist/composites/index.js +5 -0
  22. package/dist/composites/mr-plan-report.js +42 -0
  23. package/dist/composites/node-pipeline.js +67 -0
  24. package/dist/composites/python-pipeline.js +45 -0
  25. package/dist/composites/review-app.js +32 -0
  26. package/dist/coverage.js +35 -0
  27. package/dist/generated/index.js +25 -0
  28. package/dist/generated/runtime.js +4 -0
  29. package/dist/graph-edges.js +202 -0
  30. package/dist/import/generator.js +196 -0
  31. package/dist/import/parser.js +111 -0
  32. package/dist/index.js +26 -0
  33. package/dist/integrity.json +3 -3
  34. package/dist/intrinsics.js +33 -0
  35. package/dist/lint/audit-catalog.js +53 -0
  36. package/dist/lint/audit-lineage.js +64 -0
  37. package/dist/lint/post-synth/index.js +84 -0
  38. package/dist/lint/post-synth/wgl010.js +34 -0
  39. package/dist/lint/post-synth/wgl011.js +48 -0
  40. package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
  41. package/dist/lint/post-synth/wgl012.js +68 -0
  42. package/dist/lint/post-synth/wgl013.js +57 -0
  43. package/dist/lint/post-synth/wgl014.js +43 -0
  44. package/dist/lint/post-synth/wgl015.js +70 -0
  45. package/dist/lint/post-synth/wgl016.js +70 -0
  46. package/dist/lint/post-synth/wgl017.js +38 -0
  47. package/dist/lint/post-synth/wgl018.js +34 -0
  48. package/dist/lint/post-synth/wgl019.js +43 -0
  49. package/dist/lint/post-synth/wgl020.js +47 -0
  50. package/dist/lint/post-synth/wgl021.js +52 -0
  51. package/dist/lint/post-synth/wgl022.js +38 -0
  52. package/dist/lint/post-synth/wgl023.js +44 -0
  53. package/dist/lint/post-synth/wgl024.js +41 -0
  54. package/dist/lint/post-synth/wgl025.js +42 -0
  55. package/dist/lint/post-synth/wgl026.js +58 -0
  56. package/dist/lint/post-synth/wgl027.js +49 -0
  57. package/dist/lint/post-synth/wgl028.js +58 -0
  58. package/dist/lint/post-synth/wgl029.js +55 -0
  59. package/dist/lint/post-synth/wgl030.js +43 -0
  60. package/dist/lint/post-synth/wgl031.js +33 -0
  61. package/dist/lint/post-synth/wgl032.js +64 -0
  62. package/dist/lint/post-synth/wgl033.js +32 -0
  63. package/dist/lint/post-synth/wgl034.js +37 -0
  64. package/dist/lint/post-synth/wgl035.js +40 -0
  65. package/dist/lint/post-synth/wgl036.js +38 -0
  66. package/dist/lint/post-synth/wgl037.js +41 -0
  67. package/dist/lint/post-synth/wgl038.js +43 -0
  68. package/dist/lint/post-synth/wgl039.js +34 -0
  69. package/dist/lint/post-synth/wgl040.js +35 -0
  70. package/dist/lint/post-synth/wgl041.js +53 -0
  71. package/dist/lint/post-synth/wgl042.js +41 -0
  72. package/dist/lint/post-synth/wgl043.js +39 -0
  73. package/dist/lint/post-synth/wgl044.js +35 -0
  74. package/dist/lint/post-synth/wgl045.js +54 -0
  75. package/dist/lint/post-synth/wgl046.js +40 -0
  76. package/dist/lint/post-synth/wgl047.js +34 -0
  77. package/dist/lint/post-synth/wgl048.js +32 -0
  78. package/dist/lint/post-synth/wgl049.js +53 -0
  79. package/dist/lint/post-synth/wgl050.js +42 -0
  80. package/dist/lint/post-synth/yaml-helpers.js +432 -0
  81. package/dist/lint/rules/artifact-no-expiry.js +53 -0
  82. package/dist/lint/rules/data/known-include-sources.js +44 -0
  83. package/dist/lint/rules/data/untrusted-variables.js +30 -0
  84. package/dist/lint/rules/deprecated-only-except.js +46 -0
  85. package/dist/lint/rules/import-source.js +61 -0
  86. package/dist/lint/rules/index.js +7 -0
  87. package/dist/lint/rules/missing-script.js +67 -0
  88. package/dist/lint/rules/missing-stage.js +60 -0
  89. package/dist/lsp/completions.d.ts.map +1 -1
  90. package/dist/lsp/completions.js +18 -0
  91. package/dist/lsp/hover.d.ts.map +1 -1
  92. package/dist/lsp/hover.js +33 -0
  93. package/dist/manifest.json +1 -1
  94. package/dist/mcp/context-tools.js +262 -0
  95. package/dist/migrate/from-github/actions/index.js +17 -0
  96. package/dist/migrate/from-github/actions/registry.js +61 -0
  97. package/dist/migrate/from-github/actions/tier-1.js +298 -0
  98. package/dist/migrate/from-github/actions/tier-2.js +241 -0
  99. package/dist/migrate/from-github/actions/tier-3.js +96 -0
  100. package/dist/migrate/from-github/composites/patterns.js +151 -0
  101. package/dist/migrate/from-github/composites/rewriter.js +22 -0
  102. package/dist/migrate/from-github/diagnostics.js +53 -0
  103. package/dist/migrate/from-github/emit-yaml.js +121 -0
  104. package/dist/migrate/from-github/expressions.js +275 -0
  105. package/dist/migrate/from-github/index.js +96 -0
  106. package/dist/migrate/from-github/provenance.js +35 -0
  107. package/dist/migrate/from-github/rules.js +74 -0
  108. package/dist/migrate/from-github/security.js +189 -0
  109. package/dist/migrate/from-github/stages.js +156 -0
  110. package/dist/migrate/from-github/transformer.js +633 -0
  111. package/dist/op/activities/gitlab.js +38 -0
  112. package/dist/op/activities/index.js +10 -0
  113. package/dist/op/builders.js +26 -0
  114. package/dist/package-cli.js +21 -0
  115. package/dist/plugin.js +536 -0
  116. package/dist/rules/wgl012.ts +2 -2
  117. package/dist/serializer.js +197 -0
  118. package/dist/testdata/create-fixture.js +40 -0
  119. package/dist/testdata/load-fixtures.js +20 -0
  120. package/dist/validate-cli.js +16 -0
  121. package/dist/validate.js +29 -0
  122. package/dist/variables.js +30 -0
  123. package/package.json +8 -8
  124. package/src/codegen/fetch.ts +2 -2
  125. package/src/codegen/package.ts +2 -2
  126. package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
  127. package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +46 -0
  128. package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
  129. package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
  130. package/src/components/generate-op-pipeline.test.ts +10 -0
  131. package/src/components/generate-op-pipeline.ts +8 -1
  132. package/src/components/generate-pipeline.ts +71 -30
  133. package/src/components/generate-pr-pipeline.test.ts +61 -18
  134. package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
  135. package/src/lint/post-synth/wgl012.ts +2 -2
  136. package/src/lsp/completions.ts +2 -1
  137. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Generate mode — Op → GitLab CI YAML (#927, #2084, #2256).
3
+ *
4
+ * The Op counterpart to `./generate-pipeline.ts` (#563): that module
5
+ * synthesizes a deploy-time component graph as one `.gitlab-ci.yml`; this one
6
+ * synthesizes one job per Op, selected by its own `rules:`.
7
+ *
8
+ * What GitLab genuinely lacks is in-file cron. A schedule is a project-level
9
+ * object (Settings → CI/CD → Schedules) that runs the project's *existing*
10
+ * `.gitlab-ci.yml` with a chosen cron and CI/CD variables, so a cron-triggered
11
+ * Op becomes a job gated on `$CI_PIPELINE_SOURCE == "schedule"` plus a per-Op
12
+ * selector variable, and the generated file's header says what to set up. What
13
+ * GitLab does not lack, and what this generator wrongly refused until #2256,
14
+ * is the other two triggers (#2084): `$CI_PIPELINE_SOURCE` distinguishes
15
+ * `merge_request_event` from `push` on every pipeline, and `rules:` selects a
16
+ * job on either. So:
17
+ *
18
+ * - `cron` → `$CI_PIPELINE_SOURCE == "schedule" && $CHANT_SCHEDULED_OP ==
19
+ * "<name>"`, unchanged;
20
+ * - `pull_request` → `$CI_PIPELINE_SOURCE == "merge_request_event"`, with the
21
+ * branch filter mapped to `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`, which is
22
+ * the branch the merge request would merge INTO — the same thing github's
23
+ * `on.pull_request.branches` filters on;
24
+ * - `push` → `$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH ==
25
+ * "<branch>"`, defaulting to the same `main` github's generator assumes.
26
+ *
27
+ * Two or more branches are two or more rule entries, because `rules:` is an
28
+ * OR over its entries and a regex alternation would need every branch name
29
+ * escaped into a pattern.
30
+ *
31
+ * Every job carries a `resource_group`, GitLab's stand-in for github's per-Op
32
+ * `concurrency` group: one run at a time per Op, the next one queued rather
33
+ * than cancelled. On an apply job that is also what stops two runs racing for
34
+ * the same state lock.
35
+ *
36
+ * Unlike github, all of this lands in ONE file. A GitHub trigger is
37
+ * workflow-scoped, so an Op there needs its own workflow; a GitLab trigger is
38
+ * job-scoped, so the whole set is one document with one job apiece.
39
+ *
40
+ * Each job runs exactly one invocation, `chant run <name>` by default — never
41
+ * inlined audit/reconcile logic. The finding-mode is already baked into the
42
+ * Op's own activity args at build time by the composite that created it; this
43
+ * generator only wires what the mode needs to act.
44
+ *
45
+ * `findingMode: "comment"` works here since #2256: `reconcilePr` writes a
46
+ * merge-request note when the run is a `merge_request_event` pipeline, by the
47
+ * same hidden-marker edit-in-place recipe it uses on a GitHub pull request.
48
+ * What survives of the old blanket refusal is the constraint github already
49
+ * has — the mode posts onto the merge request that triggered the run, so a
50
+ * cron or push job carrying it is refused by name at build time rather than
51
+ * emitted to fail at its Report step ({@link assertTriggerSupportsMode}).
52
+ * GitLab has no per-job `permissions:` concept, so the note's write access
53
+ * comes from a `GITLAB_TOKEN` CI/CD variable, which the header names.
54
+ *
55
+ * #2242's two per-Op options cross over one and a half times. A `setup` entry
56
+ * spelled `{ uses }` is a GitHub Actions marketplace action; GitLab CI has
57
+ * `script` and nothing else, so there is no shape to translate it into and it
58
+ * is refused by name. A `{ run }` entry translates exactly, and is emitted
59
+ * ahead of the `beforeScript` lines — in `script:` rather than
60
+ * `before_script:`, which is where `./generate-pipeline.ts` puts the same
61
+ * option's lines in this same lexicon, and which concatenates identically at
62
+ * run time. An additive `permissions` map has exactly one entry with a GitLab
63
+ * meaning: `id-token: write` becomes an `id_tokens:` declaration ({@link
64
+ * gitlabIdTokens}), the OIDC surface the old refusal already named as the shape
65
+ * to reach for. Every other scope is still refused, because there is no
66
+ * per-job token-scope mapping to put it in and a silently dropped scope emits
67
+ * a job that reads as granted and runs with nothing.
68
+ *
69
+ * A spec's `environment` (#2257) does cross, because GitLab has the concept
70
+ * under the same key and with the same two fields: `environment: { name, url
71
+ * }` on the job, an environment object in the project, and — on a protected
72
+ * environment — an approval rule that holds the deployment job until an
73
+ * approver releases it. So the reviewer gate the option exists for is
74
+ * expressible here, unlike the `uses` setup step above, and it is emitted
75
+ * rather than refused. What GitLab does not have is any way for this file to
76
+ * declare the protection: an environment's approval rules are project
77
+ * settings (Settings > CI/CD > Protected environments), exactly as GitHub's
78
+ * required reviewers are repository settings, so the generated header names
79
+ * the environment to protect the way it already names the schedule to create.
80
+ * chant's own gate (#2119) runs inside the job either way.
81
+ *
82
+ * The gated apply (#2243) lands in the two surfaces GitLab has. `chant run`
83
+ * returns 3 when a run stops at an unapproved gate, so a push-to-default apply
84
+ * would paint the branch red on every merge until someone approves; the push
85
+ * job runs with `--gated-exit 0`, mapping that one outcome and nothing else.
86
+ * Where GitHub Actions gets the pending block on its run page through
87
+ * `GITHUB_STEP_SUMMARY`, GitLab has no step summary at all, so the job sets
88
+ * `CHANT_GATE_SUMMARY` to a path it also declares under `artifacts:` and the
89
+ * block is downloadable from the pipeline. The human render is already in the
90
+ * job log, since the invocation carries no `--json`. There is no follow-up
91
+ * notice job: it would need a forge API call and a token this generator does
92
+ * not require of a `report`-mode Op.
93
+ *
94
+ * A spec's own `variables` (#2290) is the one per-Op option that is cheap
95
+ * here rather than merely possible: this generator already puts a job-level
96
+ * `variables:` block on a gated job (`CHANT_GATE_SUMMARY` above), so a spec's
97
+ * own entries merge into the same mapping instead of asking for a shape
98
+ * GitLab has no concept of. `options.variables` (forge-wide) is unaffected —
99
+ * it keeps landing on the file's own top-level `variables:`, read by every
100
+ * job the way a GitLab CI/CD variable always is — and a spec's own entry wins
101
+ * the job over a same-named top-level one, the ordinary variable-precedence
102
+ * GitLab already gives a job over the pipeline. `CHANT_GATE_SUMMARY` itself is
103
+ * internal and always wins at the job over a same-named spec entry, so a
104
+ * caller cannot point the gate block somewhere the `artifacts:` declaration
105
+ * beside it does not also point.
106
+ */
107
+ import { assertGitlabEnvironment, CI_IMAGE, ciJobName, environmentDoc, fillCommand, gitlabIdTokens, gitlabRules, gitlabSetupScript, nonEmpty, pushBranches, } from "@intentius/chant/ci-jobs";
108
+ import { memberGitlabChanges, memberRepoPath, memberShellDir, resolveOpTrigger } from "@intentius/chant/lexicon";
109
+ import { emitGitlabFile } from "./ci-jobs.js";
110
+ /**
111
+ * Default stage every generated job runs in, and the default base of the
112
+ * generated file's name (#2293). Overridable via `options.opsStage` (and the
113
+ * file name separately via `options.opsFileName`) — see
114
+ * {@link GenerateGitlabOpOptions} (`ComponentPipelineOptions` in
115
+ * `../../../../packages/core/src/lexicon.ts`).
116
+ *
117
+ * `"scheduled-ops"` before #2293: a constant from when this generator was
118
+ * cron-only, left in place through #2084/#2256 even after every
119
+ * merge-request and push job started sharing it, so a GitLab UI showed
120
+ * `live-check`/`live-plan`/`live-apply`/`live-adopt` grouped under a stage
121
+ * called "scheduled" and a consuming project's `include:` named a file that
122
+ * said the opposite of what it held.
123
+ */
124
+ const DEFAULT_STAGE = "ops";
125
+ /** The CI/CD variable a Pipeline Schedule sets to select which job it runs. */
126
+ const SELECTOR_VAR = "CHANT_SCHEDULED_OP";
127
+ /**
128
+ * `chant run` returns 3 when a run stops at an unapproved gate; `--gated-exit
129
+ * 0` maps that one outcome to success (#2243). `push` only — a cron watch or
130
+ * a merge-request plan that stops at a gate is a signal, not noise on a merge.
131
+ */
132
+ const GATED_EXIT_FLAG = ["--gated-exit", "0"];
133
+ /** The variable core's `writeGatedRunSummary` writes the pending-gate block to when the forge sets no step summary (#2256). */
134
+ const GATE_SUMMARY_VAR = "CHANT_GATE_SUMMARY";
135
+ /** How long a pending-gate artifact is worth keeping: long enough to outlive the approval it is waiting for. */
136
+ const GATE_ARTIFACT_EXPIRY = "30 days";
137
+ /** Refuse a blank `opsStage` (#2293): every job's `stage:` and the top-level `stages:` entry would otherwise be an empty string. */
138
+ function assertOpsStage(stage) {
139
+ if (stage.trim() === "") {
140
+ throw new Error(`The gitlab Op generator's \`opsStage\` option is empty. Name the stage every generated job shares, ` +
141
+ `or drop the option to keep the default "${DEFAULT_STAGE}".`);
142
+ }
143
+ }
144
+ /** Refuse a blank `opsFileName` (#2293): `main.ts` in a consuming project writes `result.files[].name` straight to disk. */
145
+ function assertOpsFileName(fileName) {
146
+ if (fileName.trim() === "") {
147
+ throw new Error(`The gitlab Op generator's \`opsFileName\` option is empty. Name the generated file, or drop the ` +
148
+ `option to derive it from \`opsStage\`.`);
149
+ }
150
+ }
151
+ /**
152
+ * Refuse `findingMode: "comment"` on a trigger that has no merge request
153
+ * (#2231, #2256). The mode's activity reads the merge request out of the
154
+ * pipeline's own variables at run time, so a cron- or push-triggered job
155
+ * carrying it would generate fine and then fail on every run. Refusing here
156
+ * names the Op, the mode and the trigger at build time instead — the same
157
+ * refusal github's generator makes, on the same terms.
158
+ */
159
+ function assertTriggerSupportsMode(name, mode, trigger) {
160
+ if (mode !== "comment" || trigger.kind === "pull_request")
161
+ return;
162
+ throw new Error(`Scheduled Op "${name}" has findingMode "comment", which posts its finding as a note on the merge ` +
163
+ `request that triggered the run, but its trigger is "${trigger.kind}". A ${trigger.kind} pipeline has ` +
164
+ `no merge request to post on. Give it a { kind: "pull_request" } trigger, or use findingMode "issue" ` +
165
+ `or "merge-request".`);
166
+ }
167
+ /**
168
+ * The follow-up line an Op with an `environment` adds under its own header
169
+ * line (#2257). The `environment:` key on the job binds it to the
170
+ * environment; it cannot declare the approval rule, which is a project
171
+ * setting, so the header says where that is set the same way it says where a
172
+ * schedule is created.
173
+ */
174
+ function environmentLine(environment) {
175
+ return (`# deploys to environment "${environment.name}" — protect it under Settings > CI/CD >` +
176
+ " Protected environments to require an approval before the job runs");
177
+ }
178
+ /** The artifact path a push job's pending-gate block is written to (#2243, #2256). */
179
+ function gateSummaryPath(jobName) {
180
+ return `chant-gate-${jobName}.md`;
181
+ }
182
+ /** One line per Op in the generated file's header comment, naming what fires it. */
183
+ function headerLineFor(spec, trigger, jobName, mode) {
184
+ const tokenNote = mode === "report" ? "" : " — needs a GITLAB_TOKEN CI/CD variable (masked, scope: api)";
185
+ switch (trigger.kind) {
186
+ case "cron":
187
+ return `# ${jobName}: cron "${trigger.schedule}", ${SELECTOR_VAR}="${spec.name}", finding-mode ${mode}${tokenNote}`;
188
+ case "pull_request": {
189
+ const onto = trigger.branches?.length ? trigger.branches.join(", ") : "any branch";
190
+ return `# ${jobName}: merge_request_event onto ${onto}, finding-mode ${mode}${tokenNote}`;
191
+ }
192
+ case "push": {
193
+ const branches = pushBranches(trigger.branches);
194
+ // #4169: GitLab cancels a waiting pipeline when a newer commit's starts, under the project's auto-cancel setting.
195
+ const build = trigger.build === "newest"
196
+ ? "; builds the newest commit and each ci/build/<sha> tag: keep Settings > CI/CD > Auto-cancel redundant pipelines on"
197
+ : trigger.build === "every"
198
+ ? "; builds every commit: turn Settings > CI/CD > Auto-cancel redundant pipelines off"
199
+ : "";
200
+ return (`# ${jobName}: push to ${branches.join(", ")}, finding-mode ${mode}${tokenNote}` +
201
+ ` — a gated apply stays green, its pending gate in the log and in ${gateSummaryPath(jobName)}${build}`);
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Synthesize one `.gitlab-ci.yml` job per Op, all in a single file (a GitLab
207
+ * trigger is job-scoped — see the module doc). Wired into core's Op generate
208
+ * mode via the gitlab lexicon plugin's `generateOpPipeline` (../plugin.ts).
209
+ */
210
+ export function generateGitlabOpPipeline(ops, options = {}) {
211
+ const image = options.image ?? CI_IMAGE;
212
+ const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
213
+ const beforeScript = options.beforeScript ?? [];
214
+ const extraScript = options.extraScript ?? [];
215
+ const stage = options.opsStage ?? DEFAULT_STAGE;
216
+ assertOpsStage(stage);
217
+ const baseFileName = options.opsFileName ?? `${stage}.gitlab-ci.yml`;
218
+ assertOpsFileName(baseFileName);
219
+ // A workspace member's file, jobs and resource groups carry its name
220
+ // (#2542): the root .gitlab-ci.yml includes every member's file, and job
221
+ // names share one namespace across them.
222
+ const member = options.member;
223
+ const fileName = member ? `${member.name}-${baseFileName}` : baseFileName;
224
+ const jobPrefix = member ? `${member.name}-` : "";
225
+ const changes = member ? memberGitlabChanges(member, member.fileDir ? `${member.fileDir}/${fileName}` : undefined) : undefined;
226
+ const cdLine = member && member.dir !== "." && member.dir !== "" ? [`cd ${memberShellDir(member)}`] : [];
227
+ const jobs = [];
228
+ const doc = { stages: [stage], ...nonEmpty("variables", options.variables) };
229
+ const opLines = [];
230
+ let anyCron = false;
231
+ for (const spec of ops) {
232
+ const subject = `Scheduled Op "${spec.name}"`;
233
+ const setupScript = gitlabSetupScript(subject, spec.setup);
234
+ const idTokens = gitlabIdTokens(spec.name, spec.permissions);
235
+ const findingMode = spec.findingMode ?? "report";
236
+ const trigger = resolveOpTrigger(spec);
237
+ assertTriggerSupportsMode(spec.name, findingMode, trigger);
238
+ if (trigger.kind === "cron")
239
+ anyCron = true;
240
+ const jobName = jobPrefix + ciJobName(spec.name);
241
+ jobs.push({ jobName, op: spec.name, trigger, findingMode });
242
+ opLines.push(headerLineFor(spec, trigger, jobName, findingMode));
243
+ if (spec.environment) {
244
+ assertGitlabEnvironment(spec.name, spec.environment);
245
+ opLines.push(environmentLine(spec.environment));
246
+ }
247
+ // A push job is the one a gate would otherwise paint red on every merge
248
+ // (#2243). Every other trigger keeps the plain one-line invocation.
249
+ const rules = gitlabRules(subject, trigger, { variable: SELECTOR_VAR, value: spec.name });
250
+ const gated = trigger.kind === "push";
251
+ const runParts = fillCommand(runCommand, spec.name);
252
+ const invocation = gated ? [...runParts, ...GATED_EXIT_FLAG] : runParts;
253
+ const gateSummary = gateSummaryPath(jobName);
254
+ // Per-Op credentials (#2290): a spec's own `variables` merge into this
255
+ // job's `variables:` block, the same block the gated apply's
256
+ // `CHANT_GATE_SUMMARY` already occupies below — GATE_SUMMARY_VAR is
257
+ // internal and listed last, so it always wins a name collision rather
258
+ // than a caller being able to repoint the gate block's own artifact path.
259
+ const jobVariables = {
260
+ ...(spec.variables ?? {}),
261
+ ...(gated ? { [GATE_SUMMARY_VAR]: gateSummary } : {}),
262
+ };
263
+ doc[jobName] = {
264
+ stage,
265
+ image,
266
+ // GitLab's stand-in for github's per-Op concurrency group: queue the
267
+ // next run rather than cancel the current one.
268
+ resource_group: jobName,
269
+ ...(idTokens ? { id_tokens: idTokens } : {}),
270
+ ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}),
271
+ ...nonEmpty("variables", jobVariables),
272
+ rules: changes && trigger.kind !== "cron" ? rules.map((r) => ({ ...r, changes })) : rules,
273
+ script: [...cdLine, ...setupScript, ...beforeScript, invocation.join(" "), ...extraScript],
274
+ // `when: always` because the run this publishes for is the green one: a
275
+ // gated apply succeeds, and the block is the only thing that says a
276
+ // human still has to act. A run that walked through its gate writes no
277
+ // block, and GitLab reports the empty upload as a warning, not a failure.
278
+ ...(gated
279
+ ? { artifacts: { when: "always", paths: [member ? memberRepoPath(member, gateSummary) : gateSummary], expire_in: GATE_ARTIFACT_EXPIRY } }
280
+ : {}),
281
+ };
282
+ }
283
+ const headerLines = [
284
+ `# chant Ops — one job per Op under stage "${stage}", each`,
285
+ "# selected by its own rules:. A merge_request_event or push job needs no",
286
+ "# setup; its rule fires on the event itself.",
287
+ ];
288
+ if (anyCron) {
289
+ headerLines.push("#", "# GitLab has no in-file cron. Create one Pipeline Schedule per cron Op", "# below (Settings > CI/CD > Schedules): set its cron to the value noted", `# here and its ${SELECTOR_VAR} CI/CD variable to the Op's name, so only`, "# that job runs on that schedule.");
290
+ }
291
+ headerLines.push("#", ...opLines);
292
+ const yaml = emitGitlabFile([["stages", doc.stages], ...(doc.variables ? [["variables", doc.variables]] : []), ...jobs.map(({ jobName }) => [jobName, doc[jobName]])], headerLines);
293
+ return { files: [{ name: fileName, yaml }], jobs };
294
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Op waves → GitLab CI (#3679).
3
+ *
4
+ * One file, one stage per wave, on a push to the spec's branches. Each job is
5
+ * one of core's `opWaveJobs`, and `needs:` chains them so a wave that waits at
6
+ * its gate (exit 3) or fails stops every later wave; retrying the failed job
7
+ * after `chant approve` carries on. `GIT_DEPTH: "0"` gives the runner the base
8
+ * commit it reads the gate policy at. A wide wave's deciding job keeps its
9
+ * decision as an artifact, which GitLab hands each share job through `needs:`.
10
+ * A wave's `environment` is the job's own `environment:`; protect it in the
11
+ * project settings to put a reviewer in front of the job as well.
12
+ *
13
+ * With `resume` on the spec (#3683), a `<name>-resume` job runs `chant run
14
+ * resume --op <name>` in scheduled pipelines only: create a pipeline schedule
15
+ * with the spec's cron. It retries a waiting wave's job once its approval
16
+ * arrived, with `CHANT_FORGE_TOKEN` (a project access token with the `api`
17
+ * scope and the Developer role; the job token cannot retry jobs).
18
+ *
19
+ * A `pr-review` wave (#3684) adds `<name>-record-plans`, run in merge request
20
+ * pipelines only, which records the digests the merge request's head plans.
21
+ *
22
+ * `runner` (on the spec, a wave or a job group, #3914) is the job's `tags:`; a
23
+ * GitHub runner group is a spec error here. A wave's `jobs` renders its
24
+ * deciding job with the wave's own settings and a job per group (per share of
25
+ * a group) with the group's: its tags, environment, variables over the
26
+ * wave's, setup after the wave's, and `id_tokens:` from its `idTokens`.
27
+ */
28
+ import { assertGitlabBranch, assertGitlabEnvironment, assertRunnerLabels, CI_IMAGE, environmentDoc, GITLAB_FULL_HISTORY, GITLAB_MERGE_REQUEST_SOURCE, GITLAB_SCHEDULE_SOURCE, gitlabIdTokensDoc, gitlabPushRule, gitlabSetupScript, gitlabTags, nonEmpty, pushBranches, } from "@intentius/chant/ci-jobs";
29
+ import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
30
+ import { emitGitlabFile } from "./ci-jobs.js";
31
+ /** Render an Op waves spec as one GitLab CI file, `<name>.gitlab-ci.yml` unless `opsFileName` names another. */
32
+ export function generateGitlabOpWavesPipeline(spec, options) {
33
+ const jobs = opWaveJobs(spec, options.specFile);
34
+ const image = options.image ?? CI_IMAGE;
35
+ const subject = `Op waves "${spec.name}"`;
36
+ const rules = pushBranches(spec.branches).map((branch) => {
37
+ assertGitlabBranch(subject, branch);
38
+ return { if: gitlabPushRule(JSON.stringify(branch)) };
39
+ });
40
+ // The setup and environment each wave's jobs carry, checked as an Op's are.
41
+ const setupOf = spec.waves.map((wave) => gitlabSetupScript(`${subject} wave "${wave.name}"`, wave.setup));
42
+ for (const wave of spec.waves)
43
+ if (wave.environment)
44
+ assertGitlabEnvironment(`${spec.name}/${wave.name}`, wave.environment);
45
+ // #3914: GitLab picks a runner by tags, and each job group carries its own setup and environment.
46
+ assertRunnerLabels(subject, "runner", spec.runner, "GitLab");
47
+ const groupSetup = new Map();
48
+ for (const wave of spec.waves) {
49
+ assertRunnerLabels(subject, `wave "${wave.name}" runner`, wave.runner, "GitLab");
50
+ for (const group of wave.jobs ?? []) {
51
+ assertRunnerLabels(subject, `wave "${wave.name}" job group "${group.name}" runner`, group.runner, "GitLab");
52
+ if (group.environment)
53
+ assertGitlabEnvironment(`${spec.name}/${wave.name}/${group.name}`, group.environment);
54
+ groupSetup.set(`${wave.name}/${group.name}`, gitlabSetupScript(`${subject} wave "${wave.name}" job group "${group.name}"`, group.setup));
55
+ }
56
+ }
57
+ const specTags = gitlabTags(spec.runner);
58
+ const byReview = spec.waves.some((w) => w.approval === "pr-review");
59
+ const stages = [...(byReview ? ["plans"] : []), ...spec.waves.map((wave, i) => `wave-${i + 1}-${wave.name}`)];
60
+ if (spec.resume)
61
+ stages.push("resume");
62
+ const entries = [["stages", stages]];
63
+ const variables = nonEmpty("variables", options.variables).variables;
64
+ if (variables)
65
+ entries.push(["variables", variables]);
66
+ if (byReview) {
67
+ entries.push([
68
+ `${spec.name}-record-plans`,
69
+ {
70
+ stage: "plans",
71
+ image,
72
+ ...(specTags ? { tags: specTags } : {}),
73
+ variables: { ...GITLAB_FULL_HISTORY },
74
+ rules: [{ if: GITLAB_MERGE_REQUEST_SOURCE }],
75
+ script: [...(options.beforeScript ?? []), ["chant", "run", "wave", "--spec", options.specFile, "--record-plans"].join(" ")],
76
+ },
77
+ ]);
78
+ }
79
+ for (const job of jobs) {
80
+ const wave = spec.waves[job.wave - 1];
81
+ const group = opWaveGroupOf(spec, job);
82
+ const setup = [...setupOf[job.wave - 1], ...(group ? groupSetup.get(`${wave.name}/${group.name}`) : [])];
83
+ const tags = gitlabTags(group?.runner ?? wave.runner ?? spec.runner);
84
+ const environment = group?.environment ?? wave.environment;
85
+ const idTokens = gitlabIdTokensDoc(group?.idTokens ?? wave.idTokens);
86
+ entries.push([
87
+ job.jobName,
88
+ {
89
+ stage: stages[job.wave - 1 + (byReview ? 1 : 0)],
90
+ image,
91
+ ...(tags ? { tags } : {}),
92
+ resource_group: `${spec.name}-${wave.name}`,
93
+ ...(environment ? { environment: environmentDoc(environment) } : {}),
94
+ ...(idTokens ? { id_tokens: idTokens } : {}),
95
+ variables: { ...GITLAB_FULL_HISTORY, ...(wave.variables ?? {}), ...(group?.variables ?? {}) },
96
+ rules,
97
+ ...(job.needs.length > 0 ? { needs: job.needs } : {}),
98
+ script: [...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
99
+ ...(job.kind === "decide" ? { artifacts: { when: "always", paths: [job.record], expire_in: "30 days" } } : {}),
100
+ },
101
+ ]);
102
+ }
103
+ if (spec.resume) {
104
+ entries.push([
105
+ `${spec.name}-resume`,
106
+ {
107
+ stage: "resume",
108
+ image,
109
+ ...(specTags ? { tags: specTags } : {}),
110
+ rules: [{ if: GITLAB_SCHEDULE_SOURCE }],
111
+ script: [...(options.beforeScript ?? []), opWavesResumeCommand(spec).join(" ")],
112
+ },
113
+ ]);
114
+ }
115
+ const header = [
116
+ `# chant Op waves "${spec.name}": one stage per wave, each needing the one before.`,
117
+ "# A wave that waits at its gate exits 3 and stops the waves after it; approve the",
118
+ "# digest it prints, then retry the job.",
119
+ ...(spec.resume
120
+ ? [`# The ${spec.name}-resume job retries it for you in a pipeline schedule; give the schedule the cron "${spec.resume.schedule}".`]
121
+ : []),
122
+ ];
123
+ const name = options.opsFileName ?? `${spec.name}.gitlab-ci.yml`;
124
+ return { files: [{ name, yaml: emitGitlabFile(entries, header) }], jobs };
125
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAKjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAkJtB"}
1
+ {"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAgBjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAkJtB"}