@intentius/chant-lexicon-gitlab 0.124.32 → 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/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +7 -1
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +19 -12
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +46 -0
- 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 +19 -11
- package/src/components/generate-pr-pipeline.test.ts +50 -12
- package/src/components/generate-pr-waves-pipeline.test.ts +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AAeH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAInD,gBAAgB,IAAI,sBAAsB,EAE1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AAeH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAInD,gBAAgB,IAAI,sBAAsB,EAE1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AA4HhE;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAsGxB"}
|
|
@@ -191,8 +191,14 @@ function headerLineFor(spec, trigger, jobName, mode) {
|
|
|
191
191
|
}
|
|
192
192
|
case "push": {
|
|
193
193
|
const branches = pushBranches(trigger.branches);
|
|
194
|
+
// #4169: GitLab cancels a waiting pipeline when a newer commit's starts, under the project's auto-cancel setting.
|
|
195
|
+
const build = trigger.build === "newest"
|
|
196
|
+
? "; builds the newest commit and each ci/build/<sha> tag: keep Settings > CI/CD > Auto-cancel redundant pipelines on"
|
|
197
|
+
: trigger.build === "every"
|
|
198
|
+
? "; builds every commit: turn Settings > CI/CD > Auto-cancel redundant pipelines off"
|
|
199
|
+
: "";
|
|
194
200
|
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)}`);
|
|
201
|
+
` — a gated apply stays green, its pending gate in the log and in ${gateSummaryPath(jobName)}${build}`);
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
204
|
}
|
|
@@ -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;AAgBjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAkJtB"}
|
|
@@ -35,7 +35,7 @@ import { assertGitlabBranch, CI_IMAGE, ciJobName, fillCommand, GITLAB_FULL_HISTO
|
|
|
35
35
|
import { resolveComponentGraph } from "@intentius/chant/components/driver";
|
|
36
36
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
37
37
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
38
|
-
import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-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
39
|
import { memberGitlabChanges, memberRepoPath, memberShellDir } from "@intentius/chant/lexicon";
|
|
40
40
|
import { emitGitlabFile } from "./ci-jobs.js";
|
|
41
41
|
/**
|
|
@@ -229,8 +229,8 @@ function gatedWaveGitlabPipeline(components, env, image, beforeScript, extraScri
|
|
|
229
229
|
}
|
|
230
230
|
/**
|
|
231
231
|
* The merge-request pipeline (#3183): `plan` in each merge request pipeline,
|
|
232
|
-
*
|
|
233
|
-
* environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
|
|
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
234
|
* they measure the change with. The jobs talk to GitLab with
|
|
235
235
|
* `CHANT_FORGE_TOKEN`, a project or group access token with the `api` scope
|
|
236
236
|
* set as a masked CI/CD variable, since a job token cannot write notes.
|
|
@@ -260,14 +260,16 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
260
260
|
const loop = options.prLoop;
|
|
261
261
|
const member = options.member;
|
|
262
262
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
263
|
-
const [plan, apply] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
263
|
+
const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
264
264
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
265
265
|
const planJob = member ? `${member.name}-plan` : "plan";
|
|
266
266
|
const named = (job) => (member ? `${member.name}-${job}` : job);
|
|
267
267
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
268
268
|
const applies = loop.waveGate
|
|
269
269
|
? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave)), stage: prApplyWaveJobName(job.wave) }))
|
|
270
|
-
:
|
|
270
|
+
: loop.applyOnMerge
|
|
271
|
+
? [{ job: apply, name: named("apply"), stage: "apply" }]
|
|
272
|
+
: [{ job: record, name: named("record"), stage: "record" }];
|
|
271
273
|
const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
|
|
272
274
|
if (loop.branch !== undefined)
|
|
273
275
|
assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
@@ -297,17 +299,22 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
297
299
|
stage,
|
|
298
300
|
image,
|
|
299
301
|
...(i > 0 ? { needs: [applies[i - 1].name] } : {}),
|
|
300
|
-
resource_group: prApplyGroup(env, member?.name),
|
|
302
|
+
resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
|
|
301
303
|
rules: [{ if: onMerge }],
|
|
302
304
|
variables: job.run.env,
|
|
303
305
|
script: script(job),
|
|
304
306
|
// The attempt record (#3543), kept under the pushed commit even when
|
|
305
307
|
// the job fails, so a retry finishes the apply under the same approval.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}),
|
|
311
318
|
artifacts: { when: "always", paths: [reportPath] },
|
|
312
319
|
},
|
|
313
320
|
])),
|
|
@@ -316,7 +323,7 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
316
323
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
317
324
|
...applies.map(({ job, name, stage }, i) => ({
|
|
318
325
|
jobName: name,
|
|
319
|
-
component: job.wave ? `merge request apply, wave ${job.wave}` : "merge request apply",
|
|
326
|
+
component: job.wave ? `merge request apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "merge request apply",
|
|
320
327
|
stage,
|
|
321
328
|
needs: i > 0 ? [applies[i - 1].name] : [],
|
|
322
329
|
})),
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ebb9c8f2b23dabf3657d08be03ccb5ac5aa083073552df047f3d84f56ee8b288",
|
|
5
5
|
"meta.json": "db082fbf90c5941c498b2bee0f106103449c1a4c257716ca2220f0dd490450b2",
|
|
6
6
|
"types/index.d.ts": "a048a66ebe20da0cb5fc889054cac69044d9bf882e2c56b5c5bf113d1f3ba57c",
|
|
7
7
|
"rules/artifact-no-expiry.ts": "0ca5bb972d4a542126e8a0229d43b55658394cb7dd9b9923b2c590bec1c2ad7d",
|
|
@@ -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": "8da150c5882bad8c14689fa870f26cff56ae3edb1d262d88ae5f11749ad2a3f7"
|
|
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.124.
|
|
3
|
+
"version": "0.124.33",
|
|
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.124.
|
|
71
|
-
"@intentius/chant-lexicon-github": "^0.124.
|
|
70
|
+
"@intentius/chant": "^0.124.33",
|
|
71
|
+
"@intentius/chant-lexicon-github": "^0.124.33"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@intentius/chant-lexicon-github": {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
workflow:
|
|
2
|
+
name: chant-pr-prod
|
|
3
|
+
rules:
|
|
4
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
5
|
+
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
|
+
|
|
7
|
+
stages:
|
|
8
|
+
- plan
|
|
9
|
+
- record
|
|
10
|
+
|
|
11
|
+
variables:
|
|
12
|
+
CHANT_ENV: prod
|
|
13
|
+
GIT_DEPTH: '0'
|
|
14
|
+
|
|
15
|
+
plan:
|
|
16
|
+
stage: plan
|
|
17
|
+
image: node:22
|
|
18
|
+
rules:
|
|
19
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
20
|
+
variables:
|
|
21
|
+
BASE_SHA: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
|
|
22
|
+
PR_NUMBER: '$CI_MERGE_REQUEST_IID'
|
|
23
|
+
script:
|
|
24
|
+
- command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
25
|
+
- chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge gitlab
|
|
26
|
+
artifacts:
|
|
27
|
+
when: always
|
|
28
|
+
paths:
|
|
29
|
+
- .chant/pr
|
|
30
|
+
|
|
31
|
+
record:
|
|
32
|
+
stage: record
|
|
33
|
+
image: node:22
|
|
34
|
+
resource_group: chant-record-prod
|
|
35
|
+
rules:
|
|
36
|
+
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
37
|
+
variables:
|
|
38
|
+
BASE_SHA: '$CI_COMMIT_BEFORE_SHA'
|
|
39
|
+
script:
|
|
40
|
+
- command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
41
|
+
- if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then BASE_SHA="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-}"; if [ -z "$BASE_SHA" ]; then git fetch origin $CI_DEFAULT_BRANCH && BASE_SHA="$(git merge-base FETCH_HEAD "$CI_COMMIT_SHA")"; if [ "$BASE_SHA" = "$CI_COMMIT_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$CI_COMMIT_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; fi; export BASE_SHA
|
|
42
|
+
- chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab
|
|
43
|
+
artifacts:
|
|
44
|
+
when: always
|
|
45
|
+
paths:
|
|
46
|
+
- .chant/pr
|
|
@@ -185,6 +185,16 @@ describe("generateGitlabOpPipeline: the merge_request and push triggers (#2084,
|
|
|
185
185
|
]);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
test("a newest push build also runs at a build request's tag, and the header says how auto-cancel must be set (#4169)", () => {
|
|
189
|
+
const newest = generateGitlabOpPipeline([{ name: "suites", trigger: { kind: "push", build: "newest" } }]);
|
|
190
|
+
const job = parseYAML(newest.files[0].yaml)["suites"] as { rules: Array<{ if: string }> };
|
|
191
|
+
expect(job.rules.map((r) => r.if)).toEqual(['$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"', "$CI_COMMIT_TAG =~ /^ci\\/build\\//"]);
|
|
192
|
+
expect(newest.files[0].yaml).toContain("Auto-cancel redundant pipelines on");
|
|
193
|
+
const every = generateGitlabOpPipeline([{ name: "suites", trigger: { kind: "push", build: "every" } }]);
|
|
194
|
+
expect((parseYAML(every.files[0].yaml)["suites"] as { rules: unknown[] }).rules).toHaveLength(1);
|
|
195
|
+
expect(every.files[0].yaml).toContain("Auto-cancel redundant pipelines off");
|
|
196
|
+
});
|
|
197
|
+
|
|
188
198
|
test("an unfiltered push trigger defaults to main, the same branch github's generator assumes", () => {
|
|
189
199
|
const result = generateGitlabOpPipeline([{ name: "tf-apply", trigger: { kind: "push" } }]);
|
|
190
200
|
const job = parseYAML(result.files[0].yaml)["tf-apply"] as { rules: Array<{ if: string }> };
|
|
@@ -239,9 +239,16 @@ function headerLineFor(
|
|
|
239
239
|
}
|
|
240
240
|
case "push": {
|
|
241
241
|
const branches = pushBranches(trigger.branches);
|
|
242
|
+
// #4169: GitLab cancels a waiting pipeline when a newer commit's starts, under the project's auto-cancel setting.
|
|
243
|
+
const build =
|
|
244
|
+
trigger.build === "newest"
|
|
245
|
+
? "; builds the newest commit and each ci/build/<sha> tag: keep Settings > CI/CD > Auto-cancel redundant pipelines on"
|
|
246
|
+
: trigger.build === "every"
|
|
247
|
+
? "; builds every commit: turn Settings > CI/CD > Auto-cancel redundant pipelines off"
|
|
248
|
+
: "";
|
|
242
249
|
return (
|
|
243
250
|
`# ${jobName}: push to ${branches.join(", ")}, finding-mode ${mode}${tokenNote}` +
|
|
244
|
-
` — a gated apply stays green, its pending gate in the log and in ${gateSummaryPath(jobName)}`
|
|
251
|
+
` — a gated apply stays green, its pending gate in the log and in ${gateSummaryPath(jobName)}${build}`
|
|
245
252
|
);
|
|
246
253
|
}
|
|
247
254
|
}
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
PR_APPLY_RECORD,
|
|
49
49
|
PR_LOOP_REPORT_DIR,
|
|
50
50
|
prApplyGroup,
|
|
51
|
+
prRecordGroup,
|
|
51
52
|
prApplyRecordKey,
|
|
52
53
|
prApplyWaveJobName,
|
|
53
54
|
prLoopJobs,
|
|
@@ -272,8 +273,8 @@ function gatedWaveGitlabPipeline(
|
|
|
272
273
|
|
|
273
274
|
/**
|
|
274
275
|
* The merge-request pipeline (#3183): `plan` in each merge request pipeline,
|
|
275
|
-
*
|
|
276
|
-
* environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
|
|
276
|
+
* and on each push to the target branch `record` (#4168), or `apply` with
|
|
277
|
+
* `applyOnMerge`, one at a time per environment (`resource_group`). `GIT_DEPTH: "0"` gives both the history
|
|
277
278
|
* they measure the change with. The jobs talk to GitLab with
|
|
278
279
|
* `CHANT_FORGE_TOKEN`, a project or group access token with the `api` scope
|
|
279
280
|
* set as a masked CI/CD variable, since a job token cannot write notes.
|
|
@@ -310,14 +311,16 @@ function prLoopGitlabPipeline(
|
|
|
310
311
|
const loop = options.prLoop!;
|
|
311
312
|
const member = options.member;
|
|
312
313
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
313
|
-
const [plan, apply] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
314
|
+
const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
314
315
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
315
316
|
const planJob = member ? `${member.name}-plan` : "plan";
|
|
316
317
|
const named = (job: string) => (member ? `${member.name}-${job}` : job);
|
|
317
318
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
318
319
|
const applies = loop.waveGate
|
|
319
320
|
? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave!)), stage: prApplyWaveJobName(job.wave!) }))
|
|
320
|
-
:
|
|
321
|
+
: loop.applyOnMerge
|
|
322
|
+
? [{ job: apply, name: named("apply"), stage: "apply" }]
|
|
323
|
+
: [{ job: record, name: named("record"), stage: "record" }];
|
|
321
324
|
const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
|
|
322
325
|
if (loop.branch !== undefined) assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
323
326
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
@@ -347,17 +350,22 @@ function prLoopGitlabPipeline(
|
|
|
347
350
|
stage,
|
|
348
351
|
image,
|
|
349
352
|
...(i > 0 ? { needs: [applies[i - 1]!.name] } : {}),
|
|
350
|
-
resource_group: prApplyGroup(env, member?.name),
|
|
353
|
+
resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
|
|
351
354
|
rules: [{ if: onMerge }],
|
|
352
355
|
variables: job.run.env,
|
|
353
356
|
script: script(job),
|
|
354
357
|
// The attempt record (#3543), kept under the pushed commit even when
|
|
355
358
|
// the job fails, so a retry finishes the apply under the same approval.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
// The record job (#4168) applies nothing and keeps none.
|
|
360
|
+
...(job.jobName === "record"
|
|
361
|
+
? {}
|
|
362
|
+
: {
|
|
363
|
+
cache: {
|
|
364
|
+
key: prApplyRecordKey(env, "$CI_COMMIT_SHA", member?.name),
|
|
365
|
+
paths: [member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD],
|
|
366
|
+
when: "always",
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
361
369
|
artifacts: { when: "always", paths: [reportPath] },
|
|
362
370
|
},
|
|
363
371
|
]),
|
|
@@ -367,7 +375,7 @@ function prLoopGitlabPipeline(
|
|
|
367
375
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
368
376
|
...applies.map(({ job, name, stage }, i) => ({
|
|
369
377
|
jobName: name,
|
|
370
|
-
component: job.wave ? `merge request apply, wave ${job.wave}` : "merge request apply",
|
|
378
|
+
component: job.wave ? `merge request apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "merge request apply",
|
|
371
379
|
stage,
|
|
372
380
|
needs: i > 0 ? [applies[i - 1]!.name] : [],
|
|
373
381
|
})),
|
|
@@ -33,7 +33,7 @@ interface Job {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe("the GitLab merge-request pipeline", () => {
|
|
36
|
-
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
36
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
37
37
|
const doc = parseYAML(result.yaml) as { workflow: { rules: unknown[] }; variables: Record<string, string>; plan: Job; apply: Job };
|
|
38
38
|
|
|
39
39
|
test("matches its golden", () => {
|
|
@@ -57,7 +57,7 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
57
57
|
...PR_LOOP_SETUP,'chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge gitlab']);
|
|
58
58
|
expect(doc.apply.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
|
|
59
59
|
expect(doc.apply.variables).toEqual({ BASE_SHA: "$CI_COMMIT_BEFORE_SHA" });
|
|
60
|
-
// #4156: ci run builds pr-apply with --require-review, and GitLab hands CHANT_RUNNER_KEY to the job as a CI/CD variable when it has one.
|
|
60
|
+
// #4156: ci run builds pr-apply (with no --require-review since #4168), and GitLab hands CHANT_RUNNER_KEY to the job as a CI/CD variable when it has one.
|
|
61
61
|
expect(doc.apply.script.at(-1)).toBe('chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab');
|
|
62
62
|
expect(result.yaml).not.toContain("CHANT_RUNNER_KEY");
|
|
63
63
|
expect(doc.apply.resource_group).toBe("chant-apply-prod");
|
|
@@ -66,7 +66,7 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
test("events and reportUrl stream the run's events into the report artifact and copy the reports out (#4007)", () => {
|
|
69
|
-
const loud = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { events: true, reportUrl: "$CHANT_REPORT_URL" } }).yaml) as { plan: Job; apply: Job };
|
|
69
|
+
const loud = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, events: true, reportUrl: "$CHANT_REPORT_URL" } }).yaml) as { plan: Job; apply: Job };
|
|
70
70
|
for (const job of [loud.plan, loud.apply]) {
|
|
71
71
|
expect(job.script.at(-1)).toContain('--events-json --report-url "$CHANT_REPORT_URL"');
|
|
72
72
|
expect(job.variables.CHANT_RUN_EVENTS_DIR).toBe(".chant/pr/run-events");
|
|
@@ -78,7 +78,7 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
78
78
|
test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
|
|
79
79
|
expect(doc.plan.image).toBe("node:22");
|
|
80
80
|
expect(doc.apply.script[0]).toBe(PR_LOOP_SETUP[0]);
|
|
81
|
-
const own = generateGitlabPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
|
|
81
|
+
const own = generateGitlabPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true } }).yaml;
|
|
82
82
|
expect(own).toContain("image: example/ci:1");
|
|
83
83
|
expect(own).not.toContain("install-opentofu");
|
|
84
84
|
});
|
|
@@ -92,7 +92,7 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
test("a named branch replaces the default branch in the apply rule", () => {
|
|
95
|
-
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { branch: "trunk" } }).yaml;
|
|
95
|
+
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml;
|
|
96
96
|
expect(yaml).toContain('$CI_COMMIT_BRANCH == "trunk"');
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -106,13 +106,13 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test("refuses a promote job beside it, and a canary beside --wave-gate (#4003)", () => {
|
|
109
|
-
expect(() => generateGitlabPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
|
|
110
|
-
expect(() => generateGitlabPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
|
|
109
|
+
expect(() => generateGitlabPipeline(ESTATE, { prLoop: { applyOnMerge: true }, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
|
|
110
|
+
expect(() => generateGitlabPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
test("inside a workspace member: member-named jobs that cd into the member, with no changes rule (#3465)", () => {
|
|
114
114
|
const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
|
|
115
|
-
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, member });
|
|
115
|
+
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member });
|
|
116
116
|
const mdoc = parseYAML(scoped.yaml) as Record<string, unknown> & { workflow: { name?: string; rules: unknown[] } };
|
|
117
117
|
expect(scoped.jobs.map((j) => j.jobName)).toEqual(["network-plan", "network-apply"]);
|
|
118
118
|
expect(mdoc.plan).toBeUndefined();
|
|
@@ -137,12 +137,12 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
137
137
|
|
|
138
138
|
describe("the merge-request pipeline's branch (#3765)", () => {
|
|
139
139
|
test("a branch that would break the rules: expression is refused, as an Op's is", () => {
|
|
140
|
-
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { branch: "re$lease" } })).toThrow(/filters on branch "re\$lease"/);
|
|
140
|
+
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "re$lease" } })).toThrow(/filters on branch "re\$lease"/);
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
describe("the gitlab pull-request pipeline with build (#4088)", () => {
|
|
145
|
-
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { build: true } });
|
|
145
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, build: true } });
|
|
146
146
|
const doc = parseYAML(result.yaml) as Record<string, any>;
|
|
147
147
|
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-build.gitlab.golden.yml");
|
|
148
148
|
|
|
@@ -161,14 +161,52 @@ describe("the gitlab pull-request pipeline with build (#4088)", () => {
|
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
test("buildCommand replaces the build line and turns the option on", () => {
|
|
164
|
-
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { buildCommand: "npm run build" } }).yaml;
|
|
164
|
+
const yaml = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, buildCommand: "npm run build" } }).yaml;
|
|
165
165
|
expect(yaml).toContain("npm run build");
|
|
166
166
|
expect(yaml).not.toContain("chant build --env");
|
|
167
167
|
expect(yaml).toContain("chant lint");
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
test("the default output carries neither line", () => {
|
|
171
|
-
const plain = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml;
|
|
171
|
+
const plain = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } }).yaml;
|
|
172
172
|
expect(plain).not.toContain("chant lint");
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
|
+
|
|
176
|
+
describe("the GitLab merge-request pipeline records on merge by default (#4168)", () => {
|
|
177
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {} });
|
|
178
|
+
const doc = parseYAML(result.yaml) as Record<string, unknown> & { stages: string[]; record: Job };
|
|
179
|
+
const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-record.gitlab.golden.yml");
|
|
180
|
+
|
|
181
|
+
test("matches its golden", () => {
|
|
182
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(golden, result.yaml);
|
|
183
|
+
expect(result.yaml).toBe(readFileSync(golden, "utf-8"));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("validates against the vendored GitLab CI schema", () => {
|
|
187
|
+
const req = createRequire(import.meta.url);
|
|
188
|
+
const mod = req("ajv") as { default?: unknown };
|
|
189
|
+
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): ((d: unknown) => boolean) & { errors?: unknown } };
|
|
190
|
+
const validate = new Ajv({ strict: false, allErrors: true }).compile(JSON.parse(readFileSync(SCHEMA, "utf-8")));
|
|
191
|
+
expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a push to the default branch runs the record job, which applies nothing and keeps no cache", () => {
|
|
195
|
+
expect(doc.stages).toEqual(["plan", "record"]);
|
|
196
|
+
expect(doc.apply).toBeUndefined();
|
|
197
|
+
expect(doc.record.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
|
|
198
|
+
expect(doc.record.script.at(-1)).toBe('chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab');
|
|
199
|
+
expect(doc.record.resource_group).toBe("chant-record-prod");
|
|
200
|
+
expect(doc.record.cache).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a gate per wave gates the apply, so it needs applyOnMerge", () => {
|
|
204
|
+
expect(() => generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } })).toThrow(/needs --apply-on-merge/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("inside a workspace member the record job takes the member's name and group", () => {
|
|
208
|
+
const scoped = parseYAML(generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "network", dir: "infra/network" } }).yaml) as Record<string, Job>;
|
|
209
|
+
expect(scoped["network-record"].resource_group).toBe("chant-record.network.prod");
|
|
210
|
+
expect(scoped["network-record"].script.at(-1)).toContain("chant ci run record");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -34,7 +34,7 @@ interface Job {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () => {
|
|
37
|
-
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } });
|
|
37
|
+
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" } });
|
|
38
38
|
const doc = parseYAML(result.yaml) as Record<string, unknown> & { stages: string[] };
|
|
39
39
|
|
|
40
40
|
test("matches its golden", () => {
|
|
@@ -63,7 +63,7 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
63
63
|
expect(job.stage).toBe(`apply-wave-${k}`);
|
|
64
64
|
expect(job.needs).toEqual(k > 1 ? [`apply-wave-${k - 1}`] : undefined);
|
|
65
65
|
expect(job.resource_group).toBe("chant-apply-prod");
|
|
66
|
-
// #4156: through chant ci run, which runs pr-apply --
|
|
66
|
+
// #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
|
|
67
67
|
expect(job.script.at(-1)).toBe(`chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab --wave-gate release --wave ${k}`);
|
|
68
68
|
// Every wave shares the commit's record, so wave k pulls what wave k-1 pushed.
|
|
69
69
|
expect(job.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
|
|
@@ -74,7 +74,7 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
74
74
|
|
|
75
75
|
test("inside a workspace member the wave jobs carry the member's name", () => {
|
|
76
76
|
const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
|
|
77
|
-
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" }, member });
|
|
77
|
+
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" }, member });
|
|
78
78
|
expect(scoped.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
79
79
|
["network-plan", []],
|
|
80
80
|
["network-apply-wave-1", []],
|