@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.
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +157 -0
- package/dist/codegen/fetch.js +93 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +105 -0
- package/dist/codegen/generate.js +125 -0
- package/dist/codegen/naming.js +48 -0
- package/dist/codegen/package.js +41 -0
- package/dist/codegen/parse.js +539 -0
- package/dist/codegen/patches.js +84 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/components/ci-jobs.js +16 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +294 -0
- package/dist/components/generate-op-waves-pipeline.js +125 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +376 -0
- package/dist/composites/catalog.js +418 -0
- package/dist/composites/docker-build.js +38 -0
- package/dist/composites/index.js +5 -0
- package/dist/composites/mr-plan-report.js +42 -0
- package/dist/composites/node-pipeline.js +67 -0
- package/dist/composites/python-pipeline.js +45 -0
- package/dist/composites/review-app.js +32 -0
- package/dist/coverage.js +35 -0
- package/dist/generated/index.js +25 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +202 -0
- package/dist/import/generator.js +196 -0
- package/dist/import/parser.js +111 -0
- package/dist/index.js +26 -0
- package/dist/integrity.json +3 -3
- package/dist/intrinsics.js +33 -0
- package/dist/lint/audit-catalog.js +53 -0
- package/dist/lint/audit-lineage.js +64 -0
- package/dist/lint/post-synth/index.js +84 -0
- package/dist/lint/post-synth/wgl010.js +34 -0
- package/dist/lint/post-synth/wgl011.js +48 -0
- package/dist/lint/post-synth/wgl012.d.ts.map +1 -1
- package/dist/lint/post-synth/wgl012.js +68 -0
- package/dist/lint/post-synth/wgl013.js +57 -0
- package/dist/lint/post-synth/wgl014.js +43 -0
- package/dist/lint/post-synth/wgl015.js +70 -0
- package/dist/lint/post-synth/wgl016.js +70 -0
- package/dist/lint/post-synth/wgl017.js +38 -0
- package/dist/lint/post-synth/wgl018.js +34 -0
- package/dist/lint/post-synth/wgl019.js +43 -0
- package/dist/lint/post-synth/wgl020.js +47 -0
- package/dist/lint/post-synth/wgl021.js +52 -0
- package/dist/lint/post-synth/wgl022.js +38 -0
- package/dist/lint/post-synth/wgl023.js +44 -0
- package/dist/lint/post-synth/wgl024.js +41 -0
- package/dist/lint/post-synth/wgl025.js +42 -0
- package/dist/lint/post-synth/wgl026.js +58 -0
- package/dist/lint/post-synth/wgl027.js +49 -0
- package/dist/lint/post-synth/wgl028.js +58 -0
- package/dist/lint/post-synth/wgl029.js +55 -0
- package/dist/lint/post-synth/wgl030.js +43 -0
- package/dist/lint/post-synth/wgl031.js +33 -0
- package/dist/lint/post-synth/wgl032.js +64 -0
- package/dist/lint/post-synth/wgl033.js +32 -0
- package/dist/lint/post-synth/wgl034.js +37 -0
- package/dist/lint/post-synth/wgl035.js +40 -0
- package/dist/lint/post-synth/wgl036.js +38 -0
- package/dist/lint/post-synth/wgl037.js +41 -0
- package/dist/lint/post-synth/wgl038.js +43 -0
- package/dist/lint/post-synth/wgl039.js +34 -0
- package/dist/lint/post-synth/wgl040.js +35 -0
- package/dist/lint/post-synth/wgl041.js +53 -0
- package/dist/lint/post-synth/wgl042.js +41 -0
- package/dist/lint/post-synth/wgl043.js +39 -0
- package/dist/lint/post-synth/wgl044.js +35 -0
- package/dist/lint/post-synth/wgl045.js +54 -0
- package/dist/lint/post-synth/wgl046.js +40 -0
- package/dist/lint/post-synth/wgl047.js +34 -0
- package/dist/lint/post-synth/wgl048.js +32 -0
- package/dist/lint/post-synth/wgl049.js +53 -0
- package/dist/lint/post-synth/wgl050.js +42 -0
- package/dist/lint/post-synth/yaml-helpers.js +432 -0
- package/dist/lint/rules/artifact-no-expiry.js +53 -0
- package/dist/lint/rules/data/known-include-sources.js +44 -0
- package/dist/lint/rules/data/untrusted-variables.js +30 -0
- package/dist/lint/rules/deprecated-only-except.js +46 -0
- package/dist/lint/rules/import-source.js +61 -0
- package/dist/lint/rules/index.js +7 -0
- package/dist/lint/rules/missing-script.js +67 -0
- package/dist/lint/rules/missing-stage.js +60 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +262 -0
- package/dist/migrate/from-github/actions/index.js +17 -0
- package/dist/migrate/from-github/actions/registry.js +61 -0
- package/dist/migrate/from-github/actions/tier-1.js +298 -0
- package/dist/migrate/from-github/actions/tier-2.js +241 -0
- package/dist/migrate/from-github/actions/tier-3.js +96 -0
- package/dist/migrate/from-github/composites/patterns.js +151 -0
- package/dist/migrate/from-github/composites/rewriter.js +22 -0
- package/dist/migrate/from-github/diagnostics.js +53 -0
- package/dist/migrate/from-github/emit-yaml.js +121 -0
- package/dist/migrate/from-github/expressions.js +275 -0
- package/dist/migrate/from-github/index.js +96 -0
- package/dist/migrate/from-github/provenance.js +35 -0
- package/dist/migrate/from-github/rules.js +74 -0
- package/dist/migrate/from-github/security.js +189 -0
- package/dist/migrate/from-github/stages.js +156 -0
- package/dist/migrate/from-github/transformer.js +633 -0
- package/dist/op/activities/gitlab.js +38 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +26 -0
- package/dist/package-cli.js +21 -0
- package/dist/plugin.js +536 -0
- package/dist/rules/wgl012.ts +2 -2
- package/dist/serializer.js +197 -0
- package/dist/testdata/create-fixture.js +40 -0
- package/dist/testdata/load-fixtures.js +20 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +29 -0
- package/dist/variables.js +30 -0
- package/package.json +8 -8
- package/src/codegen/fetch.ts +2 -2
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -3
- package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +46 -0
- package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +101 -0
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
- package/src/components/generate-op-pipeline.test.ts +10 -0
- package/src/components/generate-op-pipeline.ts +8 -1
- package/src/components/generate-pipeline.ts +71 -30
- package/src/components/generate-pr-pipeline.test.ts +61 -18
- package/src/components/generate-pr-waves-pipeline.test.ts +88 -0
- package/src/lint/post-synth/wgl012.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- 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
|
+
}
|