@intentius/chant-lexicon-gitlab 0.124.46 → 0.124.49
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 +17 -0
- package/dist/components/generate-op-waves-pipeline.d.ts +10 -2
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +16 -5
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +31 -16
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -1
- package/package.json +3 -3
- package/src/components/__fixtures__/pr-loop-build.gitlab.golden.yml +3 -10
- package/src/components/__fixtures__/pr-loop-record.gitlab.golden.yml +3 -10
- package/src/components/__fixtures__/pr-loop-waves.gitlab.golden.yml +3 -10
- package/src/components/__fixtures__/pr-loop.gitlab.golden.yml +3 -10
- package/src/components/__golden__/op-waves-jobs.yml +30 -1
- package/src/components/generate-op-pipeline.review.test.ts +21 -0
- package/src/components/generate-op-pipeline.ts +19 -0
- package/src/components/generate-op-waves-pipeline.ts +16 -5
- package/src/components/generate-pipeline.ts +33 -15
- package/src/components/generate-pr-env-pipeline.test.ts +2 -2
- package/src/components/generate-pr-pipeline.test.ts +16 -8
- package/src/components/generate-pr-waves-pipeline.test.ts +2 -2
- package/src/plugin.ts +2 -1
|
@@ -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;AA8IhE;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAuGxB"}
|
|
@@ -164,6 +164,22 @@ function assertTriggerSupportsMode(name, mode, trigger) {
|
|
|
164
164
|
`no merge request to post on. Give it a { kind: "pull_request" } trigger, or use findingMode "issue" ` +
|
|
165
165
|
`or "merge-request".`);
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Refuse `reviewAgent` (#4326). The split posts the review from a second job
|
|
169
|
+
* that holds the forge write token, and a GitLab merge request pipeline hands
|
|
170
|
+
* every job the project's unprotected CI/CD variables, the review job's
|
|
171
|
+
* included. A token protected enough to stay out of it is not available to a
|
|
172
|
+
* job of that pipeline either, so the post has to start from a pipeline of
|
|
173
|
+
* the default branch, which this generator does not build. The refusal names
|
|
174
|
+
* the Op and the way out rather than emitting a review job that holds the token.
|
|
175
|
+
*/
|
|
176
|
+
function assertNoReviewSplit(spec) {
|
|
177
|
+
if (!spec.reviewAgent)
|
|
178
|
+
return;
|
|
179
|
+
throw new Error(`Scheduled Op "${spec.name}" sets reviewAgent, which keeps the model's job free of the forge write token and posts the note from a second job. ` +
|
|
180
|
+
`A GitLab merge request pipeline hands every job the project's unprotected variables, so the gitlab generator does not build that split. ` +
|
|
181
|
+
`Drop reviewAgent and run the review with findingMode "comment" (the model command still runs with a cleared environment), or generate this Op for GitHub or Forgejo.`);
|
|
182
|
+
}
|
|
167
183
|
/**
|
|
168
184
|
* The follow-up line an Op with an `environment` adds under its own header
|
|
169
185
|
* line (#2257). The `environment:` key on the job binds it to the
|
|
@@ -235,6 +251,7 @@ export function generateGitlabOpPipeline(ops, options = {}) {
|
|
|
235
251
|
const findingMode = spec.findingMode ?? "report";
|
|
236
252
|
const trigger = resolveOpTrigger(spec);
|
|
237
253
|
assertTriggerSupportsMode(spec.name, findingMode, trigger);
|
|
254
|
+
assertNoReviewSplit(spec);
|
|
238
255
|
if (trigger.kind === "cron")
|
|
239
256
|
anyCron = true;
|
|
240
257
|
const jobName = jobPrefix + ciJobName(spec.name);
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One file, one stage per wave, on a push to the spec's branches. Each job is
|
|
5
5
|
* one of core's `opWaveJobs`, and `needs:` chains them so a wave that waits at
|
|
6
|
-
* its gate (exit 3)
|
|
7
|
-
*
|
|
6
|
+
* its gate (exit 3) stops every later wave; retrying the failed job after
|
|
7
|
+
* `chant approve` carries on, and the jobs it skipped run after it.
|
|
8
|
+
*
|
|
9
|
+
* A job that exits 1 (a run failed, or was blocked) is an allowed failure
|
|
10
|
+
* (`allow_failure: exit_codes: [1]`, #4322), so the next wave's jobs run
|
|
11
|
+
* after it and block only the runs that depend on a failed one. Every job
|
|
12
|
+
* keeps the outcome directory as an artifact, holding the outcome files it
|
|
13
|
+
* received through `needs:` and its own, so each later job reads every
|
|
14
|
+
* earlier one. Any other non-zero exit still stops the waves after it.
|
|
15
|
+
* `GIT_DEPTH: "0"` gives the runner the base
|
|
8
16
|
* commit it reads the gate policy at. A wide wave's deciding job keeps its
|
|
9
17
|
* decision as an artifact, which GitLab hands each share job through `needs:`.
|
|
10
18
|
* A wave's `environment` is the job's own `environment:`; protect it in the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAkBH,OAAO,EAAqE,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACnI,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAI9F,gHAAgH;AAChH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CA+FvH"}
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One file, one stage per wave, on a push to the spec's branches. Each job is
|
|
5
5
|
* one of core's `opWaveJobs`, and `needs:` chains them so a wave that waits at
|
|
6
|
-
* its gate (exit 3)
|
|
7
|
-
*
|
|
6
|
+
* its gate (exit 3) stops every later wave; retrying the failed job after
|
|
7
|
+
* `chant approve` carries on, and the jobs it skipped run after it.
|
|
8
|
+
*
|
|
9
|
+
* A job that exits 1 (a run failed, or was blocked) is an allowed failure
|
|
10
|
+
* (`allow_failure: exit_codes: [1]`, #4322), so the next wave's jobs run
|
|
11
|
+
* after it and block only the runs that depend on a failed one. Every job
|
|
12
|
+
* keeps the outcome directory as an artifact, holding the outcome files it
|
|
13
|
+
* received through `needs:` and its own, so each later job reads every
|
|
14
|
+
* earlier one. Any other non-zero exit still stops the waves after it.
|
|
15
|
+
* `GIT_DEPTH: "0"` gives the runner the base
|
|
8
16
|
* commit it reads the gate policy at. A wide wave's deciding job keeps its
|
|
9
17
|
* decision as an artifact, which GitLab hands each share job through `needs:`.
|
|
10
18
|
* A wave's `environment` is the job's own `environment:`; protect it in the
|
|
@@ -31,7 +39,7 @@
|
|
|
31
39
|
* checkout runs.
|
|
32
40
|
*/
|
|
33
41
|
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";
|
|
34
|
-
import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
|
|
42
|
+
import { opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
|
|
35
43
|
import { gitlabTradeIdTokens, gitlabTradeLines, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
|
|
36
44
|
import { emitGitlabFile } from "./ci-jobs.js";
|
|
37
45
|
/** Render an Op waves spec as one GitLab CI file, `<name>.gitlab-ci.yml` unless `opsFileName` names another. */
|
|
@@ -105,7 +113,9 @@ export function generateGitlabOpWavesPipeline(spec, options) {
|
|
|
105
113
|
rules,
|
|
106
114
|
...(job.needs.length > 0 ? { needs: job.needs } : {}),
|
|
107
115
|
script: [...(cloud ? gitlabTradeLines() : []), ...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
|
|
108
|
-
|
|
116
|
+
// #4322: a run that failed, or was blocked, lets the next wave run; any other failure stops it.
|
|
117
|
+
allow_failure: { exit_codes: [1] },
|
|
118
|
+
artifacts: { when: "always", paths: [...(job.kind === "decide" ? [job.record] : []), `${opWaveOutcomeDir(spec)}/`], expire_in: "30 days" },
|
|
109
119
|
},
|
|
110
120
|
]);
|
|
111
121
|
}
|
|
@@ -124,7 +134,8 @@ export function generateGitlabOpWavesPipeline(spec, options) {
|
|
|
124
134
|
const header = [
|
|
125
135
|
`# chant Op waves "${spec.name}": one stage per wave, each needing the one before.`,
|
|
126
136
|
"# A wave that waits at its gate exits 3 and stops the waves after it; approve the",
|
|
127
|
-
"# digest it prints, then retry the job.",
|
|
137
|
+
"# digest it prints, then retry the job. A job whose runs failed exits 1, an allowed",
|
|
138
|
+
"# failure, and the waves after it run what does not depend on those runs.",
|
|
128
139
|
...(spec.resume
|
|
129
140
|
? [`# The ${spec.name}-resume job retries it for you in a pipeline schedule; give the schedule the cron "${spec.resume.schedule}".`]
|
|
130
141
|
: []),
|
|
@@ -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;AA0BjG,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, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, GITLAB_APPLY_PR_VAR, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
|
|
38
|
+
import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, GITLAB_APPLY_PR_VAR, GITLAB_PLAN_NOTES_VAR, gitlabPlanJobName, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
|
|
39
39
|
import { memberGitlabChanges, memberRepoPath, memberShellDir } from "@intentius/chant/lexicon";
|
|
40
40
|
import { FORGE_TOKEN_VARS, GITLAB_DROP_FORGE_TOKENS } from "@intentius/chant/ci-handover";
|
|
41
41
|
import { gitlabTradeIdTokens, gitlabTradeLines, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
|
|
@@ -263,12 +263,16 @@ function gatedWaveGitlabPipeline(components, env, image, beforeScript, extraScri
|
|
|
263
263
|
* The plan job holds no forge token while the merge request's code runs
|
|
264
264
|
* (#4295, `@intentius/chant/ci-handover`): its script starts by unsetting
|
|
265
265
|
* the token variables, and `pr-plan --no-status` writes the note and the
|
|
266
|
-
* status into its report
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* `
|
|
271
|
-
*
|
|
266
|
+
* status into its report, which it keeps as its artifact. GitLab builds a
|
|
267
|
+
* merge request pipeline from the merge request's own files, so no job in it
|
|
268
|
+
* may hold the token (#4328): `CHANT_FORGE_TOKEN` and `CHANT_RUNNER_KEY` are
|
|
269
|
+
* protected variables, which a merge request pipeline never gets. The
|
|
270
|
+
* `plan-note` job runs in a pipeline of the target branch, built from that
|
|
271
|
+
* branch's files, that a pipeline schedule (or a person) starts with
|
|
272
|
+
* `CHANT_PLAN_NOTES` set to `1`. It runs `chant components pr-post --open`,
|
|
273
|
+
* which asks GitLab for each open merge request's plan job, reads the report
|
|
274
|
+
* from its artifacts and posts the note and the status for each job that
|
|
275
|
+
* settled since the last run.
|
|
272
276
|
*
|
|
273
277
|
* With `identity` (#4296, `@intentius/chant/ci-identity`) the plan, record
|
|
274
278
|
* and apply jobs take an `id_tokens:` entry per cloud, and each script
|
|
@@ -290,6 +294,15 @@ const WAVE_GREEN_DOTENV = "chant-green.env";
|
|
|
290
294
|
function gitlabApplyPrRule(branch) {
|
|
291
295
|
return `$CI_PIPELINE_SOURCE =~ /^(web|api|trigger)$/ && $${GITLAB_APPLY_PR_VAR} =~ /^[0-9]+$/ && $CI_COMMIT_BRANCH == ${branch}`;
|
|
292
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The rule of the plan-note job (#4328): a pipeline of `branch` that a
|
|
299
|
+
* pipeline schedule, a person or a trigger started with `CHANT_PLAN_NOTES`
|
|
300
|
+
* set to `1`. A merge request pipeline's own trigger job starts a pipeline
|
|
301
|
+
* whose source is `pipeline`, which this rule leaves out.
|
|
302
|
+
*/
|
|
303
|
+
function gitlabPlanNotesRule(branch) {
|
|
304
|
+
return `$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $${GITLAB_PLAN_NOTES_VAR} == "1" && $CI_COMMIT_BRANCH == ${branch}`;
|
|
305
|
+
}
|
|
293
306
|
/** The rule of a pipeline for a pushed `ci/green/` tag (#4168 step 9), which runs the apply under apply on merge. */
|
|
294
307
|
const GITLAB_GREEN_TAG_RULE = '$CI_COMMIT_TAG =~ /^ci\\/green\\//';
|
|
295
308
|
function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript, options) {
|
|
@@ -298,7 +311,8 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
298
311
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
299
312
|
const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
300
313
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
301
|
-
|
|
314
|
+
// #4328: the plan-note job's pr-post --open finds the plan job by this name.
|
|
315
|
+
const planJob = gitlabPlanJobName(member?.name);
|
|
302
316
|
const named = (job) => (member ? `${member.name}-${job}` : job);
|
|
303
317
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
304
318
|
// #4168 step 9: a push to the branch records, and under apply on merge a ci/green/ tag pipeline applies.
|
|
@@ -313,6 +327,8 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
313
327
|
assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
314
328
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
315
329
|
// #4301: the apply before merge runs in a pipeline of the target branch a person starts with CHANT_APPLY_PR set.
|
|
330
|
+
// #4328: the plan notes are posted from a pipeline of the target branch, never from the merge request's own.
|
|
331
|
+
const onPlanNotes = gitlabPlanNotesRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
316
332
|
const onGreen = loop.applyBeforeMerge ? gitlabApplyPrRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH") : GITLAB_GREEN_TAG_RULE;
|
|
317
333
|
// A member's jobs start in its directory, after the image's install lines.
|
|
318
334
|
const enter = rooted ? [] : [`cd ${memberShellDir(member)}`];
|
|
@@ -333,7 +349,7 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
333
349
|
const doc = {
|
|
334
350
|
// Included files merge their top-level keys, so a member's file carries
|
|
335
351
|
// the rules every member shares and no name: two names would collide.
|
|
336
|
-
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, ...(greens.length > 0 ? [{ if: onGreen }] : []), ...(prEnv ? [{ if: GITLAB_SCHEDULE_RULE }] : [])] },
|
|
352
|
+
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, { if: onPlanNotes }, ...(greens.length > 0 ? [{ if: onGreen }] : []), ...(prEnv ? [{ if: GITLAB_SCHEDULE_RULE }] : [])] },
|
|
337
353
|
stages,
|
|
338
354
|
variables: { ...options.variables, CHANT_ENV: env, ...GITLAB_FULL_HISTORY },
|
|
339
355
|
[planJob]: {
|
|
@@ -347,14 +363,12 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
347
363
|
script: [...(identity ? gitlabTradeLines(FORGE_TOKEN_VARS) : [GITLAB_DROP_FORGE_TOKENS]), ...script(plan)],
|
|
348
364
|
artifacts: { when: "always", paths: [reportPath] },
|
|
349
365
|
},
|
|
350
|
-
// #4295: a
|
|
366
|
+
// #4295, #4328: a job of the target branch's pipeline posts each open merge request's note and status from its plan job's report.
|
|
351
367
|
[noteJob]: {
|
|
352
368
|
stage: PR_PLAN_NOTE_JOB,
|
|
353
369
|
image,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
variables: post.env,
|
|
357
|
-
script: ['git checkout --quiet --detach "$BASE_SHA"', ...enter, ...beforeScript, post.command],
|
|
370
|
+
rules: [{ if: onPlanNotes }],
|
|
371
|
+
script: [...enter, ...beforeScript, post.command],
|
|
358
372
|
},
|
|
359
373
|
...Object.fromEntries(applies.map(({ job, name, stage }, i) => [
|
|
360
374
|
name,
|
|
@@ -387,7 +401,7 @@ function prLoopGitlabPipeline(components, env, image, beforeScript, extraScript,
|
|
|
387
401
|
};
|
|
388
402
|
const jobs = [
|
|
389
403
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
390
|
-
{ jobName: noteJob, component: "merge request plan note", stage: PR_PLAN_NOTE_JOB, needs: [
|
|
404
|
+
{ jobName: noteJob, component: "merge request plan note", stage: PR_PLAN_NOTE_JOB, needs: [] },
|
|
391
405
|
...applies.map(({ job, name, stage }, i) => ({
|
|
392
406
|
jobName: name,
|
|
393
407
|
component: job.wave ? `green apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : "green apply",
|
|
@@ -465,7 +479,8 @@ function prEnvGitlabJobs(image, beforeScript, extraScript, enter, loop, member,
|
|
|
465
479
|
[named("env-sweep")]: {
|
|
466
480
|
stage: PR_ENV_STAGE,
|
|
467
481
|
image,
|
|
468
|
-
|
|
482
|
+
// #4328: not in the plan notes' schedule, which runs far more often than a sweep needs.
|
|
483
|
+
rules: [{ if: `${GITLAB_SCHEDULE_RULE} && $${GITLAB_PLAN_NOTES_VAR} != "1"` }],
|
|
469
484
|
resource_group: `chant-pr-env-sweep${member ? `-${member.name}` : ""}`,
|
|
470
485
|
...trading,
|
|
471
486
|
variables: { ...sweep.env, ...tradeVars },
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "dc30895365f349bcbfaeb0990c8b0638219aba7d367239c2b505f788643e8ffc",
|
|
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": "6a7c912a036e6265bb02e2fe786dcbd81fade613ec38bf3c342b9886df92d122"
|
|
59
59
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAsB7F,eAAO,MAAM,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAsB7F,eAAO,MAAM,YAAY,EAAE,aA6hB1B,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -305,7 +305,8 @@ export const test = new Job({
|
|
|
305
305
|
async transform(content, opts) {
|
|
306
306
|
const { transform } = await import("./migrate/from-github/index.js");
|
|
307
307
|
const result = await transform(content, {
|
|
308
|
-
|
|
308
|
+
// "sql" is for sql targets (#4399); chant migrate refuses it for this pair.
|
|
309
|
+
emit: opts.emit === "sql" ? undefined : opts.emit,
|
|
309
310
|
useComposites: opts.useComposites,
|
|
310
311
|
sourceFile: opts.sourceFile,
|
|
311
312
|
strict: opts.strict,
|
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.49",
|
|
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.49",
|
|
71
|
+
"@intentius/chant-lexicon-github": "^0.124.49"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@intentius/chant-lexicon-github": {
|
|
@@ -3,6 +3,7 @@ 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_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
7
|
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
7
8
|
|
|
8
9
|
stages:
|
|
@@ -37,18 +38,10 @@ plan:
|
|
|
37
38
|
plan-note:
|
|
38
39
|
stage: plan-note
|
|
39
40
|
image: node:22
|
|
40
|
-
needs:
|
|
41
|
-
- plan
|
|
42
41
|
rules:
|
|
43
|
-
- if: '$CI_PIPELINE_SOURCE == "
|
|
44
|
-
when: always
|
|
45
|
-
variables:
|
|
46
|
-
PR_NUMBER: '$CI_MERGE_REQUEST_IID'
|
|
47
|
-
HEAD_SHA: '$CI_COMMIT_SHA'
|
|
48
|
-
BASE_SHA: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
|
|
42
|
+
- if: '$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
49
43
|
script:
|
|
50
|
-
-
|
|
51
|
-
- chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge gitlab
|
|
44
|
+
- chant components pr-post --open --env prod --output .chant/pr --forge gitlab
|
|
52
45
|
|
|
53
46
|
record:
|
|
54
47
|
stage: record
|
|
@@ -3,6 +3,7 @@ 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_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
7
|
|
|
7
8
|
stages:
|
|
8
9
|
- plan
|
|
@@ -33,18 +34,10 @@ plan:
|
|
|
33
34
|
plan-note:
|
|
34
35
|
stage: plan-note
|
|
35
36
|
image: node:22
|
|
36
|
-
needs:
|
|
37
|
-
- plan
|
|
38
37
|
rules:
|
|
39
|
-
- if: '$CI_PIPELINE_SOURCE == "
|
|
40
|
-
when: always
|
|
41
|
-
variables:
|
|
42
|
-
PR_NUMBER: '$CI_MERGE_REQUEST_IID'
|
|
43
|
-
HEAD_SHA: '$CI_COMMIT_SHA'
|
|
44
|
-
BASE_SHA: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
|
|
38
|
+
- if: '$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
45
39
|
script:
|
|
46
|
-
-
|
|
47
|
-
- chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge gitlab
|
|
40
|
+
- chant components pr-post --open --env prod --output .chant/pr --forge gitlab
|
|
48
41
|
|
|
49
42
|
record:
|
|
50
43
|
stage: record
|
|
@@ -3,6 +3,7 @@ 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_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
7
|
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
7
8
|
|
|
8
9
|
stages:
|
|
@@ -37,18 +38,10 @@ plan:
|
|
|
37
38
|
plan-note:
|
|
38
39
|
stage: plan-note
|
|
39
40
|
image: node:22
|
|
40
|
-
needs:
|
|
41
|
-
- plan
|
|
42
41
|
rules:
|
|
43
|
-
- if: '$CI_PIPELINE_SOURCE == "
|
|
44
|
-
when: always
|
|
45
|
-
variables:
|
|
46
|
-
PR_NUMBER: '$CI_MERGE_REQUEST_IID'
|
|
47
|
-
HEAD_SHA: '$CI_COMMIT_SHA'
|
|
48
|
-
BASE_SHA: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
|
|
42
|
+
- if: '$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
49
43
|
script:
|
|
50
|
-
-
|
|
51
|
-
- chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge gitlab
|
|
44
|
+
- chant components pr-post --open --env prod --output .chant/pr --forge gitlab
|
|
52
45
|
|
|
53
46
|
record:
|
|
54
47
|
stage: record
|
|
@@ -3,6 +3,7 @@ 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_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
6
7
|
- if: '$CI_COMMIT_TAG =~ /^ci\/green\//'
|
|
7
8
|
|
|
8
9
|
stages:
|
|
@@ -35,18 +36,10 @@ plan:
|
|
|
35
36
|
plan-note:
|
|
36
37
|
stage: plan-note
|
|
37
38
|
image: node:22
|
|
38
|
-
needs:
|
|
39
|
-
- plan
|
|
40
39
|
rules:
|
|
41
|
-
- if: '$CI_PIPELINE_SOURCE == "
|
|
42
|
-
when: always
|
|
43
|
-
variables:
|
|
44
|
-
PR_NUMBER: '$CI_MERGE_REQUEST_IID'
|
|
45
|
-
HEAD_SHA: '$CI_COMMIT_SHA'
|
|
46
|
-
BASE_SHA: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
|
|
40
|
+
- if: '$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
47
41
|
script:
|
|
48
|
-
-
|
|
49
|
-
- chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge gitlab
|
|
42
|
+
- chant components pr-post --open --env prod --output .chant/pr --forge gitlab
|
|
50
43
|
|
|
51
44
|
record:
|
|
52
45
|
stage: record
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# chant Op waves "migrations": one stage per wave, each needing the one before.
|
|
2
2
|
# A wave that waits at its gate exits 3 and stops the waves after it; approve the
|
|
3
|
-
# digest it prints, then retry the job.
|
|
3
|
+
# digest it prints, then retry the job. A job whose runs failed exits 1, an allowed
|
|
4
|
+
# failure, and the waves after it run what does not depend on those runs.
|
|
4
5
|
|
|
5
6
|
stages:
|
|
6
7
|
- wave-1-dev
|
|
@@ -19,6 +20,14 @@ wave-1-dev:
|
|
|
19
20
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
|
|
20
21
|
script:
|
|
21
22
|
- chant run wave --spec waves.json --wave 1
|
|
23
|
+
allow_failure:
|
|
24
|
+
exit_codes:
|
|
25
|
+
- 1
|
|
26
|
+
artifacts:
|
|
27
|
+
when: always
|
|
28
|
+
paths:
|
|
29
|
+
- .chant/op-waves/migrations/outcomes/
|
|
30
|
+
expire_in: '30 days'
|
|
22
31
|
|
|
23
32
|
wave-2-prod-decide:
|
|
24
33
|
stage: wave-2-prod
|
|
@@ -37,10 +46,14 @@ wave-2-prod-decide:
|
|
|
37
46
|
- wave-1-dev
|
|
38
47
|
script:
|
|
39
48
|
- chant run wave --spec waves.json --wave 2 --decide
|
|
49
|
+
allow_failure:
|
|
50
|
+
exit_codes:
|
|
51
|
+
- 1
|
|
40
52
|
artifacts:
|
|
41
53
|
when: always
|
|
42
54
|
paths:
|
|
43
55
|
- .chant/op-waves/migrations/wave-2.json
|
|
56
|
+
- .chant/op-waves/migrations/outcomes/
|
|
44
57
|
expire_in: '30 days'
|
|
45
58
|
|
|
46
59
|
wave-2-prod-us:
|
|
@@ -64,6 +77,14 @@ wave-2-prod-us:
|
|
|
64
77
|
script:
|
|
65
78
|
- echo us
|
|
66
79
|
- chant run wave --spec waves.json --wave 2 --group us
|
|
80
|
+
allow_failure:
|
|
81
|
+
exit_codes:
|
|
82
|
+
- 1
|
|
83
|
+
artifacts:
|
|
84
|
+
when: always
|
|
85
|
+
paths:
|
|
86
|
+
- .chant/op-waves/migrations/outcomes/
|
|
87
|
+
expire_in: '30 days'
|
|
67
88
|
|
|
68
89
|
wave-2-prod-eu:
|
|
69
90
|
stage: wave-2-prod
|
|
@@ -84,3 +105,11 @@ wave-2-prod-eu:
|
|
|
84
105
|
- wave-2-prod-decide
|
|
85
106
|
script:
|
|
86
107
|
- chant run wave --spec waves.json --wave 2 --group eu
|
|
108
|
+
allow_failure:
|
|
109
|
+
exit_codes:
|
|
110
|
+
- 1
|
|
111
|
+
artifacts:
|
|
112
|
+
when: always
|
|
113
|
+
paths:
|
|
114
|
+
- .chant/op-waves/migrations/outcomes/
|
|
115
|
+
expire_in: '30 days'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reviewAgent` on GitLab (#4326): refused by name. A merge request pipeline
|
|
3
|
+
* hands every job the project's unprotected variables, so a review job there
|
|
4
|
+
* cannot be kept apart from a token that writes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { generateGitlabOpPipeline } from "./generate-op-pipeline";
|
|
9
|
+
|
|
10
|
+
describe("generateGitlabOpPipeline: reviewAgent", () => {
|
|
11
|
+
test("is refused, naming the Op and the way out", () => {
|
|
12
|
+
expect(() => generateGitlabOpPipeline([{ name: "pr-review", trigger: { kind: "pull_request" }, reviewAgent: true }])).toThrow(
|
|
13
|
+
/Scheduled Op "pr-review" sets reviewAgent.*unprotected variables.*findingMode "comment".*GitHub or Forgejo/s,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("an Op without it generates as before", () => {
|
|
18
|
+
const out = generateGitlabOpPipeline([{ name: "pr-review", trigger: { kind: "pull_request" }, findingMode: "comment" }]);
|
|
19
|
+
expect(out.jobs.map((j) => j.jobName)).toEqual(["pr-review"]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -202,6 +202,24 @@ function assertTriggerSupportsMode(name: string, mode: OpFindingMode, trigger: O
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Refuse `reviewAgent` (#4326). The split posts the review from a second job
|
|
207
|
+
* that holds the forge write token, and a GitLab merge request pipeline hands
|
|
208
|
+
* every job the project's unprotected CI/CD variables, the review job's
|
|
209
|
+
* included. A token protected enough to stay out of it is not available to a
|
|
210
|
+
* job of that pipeline either, so the post has to start from a pipeline of
|
|
211
|
+
* the default branch, which this generator does not build. The refusal names
|
|
212
|
+
* the Op and the way out rather than emitting a review job that holds the token.
|
|
213
|
+
*/
|
|
214
|
+
function assertNoReviewSplit(spec: ScheduledOpSpec): void {
|
|
215
|
+
if (!spec.reviewAgent) return;
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Scheduled Op "${spec.name}" sets reviewAgent, which keeps the model's job free of the forge write token and posts the note from a second job. ` +
|
|
218
|
+
`A GitLab merge request pipeline hands every job the project's unprotected variables, so the gitlab generator does not build that split. ` +
|
|
219
|
+
`Drop reviewAgent and run the review with findingMode "comment" (the model command still runs with a cleared environment), or generate this Op for GitHub or Forgejo.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
205
223
|
/**
|
|
206
224
|
* The follow-up line an Op with an `environment` adds under its own header
|
|
207
225
|
* line (#2257). The `environment:` key on the job binds it to the
|
|
@@ -293,6 +311,7 @@ export function generateGitlabOpPipeline(
|
|
|
293
311
|
const findingMode = spec.findingMode ?? "report";
|
|
294
312
|
const trigger = resolveOpTrigger(spec);
|
|
295
313
|
assertTriggerSupportsMode(spec.name, findingMode, trigger);
|
|
314
|
+
assertNoReviewSplit(spec);
|
|
296
315
|
if (trigger.kind === "cron") anyCron = true;
|
|
297
316
|
|
|
298
317
|
const jobName = jobPrefix + ciJobName(spec.name);
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One file, one stage per wave, on a push to the spec's branches. Each job is
|
|
5
5
|
* one of core's `opWaveJobs`, and `needs:` chains them so a wave that waits at
|
|
6
|
-
* its gate (exit 3)
|
|
7
|
-
*
|
|
6
|
+
* its gate (exit 3) stops every later wave; retrying the failed job after
|
|
7
|
+
* `chant approve` carries on, and the jobs it skipped run after it.
|
|
8
|
+
*
|
|
9
|
+
* A job that exits 1 (a run failed, or was blocked) is an allowed failure
|
|
10
|
+
* (`allow_failure: exit_codes: [1]`, #4322), so the next wave's jobs run
|
|
11
|
+
* after it and block only the runs that depend on a failed one. Every job
|
|
12
|
+
* keeps the outcome directory as an artifact, holding the outcome files it
|
|
13
|
+
* received through `needs:` and its own, so each later job reads every
|
|
14
|
+
* earlier one. Any other non-zero exit still stops the waves after it.
|
|
15
|
+
* `GIT_DEPTH: "0"` gives the runner the base
|
|
8
16
|
* commit it reads the gate policy at. A wide wave's deciding job keeps its
|
|
9
17
|
* decision as an artifact, which GitLab hands each share job through `needs:`.
|
|
10
18
|
* A wave's `environment` is the job's own `environment:`; protect it in the
|
|
@@ -47,7 +55,7 @@ import {
|
|
|
47
55
|
nonEmpty,
|
|
48
56
|
pushBranches,
|
|
49
57
|
} from "@intentius/chant/ci-jobs";
|
|
50
|
-
import { opWaveGroupOf, opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
58
|
+
import { opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
51
59
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
52
60
|
import { gitlabTradeIdTokens, gitlabTradeLines, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
|
|
53
61
|
import { emitGitlabFile } from "./ci-jobs";
|
|
@@ -119,7 +127,9 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
|
|
|
119
127
|
rules,
|
|
120
128
|
...(job.needs.length > 0 ? { needs: job.needs } : {}),
|
|
121
129
|
script: [...(cloud ? gitlabTradeLines() : []), ...setup, ...(options.beforeScript ?? []), job.command.join(" "), ...(options.extraScript ?? [])],
|
|
122
|
-
|
|
130
|
+
// #4322: a run that failed, or was blocked, lets the next wave run; any other failure stops it.
|
|
131
|
+
allow_failure: { exit_codes: [1] },
|
|
132
|
+
artifacts: { when: "always", paths: [...(job.kind === "decide" ? [job.record!] : []), `${opWaveOutcomeDir(spec)}/`], expire_in: "30 days" },
|
|
123
133
|
},
|
|
124
134
|
]);
|
|
125
135
|
}
|
|
@@ -138,7 +148,8 @@ export function generateGitlabOpWavesPipeline(spec: OpWavesSpec, options: OpWave
|
|
|
138
148
|
const header = [
|
|
139
149
|
`# chant Op waves "${spec.name}": one stage per wave, each needing the one before.`,
|
|
140
150
|
"# A wave that waits at its gate exits 3 and stops the waves after it; approve the",
|
|
141
|
-
"# digest it prints, then retry the job.",
|
|
151
|
+
"# digest it prints, then retry the job. A job whose runs failed exits 1, an allowed",
|
|
152
|
+
"# failure, and the waves after it run what does not depend on those runs.",
|
|
142
153
|
...(spec.resume
|
|
143
154
|
? [`# The ${spec.name}-resume job retries it for you in a pipeline schedule; give the schedule the cron "${spec.resume.schedule}".`]
|
|
144
155
|
: []),
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
prApplyWaveJobName,
|
|
54
54
|
prLoopJobs,
|
|
55
55
|
GITLAB_APPLY_PR_VAR,
|
|
56
|
+
GITLAB_PLAN_NOTES_VAR,
|
|
57
|
+
gitlabPlanJobName,
|
|
56
58
|
prLoopPostJob,
|
|
57
59
|
prLoopSetup,
|
|
58
60
|
PR_PLAN_NOTE_JOB,
|
|
@@ -313,12 +315,16 @@ function gatedWaveGitlabPipeline(
|
|
|
313
315
|
* The plan job holds no forge token while the merge request's code runs
|
|
314
316
|
* (#4295, `@intentius/chant/ci-handover`): its script starts by unsetting
|
|
315
317
|
* the token variables, and `pr-plan --no-status` writes the note and the
|
|
316
|
-
* status into its report
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* `
|
|
321
|
-
*
|
|
318
|
+
* status into its report, which it keeps as its artifact. GitLab builds a
|
|
319
|
+
* merge request pipeline from the merge request's own files, so no job in it
|
|
320
|
+
* may hold the token (#4328): `CHANT_FORGE_TOKEN` and `CHANT_RUNNER_KEY` are
|
|
321
|
+
* protected variables, which a merge request pipeline never gets. The
|
|
322
|
+
* `plan-note` job runs in a pipeline of the target branch, built from that
|
|
323
|
+
* branch's files, that a pipeline schedule (or a person) starts with
|
|
324
|
+
* `CHANT_PLAN_NOTES` set to `1`. It runs `chant components pr-post --open`,
|
|
325
|
+
* which asks GitLab for each open merge request's plan job, reads the report
|
|
326
|
+
* from its artifacts and posts the note and the status for each job that
|
|
327
|
+
* settled since the last run.
|
|
322
328
|
*
|
|
323
329
|
* With `identity` (#4296, `@intentius/chant/ci-identity`) the plan, record
|
|
324
330
|
* and apply jobs take an `id_tokens:` entry per cloud, and each script
|
|
@@ -342,6 +348,16 @@ function gitlabApplyPrRule(branch: string): string {
|
|
|
342
348
|
return `$CI_PIPELINE_SOURCE =~ /^(web|api|trigger)$/ && $${GITLAB_APPLY_PR_VAR} =~ /^[0-9]+$/ && $CI_COMMIT_BRANCH == ${branch}`;
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The rule of the plan-note job (#4328): a pipeline of `branch` that a
|
|
353
|
+
* pipeline schedule, a person or a trigger started with `CHANT_PLAN_NOTES`
|
|
354
|
+
* set to `1`. A merge request pipeline's own trigger job starts a pipeline
|
|
355
|
+
* whose source is `pipeline`, which this rule leaves out.
|
|
356
|
+
*/
|
|
357
|
+
function gitlabPlanNotesRule(branch: string): string {
|
|
358
|
+
return `$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $${GITLAB_PLAN_NOTES_VAR} == "1" && $CI_COMMIT_BRANCH == ${branch}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
345
361
|
/** The rule of a pipeline for a pushed `ci/green/` tag (#4168 step 9), which runs the apply under apply on merge. */
|
|
346
362
|
const GITLAB_GREEN_TAG_RULE = '$CI_COMMIT_TAG =~ /^ci\\/green\\//';
|
|
347
363
|
|
|
@@ -358,7 +374,8 @@ function prLoopGitlabPipeline(
|
|
|
358
374
|
const rooted = !member || member.dir === "." || member.dir === "";
|
|
359
375
|
const [plan, apply, record] = prLoopJobs("gitlab", env, loop, member?.name);
|
|
360
376
|
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
361
|
-
|
|
377
|
+
// #4328: the plan-note job's pr-post --open finds the plan job by this name.
|
|
378
|
+
const planJob = gitlabPlanJobName(member?.name);
|
|
362
379
|
const named = (job: string) => (member ? `${member.name}-${job}` : job);
|
|
363
380
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
364
381
|
// #4168 step 9: a push to the branch records, and under apply on merge a ci/green/ tag pipeline applies.
|
|
@@ -372,6 +389,8 @@ function prLoopGitlabPipeline(
|
|
|
372
389
|
if (loop.branch !== undefined) assertGitlabBranch("The merge-request pipeline", loop.branch);
|
|
373
390
|
const onMerge = gitlabPushRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
374
391
|
// #4301: the apply before merge runs in a pipeline of the target branch a person starts with CHANT_APPLY_PR set.
|
|
392
|
+
// #4328: the plan notes are posted from a pipeline of the target branch, never from the merge request's own.
|
|
393
|
+
const onPlanNotes = gitlabPlanNotesRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH");
|
|
375
394
|
const onGreen = loop.applyBeforeMerge ? gitlabApplyPrRule(loop.branch ? JSON.stringify(loop.branch) : "$CI_DEFAULT_BRANCH") : GITLAB_GREEN_TAG_RULE;
|
|
376
395
|
// A member's jobs start in its directory, after the image's install lines.
|
|
377
396
|
const enter = rooted ? [] : [`cd ${memberShellDir(member!)}`];
|
|
@@ -393,7 +412,7 @@ function prLoopGitlabPipeline(
|
|
|
393
412
|
const doc: Record<string, unknown> = {
|
|
394
413
|
// Included files merge their top-level keys, so a member's file carries
|
|
395
414
|
// the rules every member shares and no name: two names would collide.
|
|
396
|
-
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, ...(greens.length > 0 ? [{ if: onGreen }] : []), ...(prEnv ? [{ if: GITLAB_SCHEDULE_RULE }] : [])] },
|
|
415
|
+
workflow: { ...(member ? {} : { name: `chant-pr-${env}` }), rules: [{ if: onMergeRequest }, { if: onMerge }, { if: onPlanNotes }, ...(greens.length > 0 ? [{ if: onGreen }] : []), ...(prEnv ? [{ if: GITLAB_SCHEDULE_RULE }] : [])] },
|
|
397
416
|
stages,
|
|
398
417
|
variables: { ...options.variables, CHANT_ENV: env, ...GITLAB_FULL_HISTORY },
|
|
399
418
|
[planJob]: {
|
|
@@ -407,14 +426,12 @@ function prLoopGitlabPipeline(
|
|
|
407
426
|
script: [...(identity ? gitlabTradeLines(FORGE_TOKEN_VARS) : [GITLAB_DROP_FORGE_TOKENS]), ...script(plan)],
|
|
408
427
|
artifacts: { when: "always", paths: [reportPath] },
|
|
409
428
|
},
|
|
410
|
-
// #4295: a
|
|
429
|
+
// #4295, #4328: a job of the target branch's pipeline posts each open merge request's note and status from its plan job's report.
|
|
411
430
|
[noteJob]: {
|
|
412
431
|
stage: PR_PLAN_NOTE_JOB,
|
|
413
432
|
image,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
variables: post.env,
|
|
417
|
-
script: ['git checkout --quiet --detach "$BASE_SHA"', ...enter, ...beforeScript, post.command],
|
|
433
|
+
rules: [{ if: onPlanNotes }],
|
|
434
|
+
script: [...enter, ...beforeScript, post.command],
|
|
418
435
|
},
|
|
419
436
|
...Object.fromEntries(
|
|
420
437
|
applies.map(({ job, name, stage }, i) => [
|
|
@@ -449,7 +466,7 @@ function prLoopGitlabPipeline(
|
|
|
449
466
|
};
|
|
450
467
|
const jobs: GeneratedJob[] = [
|
|
451
468
|
{ jobName: planJob, component: "merge request plan", stage: "plan", needs: [] },
|
|
452
|
-
{ jobName: noteJob, component: "merge request plan note", stage: PR_PLAN_NOTE_JOB, needs: [
|
|
469
|
+
{ jobName: noteJob, component: "merge request plan note", stage: PR_PLAN_NOTE_JOB, needs: [] },
|
|
453
470
|
...applies.map(({ job, name, stage }, i) => ({
|
|
454
471
|
jobName: name,
|
|
455
472
|
component: job.wave ? `green apply, wave ${job.wave}` : job.jobName === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : "green apply",
|
|
@@ -539,7 +556,8 @@ function prEnvGitlabJobs(
|
|
|
539
556
|
[named("env-sweep")]: {
|
|
540
557
|
stage: PR_ENV_STAGE,
|
|
541
558
|
image,
|
|
542
|
-
|
|
559
|
+
// #4328: not in the plan notes' schedule, which runs far more often than a sweep needs.
|
|
560
|
+
rules: [{ if: `${GITLAB_SCHEDULE_RULE} && $${GITLAB_PLAN_NOTES_VAR} != "1"` }],
|
|
543
561
|
resource_group: `chant-pr-env-sweep${member ? `-${member.name}` : ""}`,
|
|
544
562
|
...trading,
|
|
545
563
|
variables: { ...sweep.env, ...tradeVars },
|
|
@@ -55,8 +55,8 @@ describe("the GitLab merge-request pipeline with a merge request environment", (
|
|
|
55
55
|
expect(down.resource_group).toBe(up.resource_group);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("env-sweep runs from a pipeline schedule, which the workflow rules let start", () => {
|
|
59
|
-
expect(doc["env-sweep"]!.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "schedule"' }]);
|
|
58
|
+
test("env-sweep runs from a pipeline schedule other than the plan notes' (#4328), which the workflow rules let start", () => {
|
|
59
|
+
expect(doc["env-sweep"]!.rules).toEqual([{ if: '$CI_PIPELINE_SOURCE == "schedule" && $CHANT_PLAN_NOTES != "1"' }]);
|
|
60
60
|
expect(doc.workflow.rules).toContainEqual({ if: '$CI_PIPELINE_SOURCE == "schedule"' });
|
|
61
61
|
expect(doc["env-sweep"]!.script.at(-1)).toBe("chant components pr-env sweep --forge gitlab --output .chant/pr-env");
|
|
62
62
|
});
|
|
@@ -13,7 +13,7 @@ import { describe, expect, test } from "vitest";
|
|
|
13
13
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
14
14
|
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
15
15
|
import { generateGitlabPipeline } from "./generate-pipeline";
|
|
16
|
-
import { greenCheckout, PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
16
|
+
import { gitlabPlanJobName, greenCheckout, PR_LOOP_SETUP, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
17
17
|
import { GITLAB_DROP_FORGE_TOKENS } from "@intentius/chant/ci-handover";
|
|
18
18
|
|
|
19
19
|
const ESTATE: DriverComponent[] = [
|
|
@@ -129,7 +129,8 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
129
129
|
expect(mdoc.plan).toBeUndefined();
|
|
130
130
|
// Included files merge top-level keys: no name, so members do not collide on it.
|
|
131
131
|
expect(mdoc.workflow.name).toBeUndefined();
|
|
132
|
-
|
|
132
|
+
// The merge request, the push and the plan notes' pipeline (#4328), and the green tag.
|
|
133
|
+
expect(mdoc.workflow.rules).toHaveLength(4);
|
|
133
134
|
const plan = mdoc["network-plan"] as Job;
|
|
134
135
|
const apply = mdoc["network-apply"] as Job;
|
|
135
136
|
expect(plan.script).toContain("cd infra/network");
|
|
@@ -143,6 +144,11 @@ describe("the GitLab merge-request pipeline", () => {
|
|
|
143
144
|
expect(plan.artifacts).toEqual({ when: "always", paths: ["infra/network/.chant/pr"] });
|
|
144
145
|
expect(apply.artifacts).toEqual({ when: "always", paths: ["infra/network/.chant/pr"] });
|
|
145
146
|
expect(apply.cache).toEqual({ key: "chant-apply.network.prod-$CI_COMMIT_SHA", paths: ["infra/network/.chant/pr-resume/pr-apply.json"], when: "always" });
|
|
147
|
+
// #4328: the member's plan-note job finds the member's plan job by name and reads its artifact from the member's directory.
|
|
148
|
+
const note = mdoc["network-plan-note"] as Job;
|
|
149
|
+
expect(note.script.slice(0, 1)).toEqual(["cd infra/network"]);
|
|
150
|
+
expect(note.script.at(-1)).toBe("chant components pr-post --open --env prod --output .chant/pr --forge gitlab --member network");
|
|
151
|
+
expect(gitlabPlanJobName("network")).toBe("network-plan");
|
|
146
152
|
});
|
|
147
153
|
});
|
|
148
154
|
|
|
@@ -239,14 +245,16 @@ describe("the GitLab plan job holds no forge token while the merge request's cod
|
|
|
239
245
|
expect(prLoopJobs("gitlab", "prod")[0]!.command.endsWith(" --no-status")).toBe(true);
|
|
240
246
|
});
|
|
241
247
|
|
|
242
|
-
test("the plan-note job
|
|
248
|
+
test("the plan-note job runs in a pipeline of the target branch, never the merge request's, and posts every open merge request's plan (#4328)", () => {
|
|
243
249
|
const note = doc["plan-note"]!;
|
|
244
250
|
expect(note.stage).toBe("plan-note");
|
|
245
|
-
expect(note.needs).
|
|
246
|
-
|
|
247
|
-
expect(note.
|
|
248
|
-
expect(note.
|
|
249
|
-
expect(
|
|
251
|
+
expect(note.needs).toBeUndefined();
|
|
252
|
+
const rule = '$CI_PIPELINE_SOURCE =~ /^(schedule|web|api|trigger)$/ && $CHANT_PLAN_NOTES == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH';
|
|
253
|
+
expect(note.rules).toEqual([{ if: rule }]);
|
|
254
|
+
expect(note.rules.some((r) => r.if.includes("merge_request_event"))).toBe(false);
|
|
255
|
+
expect((doc.workflow as unknown as { rules: Array<{ if: string }> }).rules).toContainEqual({ if: rule });
|
|
256
|
+
expect(note.variables).toBeUndefined();
|
|
257
|
+
expect(note.script.at(-1)).toBe("chant components pr-post --open --env prod --output .chant/pr --forge gitlab");
|
|
250
258
|
// It runs no build, lint or plan: none of the merge request's code.
|
|
251
259
|
expect(note.script.join("\n")).not.toMatch(/chant (build|lint|ci run)/);
|
|
252
260
|
expect(result.stages).toEqual(["plan", "plan-note", "record"]);
|
|
@@ -65,7 +65,7 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
65
65
|
expect(doc.stages).toEqual(["plan", "plan-note", "record", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
66
66
|
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
67
67
|
["plan", []],
|
|
68
|
-
["plan-note", [
|
|
68
|
+
["plan-note", []],
|
|
69
69
|
["record", []],
|
|
70
70
|
["apply-wave-1", []],
|
|
71
71
|
["apply-wave-2", ["apply-wave-1"]],
|
|
@@ -90,7 +90,7 @@ describe("the GitLab merge-request pipeline with a gate per wave (#4003)", () =>
|
|
|
90
90
|
const scoped = generateGitlabPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" }, member });
|
|
91
91
|
expect(scoped.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
92
92
|
["network-plan", []],
|
|
93
|
-
["network-plan-note", [
|
|
93
|
+
["network-plan-note", []],
|
|
94
94
|
["network-record", []],
|
|
95
95
|
["network-apply-wave-1", []],
|
|
96
96
|
["network-apply-wave-2", ["network-apply-wave-1"]],
|
package/src/plugin.ts
CHANGED
|
@@ -337,7 +337,8 @@ export const test = new Job({
|
|
|
337
337
|
async transform(content: string, opts) {
|
|
338
338
|
const { transform } = await import("./migrate/from-github/index");
|
|
339
339
|
const result = await transform(content, {
|
|
340
|
-
|
|
340
|
+
// "sql" is for sql targets (#4399); chant migrate refuses it for this pair.
|
|
341
|
+
emit: opts.emit === "sql" ? undefined : opts.emit,
|
|
341
342
|
useComposites: opts.useComposites,
|
|
342
343
|
sourceFile: opts.sourceFile,
|
|
343
344
|
strict: opts.strict,
|