@intentius/chant-lexicon-gitlab 0.61.0 → 0.62.0
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/components/generate-op-pipeline.d.ts +13 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/generate-op-pipeline.test.ts +130 -13
- package/src/components/generate-op-pipeline.ts +70 -8
|
@@ -90,6 +90,19 @@
|
|
|
90
90
|
* job log, since the invocation carries no `--json`. There is no follow-up
|
|
91
91
|
* notice job: it would need a forge API call and a token this generator does
|
|
92
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.
|
|
93
106
|
*/
|
|
94
107
|
import type { ComponentPipelineOptions as GenerateGitlabOpOptions, OpPipelineResult as GenerateGitlabOpResult, ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
95
108
|
export type { GenerateGitlabOpOptions, GenerateGitlabOpResult };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AAIH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAInD,gBAAgB,IAAI,sBAAsB,EAE1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAqShE;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAwGxB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "019c4c5936e680b98f9f5e5916884cd68af6e7866d6be9522dc5d69068761197",
|
|
5
5
|
"meta.json": "931fc3246a55645b1493080bbeb160e5d205e42349e8bdca96ca243adb5f0da3",
|
|
6
6
|
"types/index.d.ts": "5cd2e99f135a929b72bdd822d00d780d39b1cd407ba0cfb3d511c7ac9d667b58",
|
|
7
7
|
"rules/artifact-no-expiry.ts": "3f3cabf9792cbf8207e53a25f506715466b19ec25e9c3b4d0d77fed6b2eb4542",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"skills/chant-gitlab-migrate.md": "4853d04980560b379e5b0f7267dcb0a1e5d5879cb9e9931681ce76b2a5fe69d8",
|
|
56
56
|
"skills/chant-gitlab-patterns.md": "6d9a44e9e8de4c3820be9d65381b0d2ede4cb3626aaaf0011e21838a9fbf556e"
|
|
57
57
|
},
|
|
58
|
-
"composite": "
|
|
58
|
+
"composite": "5961fea1f006be2aab006533a1d585287c35827438c487f5be8590685bc72a56"
|
|
59
59
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gitlab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "GitLab CI lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "^5.9.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@intentius/chant": "^0.
|
|
71
|
-
"@intentius/chant-lexicon-github": "^0.
|
|
70
|
+
"@intentius/chant": "^0.62.0",
|
|
71
|
+
"@intentius/chant-lexicon-github": "^0.62.0",
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|
|
@@ -26,10 +26,10 @@ describe("generateGitlabOpPipeline: one file, one job per scheduled Op", () => {
|
|
|
26
26
|
const result = generateGitlabOpPipeline(specs);
|
|
27
27
|
|
|
28
28
|
expect(result.files).toHaveLength(1);
|
|
29
|
-
expect(result.files[0].name).toBe("
|
|
29
|
+
expect(result.files[0].name).toBe("ops.gitlab-ci.yml");
|
|
30
30
|
|
|
31
31
|
const parsed = parseYAML(result.files[0].yaml);
|
|
32
|
-
expect(parsed.stages).toEqual(["
|
|
32
|
+
expect(parsed.stages).toEqual(["ops"]);
|
|
33
33
|
expect(parsed["actions-audit"]).toBeDefined();
|
|
34
34
|
expect(parsed["prod-reconcile"]).toBeDefined();
|
|
35
35
|
});
|
|
@@ -39,7 +39,7 @@ describe("generateGitlabOpPipeline: one file, one job per scheduled Op", () => {
|
|
|
39
39
|
const parsed = parseYAML(result.files[0].yaml);
|
|
40
40
|
const job = parsed["actions-audit"] as Record<string, unknown>;
|
|
41
41
|
|
|
42
|
-
expect(job.stage).toBe("
|
|
42
|
+
expect(job.stage).toBe("ops");
|
|
43
43
|
expect(job.rules).toEqual([
|
|
44
44
|
{ if: '$CI_PIPELINE_SOURCE == "schedule" && $CHANT_SCHEDULED_OP == "actions-audit"' },
|
|
45
45
|
]);
|
|
@@ -66,7 +66,72 @@ describe("generateGitlabOpPipeline: one file, one job per scheduled Op", () => {
|
|
|
66
66
|
expect(result.files).toHaveLength(1);
|
|
67
67
|
expect(result.jobs).toEqual([]);
|
|
68
68
|
const parsed = parseYAML(result.files[0].yaml);
|
|
69
|
-
expect(parsed.stages).toEqual(["
|
|
69
|
+
expect(parsed.stages).toEqual(["ops"]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* #2293 — the stage and file name follow the pipeline's actual contents
|
|
75
|
+
* instead of the cron-only-era constant "scheduled-ops", and both are
|
|
76
|
+
* overridable via `ComponentPipelineOptions` the way every other generator
|
|
77
|
+
* knob is (`image`, `variables`, ...).
|
|
78
|
+
*/
|
|
79
|
+
describe("generateGitlabOpPipeline: the stage and file name (#2293)", () => {
|
|
80
|
+
test("a mixed-trigger project's stage and file no longer say 'scheduled'", () => {
|
|
81
|
+
// cron + merge_request + push in one project, exactly the shape that
|
|
82
|
+
// made "scheduled-ops" wrong: a GitLab UI would show live-check-style
|
|
83
|
+
// pull-request/push jobs grouped under a stage claiming they run on a
|
|
84
|
+
// schedule.
|
|
85
|
+
const result = generateGitlabOpPipeline([
|
|
86
|
+
{ name: "nightly", schedule: "0 6 * * *" },
|
|
87
|
+
{ name: "live-plan", trigger: { kind: "pull_request", branches: ["main"] } },
|
|
88
|
+
{ name: "live-apply", trigger: { kind: "push", branches: ["main"] } },
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
expect(result.files[0].name).not.toMatch(/scheduled/);
|
|
92
|
+
expect(result.files[0].name).toBe("ops.gitlab-ci.yml");
|
|
93
|
+
|
|
94
|
+
const parsed = parseYAML(result.files[0].yaml);
|
|
95
|
+
expect(parsed.stages).toEqual(["ops"]);
|
|
96
|
+
expect((parsed["live-plan"] as Record<string, unknown>).stage).toBe("ops");
|
|
97
|
+
expect((parsed["live-apply"] as Record<string, unknown>).stage).toBe("ops");
|
|
98
|
+
expect((parsed.nightly as Record<string, unknown>).stage).toBe("ops");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("the header names the stage every job runs in", () => {
|
|
102
|
+
const yaml = generateGitlabOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }]).files[0]
|
|
103
|
+
.yaml;
|
|
104
|
+
expect(yaml).toContain('one job per Op under stage "ops"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("opsStage overrides the stage, and the file name follows it by default", () => {
|
|
108
|
+
const result = generateGitlabOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }], {
|
|
109
|
+
opsStage: "deploy",
|
|
110
|
+
});
|
|
111
|
+
expect(result.files[0].name).toBe("deploy.gitlab-ci.yml");
|
|
112
|
+
const parsed = parseYAML(result.files[0].yaml);
|
|
113
|
+
expect(parsed.stages).toEqual(["deploy"]);
|
|
114
|
+
expect((parsed["actions-audit"] as Record<string, unknown>).stage).toBe("deploy");
|
|
115
|
+
expect(result.files[0].yaml).toContain('one job per Op under stage "deploy"');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("opsFileName overrides the file name independently of opsStage", () => {
|
|
119
|
+
const result = generateGitlabOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }], {
|
|
120
|
+
opsStage: "deploy",
|
|
121
|
+
opsFileName: "ci/deploy-ops.yml",
|
|
122
|
+
});
|
|
123
|
+
expect(result.files[0].name).toBe("ci/deploy-ops.yml");
|
|
124
|
+
const parsed = parseYAML(result.files[0].yaml);
|
|
125
|
+
expect(parsed.stages).toEqual(["deploy"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a blank opsStage or opsFileName is refused by name", () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
generateGitlabOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }], { opsStage: " " }),
|
|
131
|
+
).toThrow(/opsStage.*empty/s);
|
|
132
|
+
expect(() =>
|
|
133
|
+
generateGitlabOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }], { opsFileName: "" }),
|
|
134
|
+
).toThrow(/opsFileName.*empty/s);
|
|
70
135
|
});
|
|
71
136
|
});
|
|
72
137
|
|
|
@@ -375,19 +440,23 @@ describe("generateGitlabOpPipeline: a deployment environment (#2257)", () => {
|
|
|
375
440
|
/**
|
|
376
441
|
* The document a cron spec with no `environment` emits. #2257 pinned this to
|
|
377
442
|
* prove its option is additive; #2256 moved two lines of it and it is
|
|
378
|
-
* re-pinned rather than loosened, so it still proves the same thing.
|
|
443
|
+
* re-pinned rather than loosened, so it still proves the same thing. #2293
|
|
444
|
+
* re-pinned it again: the stage (and the header line naming it) moved from
|
|
445
|
+
* the constant "scheduled-ops" to the default "ops".
|
|
379
446
|
*
|
|
380
447
|
* What moved: the header's opening paragraph, because a merge_request_event
|
|
381
448
|
* or push job needs no Pipeline Schedule and the cron instructions are now
|
|
382
|
-
* printed only for the Ops that do
|
|
383
|
-
*
|
|
384
|
-
*
|
|
449
|
+
* printed only for the Ops that do, and because the header now names the
|
|
450
|
+
* stage every job runs in (#2293); `resource_group`, which every job gains
|
|
451
|
+
* as GitLab's stand-in for github's per-Op concurrency group; and the
|
|
452
|
+
* `stage:`/`stages:` values themselves (#2293). The cron job's own rule and
|
|
453
|
+
* script are byte-for-byte what they were.
|
|
385
454
|
*/
|
|
386
455
|
const YAML_BEFORE_2257 =
|
|
387
456
|
[
|
|
388
|
-
|
|
389
|
-
"# rules:. A merge_request_event or push job needs no
|
|
390
|
-
"# fires on the event itself.",
|
|
457
|
+
'# chant Ops (#927, #2084, #2293) — one job per Op under stage "ops", each',
|
|
458
|
+
"# selected by its own rules:. A merge_request_event or push job needs no",
|
|
459
|
+
"# setup; its rule fires on the event itself.",
|
|
391
460
|
"#",
|
|
392
461
|
"# GitLab has no in-file cron. Create one Pipeline Schedule per cron Op",
|
|
393
462
|
"# below (Settings > CI/CD > Schedules): set its cron to the value noted",
|
|
@@ -398,10 +467,10 @@ describe("generateGitlabOpPipeline: a deployment environment (#2257)", () => {
|
|
|
398
467
|
" — needs a GITLAB_TOKEN CI/CD variable (masked, scope: api)",
|
|
399
468
|
"",
|
|
400
469
|
"stages:",
|
|
401
|
-
" -
|
|
470
|
+
" - ops",
|
|
402
471
|
"",
|
|
403
472
|
"actions-audit:",
|
|
404
|
-
" stage:
|
|
473
|
+
" stage: ops",
|
|
405
474
|
" image: node:22-slim",
|
|
406
475
|
" resource_group: actions-audit",
|
|
407
476
|
" rules:",
|
|
@@ -456,3 +525,51 @@ describe("generateGitlabOpPipeline: a deployment environment (#2257)", () => {
|
|
|
456
525
|
).toThrow(/neither an absolute http\(s\) URL nor a variable expression/);
|
|
457
526
|
});
|
|
458
527
|
});
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* chant #2290 — per-Op credentials. Cheap here because a gated job already
|
|
531
|
+
* carries a job-level `variables:` block (`CHANT_GATE_SUMMARY`); a spec's own
|
|
532
|
+
* `variables` merge into the same block rather than asking for a shape
|
|
533
|
+
* GitLab has no concept of.
|
|
534
|
+
*/
|
|
535
|
+
describe("generateGitlabOpPipeline: per-Op variables merge into the job's own variables: (#2290)", () => {
|
|
536
|
+
const CREDENTIAL = { AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" };
|
|
537
|
+
|
|
538
|
+
test("a spec with no variables emits no job-level variables:, even when top-level variables: is set", () => {
|
|
539
|
+
const result = generateGitlabOpPipeline(
|
|
540
|
+
[{ name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } }],
|
|
541
|
+
{ variables: { CHANT_FORGE: "gitlab" } },
|
|
542
|
+
);
|
|
543
|
+
const job = parseYAML(result.files[0].yaml)["live-check"] as Record<string, unknown>;
|
|
544
|
+
expect(job.variables).toBeUndefined();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("a spec's own variables land on the job's variables:, beside the top-level variables:", () => {
|
|
548
|
+
const result = generateGitlabOpPipeline(
|
|
549
|
+
[{ name: "live-apply", trigger: { kind: "pull_request" }, variables: CREDENTIAL }],
|
|
550
|
+
{ variables: { CHANT_FORGE: "gitlab" } },
|
|
551
|
+
);
|
|
552
|
+
const job = parseYAML(result.files[0].yaml)["live-apply"] as { variables?: Record<string, string> };
|
|
553
|
+
expect(job.variables).toEqual(CREDENTIAL);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("on a gated push job, a spec's own variables merge alongside CHANT_GATE_SUMMARY rather than replacing it", () => {
|
|
557
|
+
const result = generateGitlabOpPipeline([
|
|
558
|
+
{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL },
|
|
559
|
+
]);
|
|
560
|
+
const job = parseYAML(result.files[0].yaml)["live-apply"] as { variables?: Record<string, string> };
|
|
561
|
+
expect(job.variables).toEqual({ ...CREDENTIAL, CHANT_GATE_SUMMARY: "chant-gate-live-apply.md" });
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("CHANT_GATE_SUMMARY always wins a name collision — a caller cannot repoint the gate artifact", () => {
|
|
565
|
+
const result = generateGitlabOpPipeline([
|
|
566
|
+
{
|
|
567
|
+
name: "live-apply",
|
|
568
|
+
trigger: { kind: "push", branches: ["main"] },
|
|
569
|
+
variables: { CHANT_GATE_SUMMARY: "somewhere-else.md" },
|
|
570
|
+
},
|
|
571
|
+
]);
|
|
572
|
+
const job = parseYAML(result.files[0].yaml)["live-apply"] as { variables?: Record<string, string> };
|
|
573
|
+
expect(job.variables?.CHANT_GATE_SUMMARY).toBe("chant-gate-live-apply.md");
|
|
574
|
+
});
|
|
575
|
+
});
|
|
@@ -90,6 +90,19 @@
|
|
|
90
90
|
* job log, since the invocation carries no `--json`. There is no follow-up
|
|
91
91
|
* notice job: it would need a forge API call and a token this generator does
|
|
92
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.
|
|
93
106
|
*/
|
|
94
107
|
|
|
95
108
|
import { emitYAML } from "@intentius/chant/yaml";
|
|
@@ -112,7 +125,22 @@ function toJobName(opName: string): string {
|
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
const DEFAULT_IMAGE = "node:22-slim";
|
|
115
|
-
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Default stage every generated job runs in, and the default base of the
|
|
131
|
+
* generated file's name (#2293). Overridable via `options.opsStage` (and the
|
|
132
|
+
* file name separately via `options.opsFileName`) — see
|
|
133
|
+
* {@link GenerateGitlabOpOptions} (`ComponentPipelineOptions` in
|
|
134
|
+
* `../../../../packages/core/src/lexicon.ts`).
|
|
135
|
+
*
|
|
136
|
+
* `"scheduled-ops"` before #2293: a constant from when this generator was
|
|
137
|
+
* cron-only, left in place through #2084/#2256 even after every
|
|
138
|
+
* merge-request and push job started sharing it, so a GitLab UI showed
|
|
139
|
+
* `live-check`/`live-plan`/`live-apply`/`live-adopt` grouped under a stage
|
|
140
|
+
* called "scheduled" and a consuming project's `include:` named a file that
|
|
141
|
+
* said the opposite of what it held.
|
|
142
|
+
*/
|
|
143
|
+
const DEFAULT_STAGE = "ops";
|
|
116
144
|
|
|
117
145
|
/** The CI/CD variable a Pipeline Schedule sets to select which job it runs. */
|
|
118
146
|
const SELECTOR_VAR = "CHANT_SCHEDULED_OP";
|
|
@@ -179,6 +207,26 @@ function assertBranchName(specName: string, branch: string): void {
|
|
|
179
207
|
}
|
|
180
208
|
}
|
|
181
209
|
|
|
210
|
+
/** Refuse a blank `opsStage` (#2293): every job's `stage:` and the top-level `stages:` entry would otherwise be an empty string. */
|
|
211
|
+
function assertOpsStage(stage: string): void {
|
|
212
|
+
if (stage.trim() === "") {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`The gitlab Op generator's \`opsStage\` option is empty. Name the stage every generated job shares, ` +
|
|
215
|
+
`or drop the option to keep the default "${DEFAULT_STAGE}".`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Refuse a blank `opsFileName` (#2293): `main.ts` in a consuming project writes `result.files[].name` straight to disk. */
|
|
221
|
+
function assertOpsFileName(fileName: string): void {
|
|
222
|
+
if (fileName.trim() === "") {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`The gitlab Op generator's \`opsFileName\` option is empty. Name the generated file, or drop the ` +
|
|
225
|
+
`option to derive it from \`opsStage\`.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
182
230
|
/**
|
|
183
231
|
* This trigger's `rules:` entries. Two or more branches are two or more
|
|
184
232
|
* entries: `rules:` is an OR over its list, which is how GitLab spells the
|
|
@@ -375,9 +423,13 @@ export function generateGitlabOpPipeline(
|
|
|
375
423
|
const runCommand = options.runCommand ?? ["chant", "run", "{name}"];
|
|
376
424
|
const beforeScript = options.beforeScript ?? [];
|
|
377
425
|
const extraScript = options.extraScript ?? [];
|
|
426
|
+
const stage = options.opsStage ?? DEFAULT_STAGE;
|
|
427
|
+
assertOpsStage(stage);
|
|
428
|
+
const fileName = options.opsFileName ?? `${stage}.gitlab-ci.yml`;
|
|
429
|
+
assertOpsFileName(fileName);
|
|
378
430
|
|
|
379
431
|
const jobs: OpPipelineJob[] = [];
|
|
380
|
-
const doc: Record<string, unknown> = { stages: [
|
|
432
|
+
const doc: Record<string, unknown> = { stages: [stage] };
|
|
381
433
|
if (options.variables && Object.keys(options.variables).length > 0) doc.variables = options.variables;
|
|
382
434
|
|
|
383
435
|
const opLines: string[] = [];
|
|
@@ -406,8 +458,18 @@ export function generateGitlabOpPipeline(
|
|
|
406
458
|
const invocation = gated ? [...runParts, ...GATED_EXIT_FLAG] : runParts;
|
|
407
459
|
const gateSummary = gateSummaryPath(jobName);
|
|
408
460
|
|
|
461
|
+
// Per-Op credentials (#2290): a spec's own `variables` merge into this
|
|
462
|
+
// job's `variables:` block, the same block the gated apply's
|
|
463
|
+
// `CHANT_GATE_SUMMARY` already occupies below — GATE_SUMMARY_VAR is
|
|
464
|
+
// internal and listed last, so it always wins a name collision rather
|
|
465
|
+
// than a caller being able to repoint the gate block's own artifact path.
|
|
466
|
+
const jobVariables: Record<string, string> = {
|
|
467
|
+
...(spec.variables ?? {}),
|
|
468
|
+
...(gated ? { [GATE_SUMMARY_VAR]: gateSummary } : {}),
|
|
469
|
+
};
|
|
470
|
+
|
|
409
471
|
doc[jobName] = {
|
|
410
|
-
stage
|
|
472
|
+
stage,
|
|
411
473
|
image,
|
|
412
474
|
// GitLab's stand-in for github's per-Op concurrency group: queue the
|
|
413
475
|
// next run rather than cancel the current one.
|
|
@@ -421,7 +483,7 @@ export function generateGitlabOpPipeline(
|
|
|
421
483
|
},
|
|
422
484
|
}
|
|
423
485
|
: {}),
|
|
424
|
-
...(
|
|
486
|
+
...(Object.keys(jobVariables).length > 0 ? { variables: jobVariables } : {}),
|
|
425
487
|
rules: rulesFor(spec, trigger),
|
|
426
488
|
script: [...setupScript, ...beforeScript, invocation.join(" "), ...extraScript],
|
|
427
489
|
// `when: always` because the run this publishes for is the green one: a
|
|
@@ -435,9 +497,9 @@ export function generateGitlabOpPipeline(
|
|
|
435
497
|
}
|
|
436
498
|
|
|
437
499
|
const headerLines = [
|
|
438
|
-
|
|
439
|
-
"# rules:. A merge_request_event or push job needs no
|
|
440
|
-
"# fires on the event itself.",
|
|
500
|
+
`# chant Ops (#927, #2084, #2293) — one job per Op under stage "${stage}", each`,
|
|
501
|
+
"# selected by its own rules:. A merge_request_event or push job needs no",
|
|
502
|
+
"# setup; its rule fires on the event itself.",
|
|
441
503
|
];
|
|
442
504
|
if (anyCron) {
|
|
443
505
|
headerLines.push(
|
|
@@ -459,5 +521,5 @@ export function generateGitlabOpPipeline(
|
|
|
459
521
|
|
|
460
522
|
const yaml = headerLines.join("\n") + "\n\n" + sections.join("\n\n") + "\n";
|
|
461
523
|
|
|
462
|
-
return { files: [{ name:
|
|
524
|
+
return { files: [{ name: fileName, yaml }], jobs };
|
|
463
525
|
}
|