@intentius/chant-lexicon-gitlab 0.124.30 → 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.
- 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.js +288 -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 +369 -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-waves.gitlab.golden.yml +101 -0
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -3
- package/src/components/generate-pipeline.ts +60 -27
- package/src/components/generate-pr-pipeline.test.ts +14 -9
- 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,288 @@
|
|
|
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
|
+
return (`# ${jobName}: push to ${branches.join(", ")}, finding-mode ${mode}${tokenNote}` +
|
|
195
|
+
` — a gated apply stays green, its pending gate in the log and in ${gateSummaryPath(jobName)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Synthesize one `.gitlab-ci.yml` job per Op, all in a single file (a GitLab
|
|
201
|
+
* trigger is job-scoped — see the module doc). Wired into core's Op generate
|
|
202
|
+
* mode via the gitlab lexicon plugin's `generateOpPipeline` (../plugin.ts).
|
|
203
|
+
*/
|
|
204
|
+
export function generateGitlabOpPipeline(ops, options = {}) {
|
|
205
|
+
const image = options.image ?? CI_IMAGE;
|
|
206
|
+
const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
|
|
207
|
+
const beforeScript = options.beforeScript ?? [];
|
|
208
|
+
const extraScript = options.extraScript ?? [];
|
|
209
|
+
const stage = options.opsStage ?? DEFAULT_STAGE;
|
|
210
|
+
assertOpsStage(stage);
|
|
211
|
+
const baseFileName = options.opsFileName ?? `${stage}.gitlab-ci.yml`;
|
|
212
|
+
assertOpsFileName(baseFileName);
|
|
213
|
+
// A workspace member's file, jobs and resource groups carry its name
|
|
214
|
+
// (#2542): the root .gitlab-ci.yml includes every member's file, and job
|
|
215
|
+
// names share one namespace across them.
|
|
216
|
+
const member = options.member;
|
|
217
|
+
const fileName = member ? `${member.name}-${baseFileName}` : baseFileName;
|
|
218
|
+
const jobPrefix = member ? `${member.name}-` : "";
|
|
219
|
+
const changes = member ? memberGitlabChanges(member, member.fileDir ? `${member.fileDir}/${fileName}` : undefined) : undefined;
|
|
220
|
+
const cdLine = member && member.dir !== "." && member.dir !== "" ? [`cd ${memberShellDir(member)}`] : [];
|
|
221
|
+
const jobs = [];
|
|
222
|
+
const doc = { stages: [stage], ...nonEmpty("variables", options.variables) };
|
|
223
|
+
const opLines = [];
|
|
224
|
+
let anyCron = false;
|
|
225
|
+
for (const spec of ops) {
|
|
226
|
+
const subject = `Scheduled Op "${spec.name}"`;
|
|
227
|
+
const setupScript = gitlabSetupScript(subject, spec.setup);
|
|
228
|
+
const idTokens = gitlabIdTokens(spec.name, spec.permissions);
|
|
229
|
+
const findingMode = spec.findingMode ?? "report";
|
|
230
|
+
const trigger = resolveOpTrigger(spec);
|
|
231
|
+
assertTriggerSupportsMode(spec.name, findingMode, trigger);
|
|
232
|
+
if (trigger.kind === "cron")
|
|
233
|
+
anyCron = true;
|
|
234
|
+
const jobName = jobPrefix + ciJobName(spec.name);
|
|
235
|
+
jobs.push({ jobName, op: spec.name, trigger, findingMode });
|
|
236
|
+
opLines.push(headerLineFor(spec, trigger, jobName, findingMode));
|
|
237
|
+
if (spec.environment) {
|
|
238
|
+
assertGitlabEnvironment(spec.name, spec.environment);
|
|
239
|
+
opLines.push(environmentLine(spec.environment));
|
|
240
|
+
}
|
|
241
|
+
// A push job is the one a gate would otherwise paint red on every merge
|
|
242
|
+
// (#2243). Every other trigger keeps the plain one-line invocation.
|
|
243
|
+
const rules = gitlabRules(subject, trigger, { variable: SELECTOR_VAR, value: spec.name });
|
|
244
|
+
const gated = trigger.kind === "push";
|
|
245
|
+
const runParts = fillCommand(runCommand, spec.name);
|
|
246
|
+
const invocation = gated ? [...runParts, ...GATED_EXIT_FLAG] : runParts;
|
|
247
|
+
const gateSummary = gateSummaryPath(jobName);
|
|
248
|
+
// Per-Op credentials (#2290): a spec's own `variables` merge into this
|
|
249
|
+
// job's `variables:` block, the same block the gated apply's
|
|
250
|
+
// `CHANT_GATE_SUMMARY` already occupies below — GATE_SUMMARY_VAR is
|
|
251
|
+
// internal and listed last, so it always wins a name collision rather
|
|
252
|
+
// than a caller being able to repoint the gate block's own artifact path.
|
|
253
|
+
const jobVariables = {
|
|
254
|
+
...(spec.variables ?? {}),
|
|
255
|
+
...(gated ? { [GATE_SUMMARY_VAR]: gateSummary } : {}),
|
|
256
|
+
};
|
|
257
|
+
doc[jobName] = {
|
|
258
|
+
stage,
|
|
259
|
+
image,
|
|
260
|
+
// GitLab's stand-in for github's per-Op concurrency group: queue the
|
|
261
|
+
// next run rather than cancel the current one.
|
|
262
|
+
resource_group: jobName,
|
|
263
|
+
...(idTokens ? { id_tokens: idTokens } : {}),
|
|
264
|
+
...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}),
|
|
265
|
+
...nonEmpty("variables", jobVariables),
|
|
266
|
+
rules: changes && trigger.kind !== "cron" ? rules.map((r) => ({ ...r, changes })) : rules,
|
|
267
|
+
script: [...cdLine, ...setupScript, ...beforeScript, invocation.join(" "), ...extraScript],
|
|
268
|
+
// `when: always` because the run this publishes for is the green one: a
|
|
269
|
+
// gated apply succeeds, and the block is the only thing that says a
|
|
270
|
+
// human still has to act. A run that walked through its gate writes no
|
|
271
|
+
// block, and GitLab reports the empty upload as a warning, not a failure.
|
|
272
|
+
...(gated
|
|
273
|
+
? { artifacts: { when: "always", paths: [member ? memberRepoPath(member, gateSummary) : gateSummary], expire_in: GATE_ARTIFACT_EXPIRY } }
|
|
274
|
+
: {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const headerLines = [
|
|
278
|
+
`# chant Ops — one job per Op under stage "${stage}", each`,
|
|
279
|
+
"# selected by its own rules:. A merge_request_event or push job needs no",
|
|
280
|
+
"# setup; its rule fires on the event itself.",
|
|
281
|
+
];
|
|
282
|
+
if (anyCron) {
|
|
283
|
+
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.");
|
|
284
|
+
}
|
|
285
|
+
headerLines.push("#", ...opLines);
|
|
286
|
+
const yaml = emitGitlabFile([["stages", doc.stages], ...(doc.variables ? [["variables", doc.variables]] : []), ...jobs.map(({ jobName }) => [jobName, doc[jobName]])], headerLines);
|
|
287
|
+
return { files: [{ name: fileName, yaml }], jobs };
|
|
288
|
+
}
|
|
@@ -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;
|
|
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;AAejG,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"}
|