@intentius/chant-lexicon-gitlab 0.124.34 → 0.124.36
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-pipeline.js +18 -9
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +24 -4
- package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +39 -14
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +22 -4
- package/src/components/generate-pipeline.ts +20 -9
- package/src/components/generate-pr-pipeline.test.ts +17 -10
- package/src/components/generate-pr-waves-pipeline.test.ts +16 -3
|
@@ -256,6 +256,10 @@ function gatedWaveGitlabPipeline(components, env, image, beforeScript, extraScri
|
|
|
256
256
|
* k-1 pushed, and a retry of a wave that stopped at its gate pulls the one it
|
|
257
257
|
* pushed itself.
|
|
258
258
|
*/
|
|
259
|
+
/** The dotenv report a gated apply wave hands the green commit it applies through (#4221). */
|
|
260
|
+
const WAVE_GREEN_DOTENV = "chant-green.env";
|
|
261
|
+
/** The rule of a pipeline for a pushed `ci/green/` tag (#4168 step 9), which runs the apply under apply on merge. */
|
|
262
|
+
const GITLAB_GREEN_TAG_RULE = '$CI_COMMIT_TAG =~ /^ci\\/green\\//';
|
|
259
263
|
function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, options) {
|
|
260
264
|
const loop = options.prLoop;
|
|
261
265
|
const member = options.member;
|
|
@@ -265,24 +269,29 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
265
269
|
const planJob = member ? `${member.name}-plan` : "plan";
|
|
266
270
|
const named = (job) => (member ? `${member.name}-${job}` : job);
|
|
267
271
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
268
|
-
|
|
272
|
+
// #4168 step 9: a push to the branch records, and under apply on merge a ci/green/ tag pipeline applies.
|
|
273
|
+
const greens = loop.waveGate
|
|
269
274
|
? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave)), stage: prApplyWaveJobName(job.wave) }))
|
|
270
275
|
: loop.applyOnMerge
|
|
271
276
|
? [{ job: apply, name: named("apply"), stage: "apply" }]
|
|
272
|
-
: [
|
|
277
|
+
: [];
|
|
278
|
+
const applies = [{ job: record, name: named("record"), stage: "record" }, ...greens];
|
|
273
279
|
const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
|
|
274
280
|
if (loop.branch !== undefined)
|
|
275
281
|
assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
276
282
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
283
|
+
const onGreen = GITLAB_GREEN_TAG_RULE;
|
|
277
284
|
// A member's jobs start in its directory, after the image's install lines.
|
|
278
285
|
const enter = rooted ? [] : [`cd ${memberShellDir(member)}`];
|
|
279
286
|
// 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
|
-
|
|
287
|
+
// #4221: a wave job hands the commit it applies to the next wave as GREEN_SHA, through a dotenv report.
|
|
288
|
+
const handOff = (job) => (job.wave ? [`echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/${WAVE_GREEN_DOTENV}"`] : []);
|
|
289
|
+
const script = (job) => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.checkout ?? []), ...handOff(job), ...(job.setup ?? []), ...(job.build ?? []), job.run.command, ...extraScript];
|
|
281
290
|
const stages = ["plan", ...applies.map((a) => a.stage)];
|
|
282
291
|
const doc = {
|
|
283
292
|
// Included files merge their top-level keys, so a member's file carries
|
|
284
293
|
// the rules every member shares and no name: two names would collide.
|
|
285
|
-
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }] },
|
|
294
|
+
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, ...(greens.length > 0 ? [{ if: onGreen }] : [])] },
|
|
286
295
|
stages,
|
|
287
296
|
variables: { ...options.variables, CHANT_ENV: env, ...GITLAB_FULL_HISTORY },
|
|
288
297
|
[planJob]: {
|
|
@@ -298,9 +307,9 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
298
307
|
{
|
|
299
308
|
stage,
|
|
300
309
|
image,
|
|
301
|
-
...(i >
|
|
310
|
+
...(i > 1 ? { needs: [applies[i - 1].name] } : {}),
|
|
302
311
|
resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
|
|
303
|
-
rules: [{ if: onMerge }],
|
|
312
|
+
rules: [{ if: job.jobName === "record" ? onMerge : onGreen }],
|
|
304
313
|
variables: job.run.env,
|
|
305
314
|
script: script(job),
|
|
306
315
|
// The attempt record (#3543), kept under the pushed commit even when
|
|
@@ -315,7 +324,7 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
315
324
|
when: "always",
|
|
316
325
|
},
|
|
317
326
|
}),
|
|
318
|
-
artifacts: { when: "always", paths: [reportPath] },
|
|
327
|
+
artifacts: { when: "always", paths: [reportPath], ...(job.wave ? { reports: { dotenv: WAVE_GREEN_DOTENV } } : {}) },
|
|
319
328
|
},
|
|
320
329
|
])),
|
|
321
330
|
};
|
|
@@ -323,9 +332,9 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
323
332
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
324
333
|
...applies.map(({ job, name, stage }, i) => ({
|
|
325
334
|
jobName: name,
|
|
326
|
-
component: job.wave ? `
|
|
335
|
+
component: job.wave ? `green apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "green apply",
|
|
327
336
|
stage,
|
|
328
|
-
needs: i >
|
|
337
|
+
needs: i > 1 ? [applies[i - 1].name] : [],
|
|
329
338
|
})),
|
|
330
339
|
];
|
|
331
340
|
const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, ...applies.map((a) => a.name)].map((key) => [key, doc[key]]));
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "010ddb2b4be32c44dd4143e0edf74c071bd7172f669963e566fcf0f9bb98ee35",
|
|
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": "6417e0110043d03afc8b64c4ec5e2b971f41d3e14b74df94ac1a5dfa5c192d34"
|
|
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.36",
|
|
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.36",
|
|
71
|
+
"@intentius/chant-lexicon-github": "^0.124.36"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@intentius/chant-lexicon-github": {
|
|
@@ -3,9 +3,11 @@ workflow:
|
|
|
3
3
|
rules:
|
|
4
4
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
5
5
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
6
7
|
|
|
7
8
|
stages:
|
|
8
9
|
- plan
|
|
10
|
+
- record
|
|
9
11
|
- apply
|
|
10
12
|
|
|
11
13
|
variables:
|
|
@@ -30,10 +32,10 @@ plan:
|
|
|
30
32
|
paths:
|
|
31
33
|
- .chant/pr
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
stage:
|
|
35
|
+
record:
|
|
36
|
+
stage: record
|
|
35
37
|
image: node:22
|
|
36
|
-
resource_group: chant-
|
|
38
|
+
resource_group: chant-record-prod
|
|
37
39
|
rules:
|
|
38
40
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
39
41
|
variables:
|
|
@@ -43,7 +45,25 @@ apply:
|
|
|
43
45
|
- 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
|
|
44
46
|
- chant build --env prod
|
|
45
47
|
- chant lint
|
|
46
|
-
- chant ci run
|
|
48
|
+
- chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab
|
|
49
|
+
artifacts:
|
|
50
|
+
when: always
|
|
51
|
+
paths:
|
|
52
|
+
- .chant/pr
|
|
53
|
+
|
|
54
|
+
apply:
|
|
55
|
+
stage: apply
|
|
56
|
+
image: node:22
|
|
57
|
+
resource_group: chant-apply-prod
|
|
58
|
+
rules:
|
|
59
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
60
|
+
variables: {}
|
|
61
|
+
script:
|
|
62
|
+
- 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)
|
|
63
|
+
- git fetch --quiet --tags origin "+refs/heads/$CI_DEFAULT_BRANCH:refs/remotes/origin/$CI_DEFAULT_BRANCH" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
64
|
+
- chant build --env prod
|
|
65
|
+
- chant lint
|
|
66
|
+
- chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab
|
|
47
67
|
cache:
|
|
48
68
|
key: chant-apply-prod-$CI_COMMIT_SHA
|
|
49
69
|
paths:
|
|
@@ -3,9 +3,11 @@ workflow:
|
|
|
3
3
|
rules:
|
|
4
4
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
5
5
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
6
7
|
|
|
7
8
|
stages:
|
|
8
9
|
- plan
|
|
10
|
+
- record
|
|
9
11
|
- apply-wave-1
|
|
10
12
|
- apply-wave-2
|
|
11
13
|
- apply-wave-3
|
|
@@ -30,10 +32,10 @@ plan:
|
|
|
30
32
|
paths:
|
|
31
33
|
- .chant/pr
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
stage:
|
|
35
|
+
record:
|
|
36
|
+
stage: record
|
|
35
37
|
image: node:22
|
|
36
|
-
resource_group: chant-
|
|
38
|
+
resource_group: chant-record-prod
|
|
37
39
|
rules:
|
|
38
40
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
39
41
|
variables:
|
|
@@ -41,7 +43,24 @@ apply-wave-1:
|
|
|
41
43
|
script:
|
|
42
44
|
- 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)
|
|
43
45
|
- 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
|
|
44
|
-
- chant ci run
|
|
46
|
+
- chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab
|
|
47
|
+
artifacts:
|
|
48
|
+
when: always
|
|
49
|
+
paths:
|
|
50
|
+
- .chant/pr
|
|
51
|
+
|
|
52
|
+
apply-wave-1:
|
|
53
|
+
stage: apply-wave-1
|
|
54
|
+
image: node:22
|
|
55
|
+
resource_group: chant-apply-prod
|
|
56
|
+
rules:
|
|
57
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
58
|
+
variables: {}
|
|
59
|
+
script:
|
|
60
|
+
- 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)
|
|
61
|
+
- git fetch --quiet --tags origin "+refs/heads/$CI_DEFAULT_BRANCH:refs/remotes/origin/$CI_DEFAULT_BRANCH" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
62
|
+
- echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/chant-green.env"
|
|
63
|
+
- chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 1
|
|
45
64
|
cache:
|
|
46
65
|
key: chant-apply-prod-$CI_COMMIT_SHA
|
|
47
66
|
paths:
|
|
@@ -51,6 +70,8 @@ apply-wave-1:
|
|
|
51
70
|
when: always
|
|
52
71
|
paths:
|
|
53
72
|
- .chant/pr
|
|
73
|
+
reports:
|
|
74
|
+
dotenv: chant-green.env
|
|
54
75
|
|
|
55
76
|
apply-wave-2:
|
|
56
77
|
stage: apply-wave-2
|
|
@@ -59,13 +80,13 @@ apply-wave-2:
|
|
|
59
80
|
- apply-wave-1
|
|
60
81
|
resource_group: chant-apply-prod
|
|
61
82
|
rules:
|
|
62
|
-
- if: '$
|
|
63
|
-
variables:
|
|
64
|
-
BASE_SHA: '$CI_COMMIT_BEFORE_SHA'
|
|
83
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
84
|
+
variables: {}
|
|
65
85
|
script:
|
|
66
86
|
- 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)
|
|
67
|
-
-
|
|
68
|
-
-
|
|
87
|
+
- test -n "$GREEN_SHA" && git fetch --quiet --tags origin "+refs/heads/$CI_DEFAULT_BRANCH:refs/remotes/origin/$CI_DEFAULT_BRANCH" && git checkout --quiet --detach "$GREEN_SHA"
|
|
88
|
+
- echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/chant-green.env"
|
|
89
|
+
- chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 2
|
|
69
90
|
cache:
|
|
70
91
|
key: chant-apply-prod-$CI_COMMIT_SHA
|
|
71
92
|
paths:
|
|
@@ -75,6 +96,8 @@ apply-wave-2:
|
|
|
75
96
|
when: always
|
|
76
97
|
paths:
|
|
77
98
|
- .chant/pr
|
|
99
|
+
reports:
|
|
100
|
+
dotenv: chant-green.env
|
|
78
101
|
|
|
79
102
|
apply-wave-3:
|
|
80
103
|
stage: apply-wave-3
|
|
@@ -83,13 +106,13 @@ apply-wave-3:
|
|
|
83
106
|
- apply-wave-2
|
|
84
107
|
resource_group: chant-apply-prod
|
|
85
108
|
rules:
|
|
86
|
-
- if: '$
|
|
87
|
-
variables:
|
|
88
|
-
BASE_SHA: '$CI_COMMIT_BEFORE_SHA'
|
|
109
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
110
|
+
variables: {}
|
|
89
111
|
script:
|
|
90
112
|
- 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)
|
|
91
|
-
-
|
|
92
|
-
-
|
|
113
|
+
- test -n "$GREEN_SHA" && git fetch --quiet --tags origin "+refs/heads/$CI_DEFAULT_BRANCH:refs/remotes/origin/$CI_DEFAULT_BRANCH" && git checkout --quiet --detach "$GREEN_SHA"
|
|
114
|
+
- echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/chant-green.env"
|
|
115
|
+
- chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 3
|
|
93
116
|
cache:
|
|
94
117
|
key: chant-apply-prod-$CI_COMMIT_SHA
|
|
95
118
|
paths:
|
|
@@ -99,3 +122,5 @@ apply-wave-3:
|
|
|
99
122
|
when: always
|
|
100
123
|
paths:
|
|
101
124
|
- .chant/pr
|
|
125
|
+
reports:
|
|
126
|
+
dotenv: chant-green.env
|
|
@@ -3,9 +3,11 @@ workflow:
|
|
|
3
3
|
rules:
|
|
4
4
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
5
5
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
6
7
|
|
|
7
8
|
stages:
|
|
8
9
|
- plan
|
|
10
|
+
- record
|
|
9
11
|
- apply
|
|
10
12
|
|
|
11
13
|
variables:
|
|
@@ -28,10 +30,10 @@ plan:
|
|
|
28
30
|
paths:
|
|
29
31
|
- .chant/pr
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
stage:
|
|
33
|
+
record:
|
|
34
|
+
stage: record
|
|
33
35
|
image: node:22
|
|
34
|
-
resource_group: chant-
|
|
36
|
+
resource_group: chant-record-prod
|
|
35
37
|
rules:
|
|
36
38
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
37
39
|
variables:
|
|
@@ -39,7 +41,23 @@ apply:
|
|
|
39
41
|
script:
|
|
40
42
|
- 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
43
|
- 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
|
|
44
|
+
- chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge gitlab
|
|
45
|
+
artifacts:
|
|
46
|
+
when: always
|
|
47
|
+
paths:
|
|
48
|
+
- .chant/pr
|
|
49
|
+
|
|
50
|
+
apply:
|
|
51
|
+
stage: apply
|
|
52
|
+
image: node:22
|
|
53
|
+
resource_group: chant-apply-prod
|
|
54
|
+
rules:
|
|
55
|
+
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
56
|
+
variables: {}
|
|
57
|
+
script:
|
|
58
|
+
- 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)
|
|
59
|
+
- git fetch --quiet --tags origin "+refs/heads/$CI_DEFAULT_BRANCH:refs/remotes/origin/$CI_DEFAULT_BRANCH" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
60
|
+
- chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab
|
|
43
61
|
cache:
|
|
44
62
|
key: chant-apply-prod-$CI_COMMIT_SHA
|
|
45
63
|
paths:
|
|
@@ -300,6 +300,12 @@ function gatedWaveGitlabPipeline(
|
|
|
300
300
|
* k-1 pushed, and a retry of a wave that stopped at its gate pulls the one it
|
|
301
301
|
* pushed itself.
|
|
302
302
|
*/
|
|
303
|
+
/** The dotenv report a gated apply wave hands the green commit it applies through (#4221). */
|
|
304
|
+
const WAVE_GREEN_DOTENV = "chant-green.env";
|
|
305
|
+
|
|
306
|
+
/** The rule of a pipeline for a pushed `ci/green/` tag (#4168 step 9), which runs the apply under apply on merge. */
|
|
307
|
+
const GITLAB_GREEN_TAG_RULE = '$CI_COMMIT_TAG =~ /^ci\\/green\\//';
|
|
308
|
+
|
|
303
309
|
function prLoopGitlabPipeline(
|
|
304
310
|
components: DriverComponent[],
|
|
305
311
|
env: string,
|
|
@@ -316,23 +322,28 @@ function prLoopGitlabPipeline(
|
|
|
316
322
|
const planJob = member ? `${member.name}-plan` : "plan";
|
|
317
323
|
const named = (job: string) => (member ? `${member.name}-${job}` : job);
|
|
318
324
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
319
|
-
|
|
325
|
+
// #4168 step 9: a push to the branch records, and under apply on merge a ci/green/ tag pipeline applies.
|
|
326
|
+
const greens = loop.waveGate
|
|
320
327
|
? waveJobs.map((job) => ({ job, name: named(prApplyWaveJobName(job.wave!)), stage: prApplyWaveJobName(job.wave!) }))
|
|
321
328
|
: loop.applyOnMerge
|
|
322
329
|
? [{ job: apply, name: named("apply"), stage: "apply" }]
|
|
323
|
-
: [
|
|
330
|
+
: [];
|
|
331
|
+
const applies = [{ job: record, name: named("record"), stage: "record" }, ...greens];
|
|
324
332
|
const onMergeRequest = GITLAB_MERGE_REQUEST_SOURCE;
|
|
325
333
|
if (loop.branch !== undefined) assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
326
334
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
335
|
+
const onGreen = GITLAB_GREEN_TAG_RULE;
|
|
327
336
|
// A member's jobs start in its directory, after the image's install lines.
|
|
328
337
|
const enter = rooted ? [] : [`cd ${memberShellDir(member!)}`];
|
|
329
338
|
// The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY variable is set.
|
|
330
|
-
|
|
339
|
+
// #4221: a wave job hands the commit it applies to the next wave as GREEN_SHA, through a dotenv report.
|
|
340
|
+
const handOff = (job: typeof plan): string[] => (job.wave ? [`echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/${WAVE_GREEN_DOTENV}"`] : []);
|
|
341
|
+
const script = (job: typeof plan): string[] => [...prLoopSetup(image), ...enter, ...beforeScript, ...(job.checkout ?? []), ...handOff(job), ...(job.setup ?? []), ...(job.build ?? []), job.run.command, ...extraScript];
|
|
331
342
|
const stages = ["plan", ...applies.map((a) => a.stage)];
|
|
332
343
|
const doc: Record<string, unknown> = {
|
|
333
344
|
// Included files merge their top-level keys, so a member's file carries
|
|
334
345
|
// the rules every member shares and no name: two names would collide.
|
|
335
|
-
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }] },
|
|
346
|
+
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, ...(greens.length > 0 ? [{ if: onGreen }] : [])] },
|
|
336
347
|
stages,
|
|
337
348
|
variables: { ...options.variables, CHANT_ENV: env, ...GITLAB_FULL_HISTORY },
|
|
338
349
|
[planJob]: {
|
|
@@ -349,9 +360,9 @@ function prLoopGitlabPipeline(
|
|
|
349
360
|
{
|
|
350
361
|
stage,
|
|
351
362
|
image,
|
|
352
|
-
...(i >
|
|
363
|
+
...(i > 1 ? { needs: [applies[i - 1]!.name] } : {}),
|
|
353
364
|
resource_group: job.jobName === "record" ? prRecordGroup(env, member?.name) : prApplyGroup(env, member?.name),
|
|
354
|
-
rules: [{ if: onMerge }],
|
|
365
|
+
rules: [{ if: job.jobName === "record" ? onMerge : onGreen }],
|
|
355
366
|
variables: job.run.env,
|
|
356
367
|
script: script(job),
|
|
357
368
|
// The attempt record (#3543), kept under the pushed commit even when
|
|
@@ -366,7 +377,7 @@ function prLoopGitlabPipeline(
|
|
|
366
377
|
when: "always",
|
|
367
378
|
},
|
|
368
379
|
}),
|
|
369
|
-
artifacts: { when: "always", paths: [reportPath] },
|
|
380
|
+
artifacts: { when: "always", paths: [reportPath], ...(job.wave ? { reports: { dotenv: WAVE_GREEN_DOTENV } } : {}) },
|
|
370
381
|
},
|
|
371
382
|
]),
|
|
372
383
|
),
|
|
@@ -375,9 +386,9 @@ function prLoopGitlabPipeline(
|
|
|
375
386
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
376
387
|
...applies.map(({ job, name, stage }, i) => ({
|
|
377
388
|
jobName: name,
|
|
378
|
-
component: job.wave ? `
|
|
389
|
+
component: job.wave ? `green apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : "green apply",
|
|
379
390
|
stage,
|
|
380
|
-
needs: i >
|
|
391
|
+
needs: i > 1 ? [applies[i - 1]!.name] : [],
|
|
381
392
|
})),
|
|
382
393
|
];
|
|
383
394
|
const yaml = emitGitlabFile(["workflow", "stages", "variables", planJob, ...applies.map((a) => a.name)].map((key) => [key, doc[key]] as const));
|
|
@@ -12,7 +12,7 @@ import { describe, expect, test } from "vitest";
|
|
|
12
12
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
13
13
|
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
14
14
|
import { generateGitlabPipeline } from "./generate-pipeline";
|
|
15
|
-
import { PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
15
|
+
import { greenCheckout, PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
16
16
|
|
|
17
17
|
const ESTATE: DriverComponent[] = [
|
|
18
18
|
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
@@ -30,11 +30,12 @@ interface Job {
|
|
|
30
30
|
image?: string;
|
|
31
31
|
artifacts: Record<string, unknown>;
|
|
32
32
|
cache?: Record<string, unknown>;
|
|
33
|
+
needs?: string[];
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
describe("the GitLab merge-request pipeline", () => {
|
|
36
37
|
const result = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
37
|
-
const doc = parseYAML(result.yaml) as { workflow: { rules: unknown[] }; variables: Record<string, string>; plan: Job; apply: Job };
|
|
38
|
+
const doc = parseYAML(result.yaml) as { workflow: { rules: unknown[] }; variables: Record<string, string>; plan: Job; record: Job; apply: Job };
|
|
38
39
|
|
|
39
40
|
test("matches its golden", () => {
|
|
40
41
|
if (process.env.UPDATE_GOLDEN) writeFileSync(GOLDEN, result.yaml);
|
|
@@ -50,15 +51,21 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
50
51
|
expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
test("plans in a merge request pipeline from its diff base,
|
|
54
|
+
test("plans in a merge request pipeline from its diff base, records on a push to the default branch, applies on a ci/green/ tag, one at a time", () => {
|
|
54
55
|
expect(doc.plan.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]);
|
|
55
56
|
expect(doc.plan.variables).toEqual({ BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" });
|
|
56
57
|
expect(doc.plan.script).toEqual([
|
|
57
58
|
...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
|
-
expect(doc.
|
|
59
|
-
expect(doc.
|
|
59
|
+
expect(doc.record.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }]);
|
|
60
|
+
expect(doc.record.variables).toEqual({ BASE_SHA: "$CI_COMMIT_BEFORE_SHA" });
|
|
61
|
+
// #4168 step 9: a pushed ci/green/ tag runs the apply, which checks out the newest green commit and reads no push base.
|
|
62
|
+
expect(doc.apply.rules).toEqual([{ if: "$CI_COMMIT_TAG =~ /^ci\\/green\\//" }]);
|
|
63
|
+
expect(doc.workflow.rules).toContainEqual({ if: "$CI_COMMIT_TAG =~ /^ci\\/green\\//" });
|
|
64
|
+
expect(doc.apply.variables).toEqual({});
|
|
65
|
+
expect(doc.apply.script.at(-2)).toBe(greenCheckout("$CI_DEFAULT_BRANCH"));
|
|
66
|
+
expect(doc.apply.needs).toBeUndefined();
|
|
60
67
|
// #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
|
-
expect(doc.apply.script.at(-1)).toBe(
|
|
68
|
+
expect(doc.apply.script.at(-1)).toBe("chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab");
|
|
62
69
|
expect(result.yaml).not.toContain("CHANT_RUNNER_KEY");
|
|
63
70
|
expect(doc.apply.resource_group).toBe("chant-apply-prod");
|
|
64
71
|
expect(doc.variables.GIT_DEPTH).toBe("0");
|
|
@@ -84,10 +91,10 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
test("an all-zero CI_COMMIT_BEFORE_SHA falls back to the merge request diff base, then the target branch", () => {
|
|
87
|
-
const line = doc.
|
|
94
|
+
const line = doc.record.script.find((l) => l.includes("CI_MERGE_REQUEST_DIFF_BASE_SHA"))!;
|
|
88
95
|
expect(line).toContain("0000000000000000000000000000000000000000");
|
|
89
96
|
expect(line).toContain("git fetch origin $CI_DEFAULT_BRANCH");
|
|
90
|
-
expect(doc.
|
|
97
|
+
expect(doc.record.script.indexOf(line)).toBeLessThan(doc.record.script.length - 1);
|
|
91
98
|
expect(doc.plan.script.some((l) => l.includes("CI_MERGE_REQUEST_DIFF_BASE_SHA"))).toBe(false);
|
|
92
99
|
});
|
|
93
100
|
|
|
@@ -114,11 +121,11 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
114
121
|
const member = { name: "network", dir: "infra/network", file: ".gitlab/ci/chant-pr.network.prod.gitlab-ci.yml" };
|
|
115
122
|
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member });
|
|
116
123
|
const mdoc = parseYAML(scoped.yaml) as Record<string, unknown> & { workflow: { name?: string; rules: unknown[] } };
|
|
117
|
-
expect(scoped.jobs.map((j) => j.jobName)).toEqual(["network-plan", "network-apply"]);
|
|
124
|
+
expect(scoped.jobs.map((j) => j.jobName)).toEqual(["network-plan", "network-record", "network-apply"]);
|
|
118
125
|
expect(mdoc.plan).toBeUndefined();
|
|
119
126
|
// Included files merge top-level keys: no name, so members do not collide on it.
|
|
120
127
|
expect(mdoc.workflow.name).toBeUndefined();
|
|
121
|
-
expect(mdoc.workflow.rules).toHaveLength(
|
|
128
|
+
expect(mdoc.workflow.rules).toHaveLength(3);
|
|
122
129
|
const plan = mdoc["network-plan"] as Job;
|
|
123
130
|
const apply = mdoc["network-apply"] as Job;
|
|
124
131
|
expect(plan.script).toContain("cd infra/network");
|
|
@@ -42,6 +42,17 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
42
42
|
expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test("wave 1 takes the newest green commit, and each later wave applies the one wave 1 took (#4221)", () => {
|
|
46
|
+
for (const k of [1, 2, 3]) {
|
|
47
|
+
const job = doc[`apply-wave-${k}`] as { script: string[]; artifacts: { reports?: { dotenv?: string } } };
|
|
48
|
+
expect(job.artifacts.reports).toEqual({ dotenv: "chant-green.env" });
|
|
49
|
+
expect(job.script).toContain('echo "GREEN_SHA=$GREEN_SHA" > "$CI_PROJECT_DIR/chant-green.env"');
|
|
50
|
+
const lastGreen = job.script.some((l) => l.includes("chant ci last-green"));
|
|
51
|
+
expect(lastGreen).toBe(k === 1);
|
|
52
|
+
if (k > 1) expect(job.script.some((l) => l.startsWith('test -n "$GREEN_SHA"'))).toBe(true);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
test("validates against the vendored GitLab CI schema", () => {
|
|
46
57
|
const req = createRequire(import.meta.url);
|
|
47
58
|
const mod = req("ajv") as { default?: unknown };
|
|
@@ -51,9 +62,10 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
51
62
|
});
|
|
52
63
|
|
|
53
64
|
test("one apply job and stage per wave, each needing the one before, on the merge, one at a time", () => {
|
|
54
|
-
expect(doc.stages).toEqual(["plan", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
65
|
+
expect(doc.stages).toEqual(["plan", "record", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
55
66
|
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
56
67
|
["plan", []],
|
|
68
|
+
["record", []],
|
|
57
69
|
["apply-wave-1", []],
|
|
58
70
|
["apply-wave-2", ["apply-wave-1"]],
|
|
59
71
|
["apply-wave-3", ["apply-wave-2"]],
|
|
@@ -64,7 +76,7 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
64
76
|
expect(job.needs).toEqual(k > 1 ? [`apply-wave-${k - 1}`] : undefined);
|
|
65
77
|
expect(job.resource_group).toBe("chant-apply-prod");
|
|
66
78
|
// #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
|
|
67
|
-
expect(job.script.at(-1)).toBe(`chant ci run apply --in-place --
|
|
79
|
+
expect(job.script.at(-1)).toBe(`chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab --wave-gate release --wave ${k}`);
|
|
68
80
|
// Every wave shares the commit's record, so wave k pulls what wave k-1 pushed.
|
|
69
81
|
expect(job.cache).toEqual({ key: "chant-apply-prod-$CI_COMMIT_SHA", paths: [".chant/pr-resume/pr-apply.json"], when: "always" });
|
|
70
82
|
}
|
|
@@ -77,12 +89,13 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
77
89
|
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" }, member });
|
|
78
90
|
expect(scoped.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
79
91
|
["network-plan", []],
|
|
92
|
+
["network-record", []],
|
|
80
93
|
["network-apply-wave-1", []],
|
|
81
94
|
["network-apply-wave-2", ["network-apply-wave-1"]],
|
|
82
95
|
["network-apply-wave-3", ["network-apply-wave-2"]],
|
|
83
96
|
]);
|
|
84
97
|
// ci run, started in the member's directory, passes --member network to pr-apply.
|
|
85
98
|
expect(scoped.yaml).toContain("cd infra/network");
|
|
86
|
-
expect(scoped.yaml).toContain("chant ci run apply --in-place --
|
|
99
|
+
expect(scoped.yaml).toContain("chant ci run apply --in-place --env prod --gate pr-apply --forge gitlab --wave-gate release --wave 3");
|
|
87
100
|
});
|
|
88
101
|
});
|