@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,376 @@
1
+ /**
2
+ * Generate mode — component → GitLab CI YAML (#563, epic #551, Phase 3).
3
+ *
4
+ * The other half of "two modes, both anti-sprawl" (see
5
+ * docs/src/content/docs/components/orchestration.mdx#generate-mode and epic
6
+ * #551 §"5. Orchestrator → generate mode"): interpret mode (`../driver.ts`,
7
+ * #556) runs components directly; generate mode synthesizes a **thin**
8
+ * `.gitlab-ci.yml` from the same declarations for teams who want plain CI as
9
+ * the trigger/runner.
10
+ *
11
+ * The generated pipeline is a trigger, not the deploy logic:
12
+ * - Ordering + parallel-safe waves are resolved once, generically, by
13
+ * `resolveComponentGraph` (../driver.ts) — the exact function the local
14
+ * interpret driver uses. Generate mode does not re-derive or duplicate
15
+ * that graph logic.
16
+ * - Each wave becomes one GitLab CI `stage`; every component in a wave
17
+ * becomes one job in that stage, so independent components run in
18
+ * parallel and dependents wait for their dependencies via natural stage
19
+ * ordering (mirrored explicitly with `needs:` for direct edges, so GitLab
20
+ * can still parallelize across non-adjacent stages when safe).
21
+ * - Each job's `script` is exactly one invocation that hands off to the
22
+ * component's own composition (`chant run --components <name> ...` by
23
+ * default) — never inlined build/publish/apply steps. The deploy logic
24
+ * lives in the component's `deploy` phases and the capabilities they
25
+ * reference, not in this YAML.
26
+ *
27
+ * Cross-cutting changes (e.g. "sign every image before deploy") are made by
28
+ * editing `GenerateGitlabOptions.extraScript`/`beforeScript` (or the
29
+ * component's own composition) ONCE here — never per generated job. See
30
+ * `generate-gitlab.test.ts`'s "cross-cutting change" case for a
31
+ * demonstration: one generator-option edit reflects in every job without
32
+ * touching the component declarations.
33
+ */
34
+ import { assertGitlabBranch, CI_IMAGE, ciJobName, fillCommand, GITLAB_FULL_HISTORY, GITLAB_MERGE_REQUEST_SOURCE, gitlabPushRule, } from "@intentius/chant/ci-jobs";
35
+ import { resolveComponentGraph } from "@intentius/chant/components/driver";
36
+ import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
37
+ import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
38
+ import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
39
+ import { memberGitlabChanges, memberRepoPath, memberShellDir } from "@intentius/chant/lexicon";
40
+ import { emitGitlabFile } from "./ci-jobs.js";
41
+ /**
42
+ * Synthesize a `.gitlab-ci.yml` pipeline from a set of components: one stage
43
+ * per parallel-safe wave (`resolveComponentGraph`), one thin trigger job per
44
+ * component. Throws `DependencyCycleError`/`UnknownDependencyError` (from
45
+ * core's driver) exactly like the interpret driver does, since both consume
46
+ * the same graph resolution. Wired into core's generate mode via the gitlab
47
+ * lexicon plugin's `generateComponentPipeline` (../plugin.ts).
48
+ */
49
+ export function generateGitlabPipeline(components, options = {}) {
50
+ const env = options.env ?? "production";
51
+ const image = options.image ?? CI_IMAGE;
52
+ const runCommand = options.runCommand ?? ["chant", "run", "--components", "{name}", "--env", env];
53
+ const beforeScript = options.beforeScript ?? [];
54
+ const extraScript = options.extraScript ?? [];
55
+ const { waves } = resolveComponentGraph(components);
56
+ const byName = new Map(components.map((c) => [c.name, c]));
57
+ if (options.prLoop) {
58
+ if (options.promoteTo !== undefined)
59
+ throw new Error("a pull-request pipeline has no promote job; drop --promote-to or --pr-loop");
60
+ // #4003: --wave-gate beside --pr-loop gives the apply a gate per wave.
61
+ const prLoop = prLoopWithWaves(options.prLoop, options.gatedWaves);
62
+ return prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, { ...options, prLoop });
63
+ }
64
+ if (options.gatedWaves) {
65
+ if (options.promoteTo !== undefined)
66
+ throw new Error("a gated-wave pipeline has no promote job; drop --promote-to or --wave-gate");
67
+ return gatedWaveGitlabPipeline(components, env, image, beforeScript, extraScript, options);
68
+ }
69
+ // Components that something else depends on must hand their resolved outputs
70
+ // (stack outputs, published artifact refs) to their dependents, which run as
71
+ // separate jobs/processes. Each such producer dumps its outputs to a file and
72
+ // declares it a job artifact; each dependent seeds from that file (delivered
73
+ // across the `needs:` edge by GitLab's artifact passing) so a `stackOutput()`
74
+ // / `@<dep>.publish.*` reference resolves even though the producer ran in a
75
+ // different job. Without this, a single-component job has no in-memory outputs
76
+ // for its dependencies — see epic #551 / the adopt-alb-services example.
77
+ const dependedUpon = new Set();
78
+ for (const c of components)
79
+ for (const dep of c.dependsOn ?? [])
80
+ dependedUpon.add(dep);
81
+ const outputsFile = (name) => `${name}.outputs.json`;
82
+ // A promote job (#2575) runs apart from the deploy jobs, and a promote
83
+ // publishes from the build archive on disk, so each component job keeps the
84
+ // files its build steps wrote as artifacts. GitLab hands them to the promote
85
+ // job across its `needs:` edges. Each component job with a publish step
86
+ // also keeps the digest its run recorded (`--digest-file`), and the promote
87
+ // job passes it back as `--digest <component>=<digest>` (#2602), so it
88
+ // promotes the release this run built rather than whatever is latest in the
89
+ // source environment.
90
+ const promoteTo = options.promoteTo;
91
+ const archives = new Map();
92
+ const pinned = new Set();
93
+ const digestFile = (name) => `${name}.digest`;
94
+ if (promoteTo !== undefined) {
95
+ for (const c of components) {
96
+ const paths = promoteArchivePaths(c);
97
+ if (paths.length > 0)
98
+ archives.set(c.name, paths);
99
+ if (hasPublishStep(c))
100
+ pinned.add(c.name);
101
+ }
102
+ if (pinned.size === 0) {
103
+ throw new Error("no component has a publish step, so no deploy records a release for the promote job to promote");
104
+ }
105
+ }
106
+ const stages = waves.map((_, i) => `wave-${i + 1}`);
107
+ const jobs = [];
108
+ const jobNameByComponent = new Map();
109
+ for (const wave of waves) {
110
+ for (const name of wave)
111
+ jobNameByComponent.set(name, ciJobName(name));
112
+ }
113
+ const doc = {};
114
+ // `workflow:name` + a CHANT_ENV variable give the document a recoverable
115
+ // environment identity (#2046): two pipelines generated for two
116
+ // environments were previously identical except for one shell argument in
117
+ // each job's `script:`. CHANT_ENV always reflects the environment baked
118
+ // into the run lines, so a caller-supplied variable of the same name cannot
119
+ // make the document lie about what its jobs deploy.
120
+ doc.workflow = { name: `chant-components-${env}` };
121
+ doc.stages = stages;
122
+ doc.variables = { ...options.variables, CHANT_ENV: env };
123
+ waves.forEach((wave, waveIndex) => {
124
+ const stage = stages[waveIndex];
125
+ for (const name of wave) {
126
+ const component = byName.get(name);
127
+ const jobName = jobNameByComponent.get(name);
128
+ const needs = (component.dependsOn ?? []).map((dep) => jobNameByComponent.get(dep)).sort();
129
+ jobs.push({ jobName, component: name, stage, needs });
130
+ // Build the run invocation, then append output-threading flags: seed from
131
+ // each dependency's dumped outputs, and dump this component's own outputs
132
+ // if a dependent will need them.
133
+ const runParts = fillCommand(runCommand, name);
134
+ for (const dep of component.dependsOn ?? [])
135
+ runParts.push("--seed-outputs", outputsFile(dep));
136
+ if (dependedUpon.has(name))
137
+ runParts.push("--dump-outputs", outputsFile(name));
138
+ if (pinned.has(name))
139
+ runParts.push("--digest-file", digestFile(name));
140
+ const script = [...beforeScript, runParts.join(" "), ...extraScript];
141
+ const jobProps = {
142
+ stage,
143
+ image,
144
+ script,
145
+ };
146
+ if (needs.length > 0)
147
+ jobProps.needs = needs;
148
+ // Publish this component's dumped outputs so dependent jobs receive it.
149
+ const artifactPaths = [
150
+ ...(dependedUpon.has(name) ? [outputsFile(name)] : []),
151
+ ...(archives.get(name) ?? []),
152
+ ...(pinned.has(name) ? [digestFile(name)] : []),
153
+ ];
154
+ if (artifactPaths.length > 0)
155
+ jobProps.artifacts = { paths: artifactPaths };
156
+ doc[jobName] = jobProps;
157
+ }
158
+ });
159
+ let promoteJob;
160
+ if (promoteTo !== undefined) {
161
+ promoteJob = `promote-${ciJobName(promoteTo)}`;
162
+ if (promoteJob in doc) {
163
+ throw new Error(`the promote job "${promoteJob}" has the same name as a component job; rename the component`);
164
+ }
165
+ const command = [...(options.promoteCommand ?? ["chant", "components", "promote", "--from", env, "--to", promoteTo])];
166
+ // The file holds `<component>=<digest>`; a missing or empty one leaves
167
+ // `<component>=`, which the promote refuses.
168
+ for (const name of [...pinned].sort()) {
169
+ command.push("--digest", `"${name}=$(cut -d= -f2- ${digestFile(name)})"`);
170
+ }
171
+ doc[promoteJob] = {
172
+ stage: "promote",
173
+ image,
174
+ script: [...beforeScript, command.join(" "), ...extraScript],
175
+ needs: [...jobNameByComponent.values()].sort(),
176
+ };
177
+ }
178
+ if (options.member) {
179
+ promoteJob = scopeToMember(doc, jobs, promoteJob, options.member, env);
180
+ }
181
+ // The promote stage is YAML only: `stages` in the result stays one entry
182
+ // per graph wave.
183
+ const yamlStages = promoteJob ? [...stages, "promote"] : stages;
184
+ const yaml = emitGitlabFile([
185
+ ["workflow", doc.workflow],
186
+ ["stages", yamlStages],
187
+ ["variables", doc.variables],
188
+ ...jobs.map((job) => [job.jobName, doc[job.jobName]]),
189
+ ...(promoteJob ? [[promoteJob, doc[promoteJob]]] : []),
190
+ ]);
191
+ return { yaml, stages, jobs, env };
192
+ }
193
+ /**
194
+ * The gated-wave pipeline (#3049): one job per wave, each running one wave of
195
+ * `chant components fan-out --wave-gate`, in order through `needs:`. The
196
+ * attempt record is the artifact GitLab hands from job to job, kept even when
197
+ * a job stops at a gate (exit 3) so the re-run after `chant approve` reads it.
198
+ * `GIT_DEPTH: "0"` gives the job the history `--base` diffs against.
199
+ */
200
+ function gatedWaveGitlabPipeline(components, env, image, beforeScript, extraScript, options) {
201
+ const waveJobs = gatedWaveJobs(components, env, options.gatedWaves);
202
+ const stages = waveJobs.map((j) => j.jobName);
203
+ const doc = {
204
+ workflow: { name: `chant-components-${env}` },
205
+ stages,
206
+ variables: { ...options.variables, CHANT_ENV: env },
207
+ };
208
+ const jobs = [];
209
+ for (const job of waveJobs) {
210
+ jobs.push({ jobName: job.jobName, component: `wave ${job.wave}`, stage: job.jobName, needs: job.needs });
211
+ doc[job.jobName] = {
212
+ stage: job.jobName,
213
+ image,
214
+ variables: { ...GITLAB_FULL_HISTORY },
215
+ script: [...prLoopSetup(image), ...beforeScript, job.command.join(" "), ...extraScript],
216
+ ...(job.needs.length > 0 ? { needs: job.needs } : {}),
217
+ artifacts: { when: "always", paths: [GATED_WAVE_RECORD] },
218
+ };
219
+ }
220
+ if (options.member)
221
+ scopeToMember(doc, jobs, undefined, options.member, env);
222
+ const yaml = emitGitlabFile([
223
+ ["workflow", doc.workflow],
224
+ ["stages", stages],
225
+ ["variables", doc.variables],
226
+ ...jobs.map((job) => [job.jobName, doc[job.jobName]]),
227
+ ]);
228
+ return { yaml, stages, jobs, env };
229
+ }
230
+ /**
231
+ * The merge-request pipeline (#3183): `plan` in each merge request pipeline,
232
+ * and on each push to the target branch `record` (#4168), or `apply` with
233
+ * `applyOnMerge`, one at a time per environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
234
+ * they measure the change with. The jobs talk to GitLab with
235
+ * `CHANT_FORGE_TOKEN`, a project or group access token with the `api` scope
236
+ * set as a masked CI/CD variable, since a job token cannot write notes.
237
+ *
238
+ * For a workspace member (#3465) the file is one the root `.gitlab-ci.yml`
239
+ * includes, so the jobs are `<member>-plan` and `<member>-apply`, each
240
+ * script starts with a `cd` into the member's directory and the report
241
+ * artifact is kept from there. The jobs take no `changes:` rule, since a
242
+ * change outside the member can reach it; the plan selects from the whole
243
+ * change and passes `--member`, which keeps the member's gate, note and
244
+ * statuses apart from the other members'. The apply's resource group is the
245
+ * member's own.
246
+ *
247
+ * The apply job keeps its attempt record in the job cache under the pushed
248
+ * commit, uploaded whether the job passed or failed, so a retry of a failed
249
+ * apply passes it to `pr-apply --resume` (#3543). GitLab keeps protected
250
+ * branches' caches apart from other branches' by default, so a merge request
251
+ * pipeline cannot write the record the target branch's apply reads.
252
+ *
253
+ * With a gate per wave (`waveGate`, #4003) the apply job becomes
254
+ * `apply-wave-1` to `apply-wave-<n>`, one stage each, each needing the one
255
+ * before. They share the commit's cache key, so wave k pulls the record wave
256
+ * k-1 pushed, and a retry of a wave that stopped at its gate pulls the one it
257
+ * pushed itself.
258
+ */
259
+ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, options) {
260
+ const loop = options.prLoop;
261
+ const member = options.member;
262
+ const rooted = !member || member.dir === "." || member.dir === "";
263
+ const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
264
+ const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
265
+ const planJob = member ? `${member.name}-plan` : "plan";
266
+ const named = (job) => (member ? `${member.name}-${job}` : job);
267
+ const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
268
+ const applies = loop.waveGate
269
+ ? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave)), stage: prApplyWaveJobName(job.wave) }))
270
+ : loop.applyOnMerge
271
+ ? [{ job: apply, name: named("apply"), stage: "apply" }]
272
+ : [{ job: record, name: named("record"), stage: "record" }];
273
+ const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
274
+ if (loop.branch !== undefined)
275
+ assertGitlabBranch("The merge-request pipeline", loop.branch);
276
+ const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
277
+ // A member's jobs start in its directory, after the image's install lines.
278
+ const enter = rooted ? [] : [`cd ${memberShellDir(member)}`];
279
+ // The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY variable is set.
280
+ const script = (job) => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.setup ?? []), ...(job.build ?? []), job.run.command, ...extraScript];
281
+ const stages = ["plan", ...applies.map((a) => a.stage)];
282
+ const doc = {
283
+ // Included files merge their top-level keys, so a member's file carries
284
+ // the rules every member shares and no name: two names would collide.
285
+ workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }] },
286
+ stages,
287
+ variables: { ...options.variables, CHANT_ENV: env, ...GITLAB_FULL_HISTORY },
288
+ [planJob]: {
289
+ stage: "plan",
290
+ image,
291
+ rules: [{ if: onMergeRequest }],
292
+ variables: plan.run.env,
293
+ script: script(plan),
294
+ artifacts: { when: "always", paths: [reportPath] },
295
+ },
296
+ ...Object.fromEntries(applies.map(({ job, name, stage }, i) => [
297
+ name,
298
+ {
299
+ stage,
300
+ image,
301
+ ...(i > 0 ? { needs: [applies[i - 1].name] } : {}),
302
+ resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
303
+ rules: [{ if: onMerge }],
304
+ variables: job.run.env,
305
+ script: script(job),
306
+ // The attempt record (#3543), kept under the pushed commit even when
307
+ // the job fails, so a retry finishes the apply under the same approval.
308
+ // The record job (#4168) applies nothing and keeps none.
309
+ ...(job.jobName === "record"
310
+ ? {}
311
+ : {
312
+ cache: {
313
+ key: prApplyRecordKey(env, "$CI_COMMIT_SHA", member?.name),
314
+ paths: [member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD],
315
+ when: "always",
316
+ },
317
+ }),
318
+ artifacts: { when: "always", paths: [reportPath] },
319
+ },
320
+ ])),
321
+ };
322
+ const jobs = [
323
+ { jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
324
+ ...applies.map(({ job, name, stage }, i) => ({
325
+ jobName: name,
326
+ component: job.wave ? `merge request apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "merge request apply",
327
+ stage,
328
+ needs: i > 0 ? [applies[i - 1].name] : [],
329
+ })),
330
+ ];
331
+ const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, ...applies.map((a) => a.name)].map((key) => [key, doc[key]]));
332
+ return { yaml, stages, jobs, env };
333
+ }
334
+ /**
335
+ * Scope a pipeline to one workspace member (#2542, #2524 D19), in place on
336
+ * `doc` and `jobs`. Returns the promote job's new name.
337
+ *
338
+ * GitLab reads one `.gitlab-ci.yml`, so a member's pipeline is a file that
339
+ * the root file includes, and job names share one namespace across every
340
+ * included file. Each job therefore takes the member's name as a prefix. A
341
+ * `rules: changes:` entry limits each job to changes under the member's
342
+ * directory or to the pipeline file; for a member at `"."` there is no such
343
+ * rule, since `changes` has no exclusions. Each job's script starts with a
344
+ * `cd` into the member's directory, and artifact paths, which GitLab resolves
345
+ * against the repository root, move under it.
346
+ */
347
+ function scopeToMember(doc, jobs, promoteJob, member, env) {
348
+ const rooted = member.dir === "." || member.dir === "";
349
+ const changes = memberGitlabChanges(member);
350
+ const renamed = new Map();
351
+ for (const job of jobs)
352
+ renamed.set(job.jobName, `${member.name}-${job.jobName}`);
353
+ if (promoteJob)
354
+ renamed.set(promoteJob, `${member.name}-${promoteJob}`);
355
+ for (const [from, to] of renamed) {
356
+ const props = doc[from];
357
+ delete doc[from];
358
+ const next = { ...props };
359
+ if (!rooted)
360
+ next.script = [`cd ${memberShellDir(member)}`, ...props.script];
361
+ if (Array.isArray(props.needs))
362
+ next.needs = props.needs.map((n) => renamed.get(n) ?? n);
363
+ const artifacts = props.artifacts;
364
+ if (artifacts?.paths)
365
+ next.artifacts = { ...artifacts, paths: artifacts.paths.map((p) => memberRepoPath(member, p)) };
366
+ if (changes)
367
+ next.rules = [{ changes }];
368
+ doc[to] = next;
369
+ }
370
+ for (const job of jobs) {
371
+ job.jobName = renamed.get(job.jobName);
372
+ job.needs = job.needs.map((n) => renamed.get(n) ?? n);
373
+ }
374
+ doc.workflow = { name: `chant-components.${member.name}.${env}` };
375
+ return promoteJob ? renamed.get(promoteJob) : undefined;
376
+ }