@intentius/chant-lexicon-github 0.124.55 → 0.124.57
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 +2 -2
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +73 -8
- package/dist/components/generate-op-waves-pipeline.d.ts +16 -3
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +82 -17
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +34 -6
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__golden__/op-waves-jobs.yml +31 -4
- package/src/components/generate-op-pipeline.cloud.test.ts +96 -0
- package/src/components/generate-op-pipeline.ts +89 -15
- package/src/components/generate-op-waves-pipeline.approval.test.ts +46 -0
- package/src/components/generate-op-waves-pipeline.guard.test.ts +57 -0
- package/src/components/generate-op-waves-pipeline.identity.test.ts +8 -8
- package/src/components/generate-op-waves-pipeline.test.ts +19 -3
- package/src/components/generate-op-waves-pipeline.ts +85 -15
- package/src/components/generate-pipeline.ts +38 -6
- package/src/components/generate-pr-pipeline.comment-targets.test.ts +24 -0
- package/src/components/generate-pr-pipeline.locks.test.ts +10 -0
- package/src/components/generate-pr-pipeline.respond.test.ts +47 -0
- package/src/components/generate-pr-pipeline.runners.test.ts +61 -0
- package/src/components/generate-pr-pipeline.stale-notes.test.ts +42 -0
|
@@ -104,8 +104,8 @@ export interface GithubOpPipelineDoc {
|
|
|
104
104
|
on: Record<string, unknown>;
|
|
105
105
|
/** The `env:` mapping, when `options.variables` is set. */
|
|
106
106
|
env?: Record<string, unknown>;
|
|
107
|
-
/** The `concurrency:` mapping — one run at a time per Op. */
|
|
108
|
-
concurrency
|
|
107
|
+
/** The `concurrency:` mapping — one run at a time per Op. Absent on a waves file (ruling for ci-products 2026-10-28). */
|
|
108
|
+
concurrency?: Record<string, unknown>;
|
|
109
109
|
/**
|
|
110
110
|
* The `permissions:` mapping for this Op's finding-mode. GitHub-only:
|
|
111
111
|
* Forgejo Actions ignores `permissions:` entirely, so the forgejo dialect
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAGH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAOhB,gBAAgB,EAIjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAG1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAGH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAOhB,gBAAgB,EAIjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAG1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAUlC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAIhE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,yHAAyH;IACzH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AAuOD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,EACrC,KAAK,GAAE,QAAQ,GAAG,SAAoB,GACrC;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CAuM1D;AAwCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAMnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
|
|
@@ -77,6 +77,9 @@ import { assertReviewSplit, reviewJobs, reviewNoteJobName } from "./review-jobs.
|
|
|
77
77
|
import { resolveAgentSpec } from "@intentius/chant/ci-agent";
|
|
78
78
|
import { activeProductWords } from "@intentius/chant/ci-product-words";
|
|
79
79
|
import { agentJobName, agentPushJobName, driftRunOutputs, driftRunStep, githubAgentJobs } from "./agent-jobs.js";
|
|
80
|
+
import { assertScheduledOpCloud, scheduledOpUpdateJobName, withoutTokenRequestLine } from "@intentius/chant/scheduled-op-cloud";
|
|
81
|
+
import { TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
|
|
82
|
+
import { CHANT_EXIT } from "@intentius/chant/exit-codes";
|
|
80
83
|
// The setup, environment and permission checks are shared with the Op waves
|
|
81
84
|
// generator and the forgejo lexicon (#3765); they stay importable from here.
|
|
82
85
|
export { assertEnvironment, assertSetupSteps, mergePermissions };
|
|
@@ -313,6 +316,8 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
313
316
|
const jobs = [];
|
|
314
317
|
const words = activeProductWords();
|
|
315
318
|
for (const spec of ops) {
|
|
319
|
+
// #4631: the job's cloud identities and the update on drift, checked before anything renders.
|
|
320
|
+
assertScheduledOpCloud(spec);
|
|
316
321
|
// #4616: the comment agent is a workflow of its own, on pull request comments; the spec's trigger is not read.
|
|
317
322
|
if (spec.agent?.on === "comment") {
|
|
318
323
|
const settings = resolveAgentSpec(spec.name, spec.agent, words.product);
|
|
@@ -364,6 +369,8 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
364
369
|
throw new Error(`Scheduled Op "${spec.name}" sets a drift agent, which follows a drift check's run; give it a cron or pull_request trigger and no reviewAgent.`);
|
|
365
370
|
}
|
|
366
371
|
const stepEnv = split ? {} : jobTokenEnv(findingMode !== "report");
|
|
372
|
+
const cloud = spec.cloud;
|
|
373
|
+
const update = spec.update;
|
|
367
374
|
const setup = spec.setup ?? [];
|
|
368
375
|
assertSetupSteps(spec.name, setup);
|
|
369
376
|
if (spec.environment)
|
|
@@ -376,13 +383,17 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
376
383
|
const invocation = gated
|
|
377
384
|
? [...runParts, ...GATED_EXIT_FLAG, "--json"].join(" ")
|
|
378
385
|
: runParts.join(" ");
|
|
379
|
-
const
|
|
380
|
-
|
|
386
|
+
const later = [...setup.map(setupStepDoc), ...runSteps(beforeScript)];
|
|
387
|
+
later.push(gated
|
|
381
388
|
? gatedRunStep(RUN_STEP_ID, spec.name, invocation, stepEnv)
|
|
382
389
|
: driftAgent
|
|
383
390
|
? driftRunStep(invocation, stepEnv, forge)
|
|
384
|
-
:
|
|
385
|
-
|
|
391
|
+
: update
|
|
392
|
+
? driftFlagStep(invocation, stepEnv)
|
|
393
|
+
: { run: invocation, env: stepEnv });
|
|
394
|
+
later.push(...runSteps(extraScript));
|
|
395
|
+
// #4631: the trade before the checkout, and every step after it without the token request variables.
|
|
396
|
+
const steps = cloud ? [tradeStep(cloud), checkoutStep(), ...later.map(withoutTokenRequest)] : [checkoutStep(), ...later];
|
|
386
397
|
const review = split
|
|
387
398
|
? reviewJobs({
|
|
388
399
|
name: spec.name,
|
|
@@ -396,6 +407,7 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
396
407
|
...(options.member ? { member: options.member } : {}),
|
|
397
408
|
})
|
|
398
409
|
: undefined;
|
|
410
|
+
const workflowPermissions = mergePermissions(spec.name, permissionsFor(findingMode, trigger), spec.permissions ?? {}, trigger);
|
|
399
411
|
const doc = {
|
|
400
412
|
name: spec.name,
|
|
401
413
|
on: onTrigger(trigger),
|
|
@@ -403,7 +415,7 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
403
415
|
// One run at a time per Op — a slow audit must not overlap its own next
|
|
404
416
|
// scheduled trigger. A push Op's `build` (#4169) widens the group.
|
|
405
417
|
concurrency: concurrency(opConcurrencyGroup(jobName, trigger)),
|
|
406
|
-
permissions:
|
|
418
|
+
permissions: workflowPermissions,
|
|
407
419
|
jobsDoc: {
|
|
408
420
|
[jobName]: {
|
|
409
421
|
"runs-on": githubRunsOn(spec.runner),
|
|
@@ -418,6 +430,8 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
418
430
|
// behind the same reviewer would hold the message back until
|
|
419
431
|
// somebody had already acted.
|
|
420
432
|
...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}),
|
|
433
|
+
// #4631: the trade needs id-token: write, on this job alone; the finding-mode's scopes stay as they are.
|
|
434
|
+
...(cloud ? { permissions: { ...workflowPermissions, "id-token": "write" } } : {}),
|
|
421
435
|
...(gated
|
|
422
436
|
? {
|
|
423
437
|
outputs: Object.fromEntries(["gated", "op", "gate", "approve"].map((name) => [
|
|
@@ -427,9 +441,21 @@ export function buildGithubOpPipelineDocs(ops, options = {}, forge = "github") {
|
|
|
427
441
|
}
|
|
428
442
|
: driftAgent
|
|
429
443
|
? { outputs: driftRunOutputs(forge) }
|
|
430
|
-
:
|
|
444
|
+
: update
|
|
445
|
+
? { outputs: { drift: `\${{ steps.${DRIFT_FLAG_STEP_ID}.outputs.drift }}` } }
|
|
446
|
+
: {}),
|
|
431
447
|
steps,
|
|
432
448
|
},
|
|
449
|
+
...(update
|
|
450
|
+
? {
|
|
451
|
+
[scheduledOpUpdateJobName(jobName)]: updateJob(jobName, update, {
|
|
452
|
+
image,
|
|
453
|
+
...(spec.runner !== undefined ? { runner: spec.runner } : {}),
|
|
454
|
+
setup: setup.map(setupStepDoc),
|
|
455
|
+
beforeScript,
|
|
456
|
+
}),
|
|
457
|
+
}
|
|
458
|
+
: {}),
|
|
433
459
|
},
|
|
434
460
|
...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image, spec.runner) } } : {}),
|
|
435
461
|
};
|
|
@@ -487,14 +513,14 @@ function scopeOpFileToMember(file, member) {
|
|
|
487
513
|
const { steps, ...rest } = props;
|
|
488
514
|
return [job, { ...rest, defaults: { run: { "working-directory": member.dir } }, steps }];
|
|
489
515
|
}));
|
|
490
|
-
const group = file.doc.concurrency
|
|
516
|
+
const group = file.doc.concurrency?.group;
|
|
491
517
|
return {
|
|
492
518
|
name,
|
|
493
519
|
doc: {
|
|
494
520
|
...file.doc,
|
|
495
521
|
...(file.doc.name ? { name: `${member.name}/${file.doc.name}` } : {}),
|
|
496
522
|
on,
|
|
497
|
-
concurrency: { ...file.doc.concurrency, ...(typeof group === "string" ? { group: `${member.name}-${group}` } : {}) },
|
|
523
|
+
...(file.doc.concurrency ? { concurrency: { ...file.doc.concurrency, ...(typeof group === "string" ? { group: `${member.name}-${group}` } : {}) } } : {}),
|
|
498
524
|
jobsDoc,
|
|
499
525
|
},
|
|
500
526
|
};
|
|
@@ -524,3 +550,42 @@ export function generateGithubOpPipeline(ops, options = {}) {
|
|
|
524
550
|
jobs,
|
|
525
551
|
};
|
|
526
552
|
}
|
|
553
|
+
/** The id of the run step that says whether an update's drift check found drift (#4631). */
|
|
554
|
+
const DRIFT_FLAG_STEP_ID = "drift-check";
|
|
555
|
+
/** The Op's run step when an update follows it (#4631): `drift=true` as an output on chant's drift code, and the run's own exit code. */
|
|
556
|
+
function driftFlagStep(invocation, env) {
|
|
557
|
+
return {
|
|
558
|
+
id: DRIFT_FLAG_STEP_ID,
|
|
559
|
+
env,
|
|
560
|
+
run: ["set +e", invocation, "code=$?", `if [ "$code" -eq ${CHANT_EXIT.drift} ]; then echo "drift=true" >>"$GITHUB_OUTPUT"; fi`, 'exit "$code"'].join("\n"),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/** The trade step (#4631), as an op wave job's (#4332): the job's OIDC token for `cloud`, written to GITHUB_ENV. */
|
|
564
|
+
function tradeStep(cloud) {
|
|
565
|
+
return { name: "Trade the job's OIDC token for its cloud identities", env: { [TRADE_SPEC_ENV]: JSON.stringify(cloud) }, run: tradeLine("$GITHUB_ENV", "github-env") };
|
|
566
|
+
}
|
|
567
|
+
/** A run step after the trade, with the token request variables unset first. A `uses:` step is left as it is. */
|
|
568
|
+
function withoutTokenRequest(step) {
|
|
569
|
+
return typeof step.run === "string" ? { ...step, run: withoutTokenRequestLine(step.run) } : step;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* The update job (#4631): after the Op's job, only when its run found
|
|
573
|
+
* drift; its own trade for `update.cloud` before its checkout, then the
|
|
574
|
+
* update's command. Its permissions are `contents: read`, `id-token: write`
|
|
575
|
+
* with a cloud, and the update's own scopes, which may raise `contents` to
|
|
576
|
+
* `write` so the update records on `chant/lifecycle` (2026-10-28 ruling).
|
|
577
|
+
* The Op's own job keeps its scopes.
|
|
578
|
+
*/
|
|
579
|
+
function updateJob(after, update, i) {
|
|
580
|
+
const later = [...i.setup, ...runSteps(i.beforeScript), { name: "Update what drifted", run: update.command }];
|
|
581
|
+
return {
|
|
582
|
+
needs: after,
|
|
583
|
+
if: `always() && needs.${after}.outputs.drift == 'true'`,
|
|
584
|
+
"runs-on": githubRunsOn(i.runner),
|
|
585
|
+
container: i.image,
|
|
586
|
+
permissions: { contents: "read", ...(update.cloud ? { "id-token": "write" } : {}), ...(update.permissions ?? {}) },
|
|
587
|
+
...nonEmpty("env", update.variables),
|
|
588
|
+
...(update.environment ? { environment: environmentDoc(update.environment) } : {}),
|
|
589
|
+
steps: update.cloud ? [tradeStep(update.cloud), checkoutStep(), ...later.map(withoutTokenRequest)] : [checkoutStep(), ...later],
|
|
590
|
+
};
|
|
591
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op waves → GitHub Actions workflow (#3679).
|
|
3
3
|
*
|
|
4
|
-
* One workflow for the whole spec, on a push to its branches (and by hand)
|
|
5
|
-
*
|
|
4
|
+
* One workflow for the whole spec, on a push to its branches (and by hand).
|
|
5
|
+
* Each job is one of core's `opWaveJobs`: a wave, or a wide
|
|
6
6
|
* wave's deciding job and share jobs, chained by `needs:`. Re-running the
|
|
7
7
|
* failed jobs after `chant approve` carries on from that wave.
|
|
8
8
|
*
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* `pull-requests: read`, so a wave can read the merged pull request's
|
|
35
35
|
* reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
|
|
36
36
|
* --record-plans` on each pull request to record the digests its head plans.
|
|
37
|
+
* A `pull-request` wave (#4630) gets the plans workflow too, for the approval
|
|
38
|
+
* status before merge; it asks for no `pull-requests: read` on the waves.
|
|
37
39
|
*
|
|
38
40
|
* With `notify` on the spec (#3761), the workflow maps each variable it names
|
|
39
41
|
* from the repository secret of the same name, so a wave job can post to
|
|
@@ -62,6 +64,15 @@
|
|
|
62
64
|
* step holds the control token: the `tokenSecret` secret, mapped into
|
|
63
65
|
* `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
|
|
64
66
|
* (`controlDispatchScript`, #4568).
|
|
67
|
+
*
|
|
68
|
+
* Every wave, deciding and share job starts with the apply-job guard
|
|
69
|
+
* (#4624, `@intentius/chant/ci-job-guard`), before the trade and the
|
|
70
|
+
* checkout: it stops the job unless `GITHUB_REF` is one of the spec's
|
|
71
|
+
* branches, the job runs in its forge environment (read from an OIDC
|
|
72
|
+
* token's claims, so only in a job that may request one), and, for a
|
|
73
|
+
* runner whose labels include `self-hosted`, on a self-hosted runner. The
|
|
74
|
+
* forgejo generator passes `forge: "forgejo"`, which checks the ref and
|
|
75
|
+
* runner only.
|
|
65
76
|
*/
|
|
66
77
|
import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
67
78
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
@@ -73,7 +84,9 @@ export declare function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wa
|
|
|
73
84
|
/** The artifact a job keeps its outcome file in (#4322). */
|
|
74
85
|
export declare function opWaveOutcomeArtifact(spec: Pick<OpWavesSpec, "name">, jobName: string): string;
|
|
75
86
|
/** The workflow document, before emission, and the jobs it holds. */
|
|
76
|
-
export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions
|
|
87
|
+
export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions, dialect?: {
|
|
88
|
+
forge?: "github" | "forgejo";
|
|
89
|
+
}): {
|
|
77
90
|
doc: GithubOpPipelineDoc;
|
|
78
91
|
jobs: OpWavesPipelineResult["jobs"];
|
|
79
92
|
resumeDoc?: GithubOpPipelineDoc;
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AAEH,OAAO,EAaL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAmB9F,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,yFAAyF;AACzF,eAAO,MAAM,cAAc,YAAW,CAAC;AAEvC,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1F;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9F;AAED,qEAAqE;AACrE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,EAC/B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAO,GAC7C;IAAE,GAAG,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAgGpI;AAoID,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAMvH"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op waves → GitHub Actions workflow (#3679).
|
|
3
3
|
*
|
|
4
|
-
* One workflow for the whole spec, on a push to its branches (and by hand)
|
|
5
|
-
*
|
|
4
|
+
* One workflow for the whole spec, on a push to its branches (and by hand).
|
|
5
|
+
* Each job is one of core's `opWaveJobs`: a wave, or a wide
|
|
6
6
|
* wave's deciding job and share jobs, chained by `needs:`. Re-running the
|
|
7
7
|
* failed jobs after `chant approve` carries on from that wave.
|
|
8
8
|
*
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* `pull-requests: read`, so a wave can read the merged pull request's
|
|
35
35
|
* reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
|
|
36
36
|
* --record-plans` on each pull request to record the digests its head plans.
|
|
37
|
+
* A `pull-request` wave (#4630) gets the plans workflow too, for the approval
|
|
38
|
+
* status before merge; it asks for no `pull-requests: read` on the waves.
|
|
37
39
|
*
|
|
38
40
|
* With `notify` on the spec (#3761), the workflow maps each variable it names
|
|
39
41
|
* from the repository secret of the same name, so a wave job can post to
|
|
@@ -62,11 +64,22 @@
|
|
|
62
64
|
* step holds the control token: the `tokenSecret` secret, mapped into
|
|
63
65
|
* `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
|
|
64
66
|
* (`controlDispatchScript`, #4568).
|
|
67
|
+
*
|
|
68
|
+
* Every wave, deciding and share job starts with the apply-job guard
|
|
69
|
+
* (#4624, `@intentius/chant/ci-job-guard`), before the trade and the
|
|
70
|
+
* checkout: it stops the job unless `GITHUB_REF` is one of the spec's
|
|
71
|
+
* branches, the job runs in its forge environment (read from an OIDC
|
|
72
|
+
* token's claims, so only in a job that may request one), and, for a
|
|
73
|
+
* runner whose labels include `self-hosted`, on a self-hosted runner. The
|
|
74
|
+
* forgejo generator passes `forge: "forgejo"`, which checks the ref and
|
|
75
|
+
* runner only.
|
|
65
76
|
*/
|
|
66
|
-
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesControlTarget, opWavesLastJobs, opWavesOutcomeCommand, opWavesRecordPlansCommand, opWavesResumeCommand, } from "@intentius/chant/op/op-waves";
|
|
77
|
+
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveJobTimeout, opWaveOutcomeDir, opWavesControlTarget, opWavesLastJobs, opWavesApprovalStatusCommand, opWavesPostsApprovalStatus, opWavesOutcomeCommand, opWavesRecordPlansCommand, opWavesResumeCommand, } from "@intentius/chant/op/op-waves";
|
|
67
78
|
import { CONTROL_OUTCOME_VAR, controlDispatchJob } from "@intentius/chant/control/dispatch";
|
|
68
79
|
import { assertEnvironment, assertSetupSteps, CI_IMAGE, dirnameOf, environmentDoc, GITHUB_JOB_TOKEN, githubRunsOn, githubSecretEnv, idTokenAudienceEnv, nonEmpty, pushBranches, setupStepDoc, } from "@intentius/chant/ci-jobs";
|
|
69
80
|
import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
|
|
81
|
+
import { JOB_GUARD_ENV, jobGuardLine, jobGuardSpec, opWaveJobLabel } from "@intentius/chant/ci-job-guard";
|
|
82
|
+
import { resolveProductWords } from "@intentius/chant/ci-product-words";
|
|
70
83
|
import { checkoutStep, concurrency, DOWNLOAD_ARTIFACT, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs.js";
|
|
71
84
|
import { emitOpPipelineYAML } from "./generate-op-pipeline.js";
|
|
72
85
|
/** The default image: it carries git, which the runner needs to read the base commit. */
|
|
@@ -80,10 +93,14 @@ export function opWaveOutcomeArtifact(spec, jobName) {
|
|
|
80
93
|
return `${spec.name}-${jobName}-outcome`;
|
|
81
94
|
}
|
|
82
95
|
/** The workflow document, before emission, and the jobs it holds. */
|
|
83
|
-
export function buildGithubOpWavesDoc(spec, options) {
|
|
96
|
+
export function buildGithubOpWavesDoc(spec, options, dialect = {}) {
|
|
84
97
|
const jobs = opWaveJobs(spec, options.specFile, { bin: options.bin });
|
|
98
|
+
const forge = dialect.forge ?? "github";
|
|
99
|
+
const product = resolveProductWords(options.words ?? {}).product;
|
|
85
100
|
const image = options.image ?? OP_WAVES_IMAGE;
|
|
86
101
|
const byReview = spec.waves.some((w) => w.approval === "pr-review");
|
|
102
|
+
// #4630: a pr-review or pull-request wave gets the plans workflow and its approval status.
|
|
103
|
+
const statusBeforeMerge = opWavesPostsApprovalStatus(spec);
|
|
87
104
|
for (const wave of spec.waves) {
|
|
88
105
|
assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
|
|
89
106
|
if (wave.environment)
|
|
@@ -95,6 +112,8 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
const permissions = { contents: "write", ...(byReview ? { "pull-requests": "read" } : {}) };
|
|
115
|
+
// The ruling for ci-products (2026-10-28): every wave job carries its timeout and the lock wait under it.
|
|
116
|
+
const timeout = opWaveJobTimeout(options.jobTimeoutMinutes);
|
|
98
117
|
const jobsDoc = {};
|
|
99
118
|
for (const job of jobs) {
|
|
100
119
|
const wave = spec.waves[job.wave - 1];
|
|
@@ -102,7 +121,21 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
102
121
|
const group = opWaveGroupOf(spec, job);
|
|
103
122
|
// #4332: the job's cloud identities, traded before the checkout.
|
|
104
123
|
const cloud = group?.cloud ?? wave.cloud;
|
|
105
|
-
const
|
|
124
|
+
const environment = group?.environment ?? wave.environment;
|
|
125
|
+
const idTokens = group?.idTokens ?? wave.idTokens;
|
|
126
|
+
const idToken = idTokens !== undefined || cloud !== undefined;
|
|
127
|
+
const runner = group?.runner ?? wave.runner ?? spec.runner;
|
|
128
|
+
// #4624: the guard first, before the trade and the checkout.
|
|
129
|
+
const guard = guardStep({
|
|
130
|
+
forge,
|
|
131
|
+
job: opWaveJobLabel(spec.name, job),
|
|
132
|
+
product,
|
|
133
|
+
...(environment && forge === "github" ? { environment: environment.name } : {}),
|
|
134
|
+
branches: pushBranches(spec.branches),
|
|
135
|
+
...(runner !== undefined ? { runner } : {}),
|
|
136
|
+
idToken,
|
|
137
|
+
});
|
|
138
|
+
const steps = [guard, ...(cloud ? [tradeStep(cloud)] : []), checkoutStep(true)];
|
|
106
139
|
if (job.kind === "share") {
|
|
107
140
|
steps.push(downloadArtifactStep(`Download wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), dirnameOf(job.record)));
|
|
108
141
|
}
|
|
@@ -119,11 +152,8 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
119
152
|
steps.push(keepRecordStep(`Keep wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), job.record));
|
|
120
153
|
}
|
|
121
154
|
steps.push(keepRecordStep(`Keep the job's outcome`, opWaveOutcomeArtifact(spec, job.jobName), job.outcome));
|
|
122
|
-
const environment = group?.environment ?? wave.environment;
|
|
123
|
-
const idTokens = group?.idTokens ?? wave.idTokens;
|
|
124
|
-
const idToken = idTokens !== undefined || cloud !== undefined;
|
|
125
155
|
jobsDoc[job.jobName] = {
|
|
126
|
-
"runs-on": githubRunsOn(
|
|
156
|
+
"runs-on": githubRunsOn(runner),
|
|
127
157
|
...(job.needs.length > 0 ? { needs: job.needs } : {}),
|
|
128
158
|
// #4322: a wave's jobs run after a failure in the wave before; a share job runs only on its deciding job's success.
|
|
129
159
|
...(job.needs.length > 0 && job.kind !== "share" ? { if: "!cancelled()" } : {}),
|
|
@@ -131,7 +161,8 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
131
161
|
...(environment ? { environment: environmentDoc(environment) } : {}),
|
|
132
162
|
// #3914: a job's own permissions replace the workflow's, so they repeat them.
|
|
133
163
|
...(idToken ? { permissions: { ...permissions, "id-token": "write" } } : {}),
|
|
134
|
-
|
|
164
|
+
"timeout-minutes": timeout.minutes,
|
|
165
|
+
env: { ...timeout.env, ...(group || idTokens ? { ...wave.variables, ...group?.variables, ...idTokenAudienceEnv(idTokens) } : wave.variables) },
|
|
135
166
|
steps,
|
|
136
167
|
};
|
|
137
168
|
}
|
|
@@ -146,7 +177,8 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
146
177
|
name: spec.name,
|
|
147
178
|
on: { push: { branches: pushBranches(spec.branches) }, workflow_dispatch: {} },
|
|
148
179
|
...nonEmpty("env", env),
|
|
149
|
-
|
|
180
|
+
// The ruling for ci-products (2026-10-28): no workflow-level concurrency group. Runs on the target and repo grains wait for
|
|
181
|
+
// their rows on chant's ledger, oldest commit first; plunge stacks on the resource grain lease their resource rows and run.
|
|
150
182
|
permissions,
|
|
151
183
|
jobsDoc,
|
|
152
184
|
};
|
|
@@ -154,7 +186,7 @@ export function buildGithubOpWavesDoc(spec, options) {
|
|
|
154
186
|
doc,
|
|
155
187
|
jobs,
|
|
156
188
|
...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}),
|
|
157
|
-
...(
|
|
189
|
+
...(statusBeforeMerge ? { plansDoc: buildPlansDoc(spec, options, forge) } : {}),
|
|
158
190
|
};
|
|
159
191
|
}
|
|
160
192
|
/**
|
|
@@ -213,20 +245,53 @@ function buildResumeDoc(spec, options) {
|
|
|
213
245
|
jobsDoc: { resume: { "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps } },
|
|
214
246
|
};
|
|
215
247
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
248
|
+
/**
|
|
249
|
+
* The pull request workflow a `pr-review` wave needs (#3684): record the
|
|
250
|
+
* digests each head plans. The `approval-status` job after it (#4630) posts
|
|
251
|
+
* `<product>/approval` on the head, and runs alone on each review submitted
|
|
252
|
+
* or dismissed. It holds a read token that may write statuses and nothing
|
|
253
|
+
* else. A `pull-request` wave gets the same workflow: the record keeps its
|
|
254
|
+
* change set, and the status reads the pull request's own approval of it.
|
|
255
|
+
*/
|
|
256
|
+
function buildPlansDoc(spec, options, forge) {
|
|
218
257
|
const steps = [checkoutStep(true), ...(spec.waves[0]?.setup ?? []).map(setupStepDoc), ...runSteps(options.beforeScript ?? [])];
|
|
219
258
|
steps.push({ name: "Plan every wave at the head and record its digests", run: opWavesRecordPlansCommand(options.specFile, options.bin).join(" ") });
|
|
259
|
+
const statusSteps = [checkoutStep(true), ...runSteps(options.beforeScript ?? [])];
|
|
260
|
+
statusSteps.push({
|
|
261
|
+
name: "Post the approval status on the head",
|
|
262
|
+
run: opWavesApprovalStatusCommand(options.specFile, forge, options.bin).join(" "),
|
|
263
|
+
env: {
|
|
264
|
+
GITHUB_TOKEN: GITHUB_JOB_TOKEN,
|
|
265
|
+
PR_NUMBER: "${{ github.event.pull_request.number }}",
|
|
266
|
+
HEAD_SHA: "${{ github.event.pull_request.head.sha }}",
|
|
267
|
+
BASE_SHA: "${{ github.event.pull_request.base.sha }}",
|
|
268
|
+
},
|
|
269
|
+
});
|
|
220
270
|
return {
|
|
221
271
|
name: `${spec.name}-plans`,
|
|
222
|
-
on: { pull_request: { branches: pushBranches(spec.branches) } },
|
|
272
|
+
on: { pull_request: { branches: pushBranches(spec.branches) }, pull_request_review: { types: ["submitted", "dismissed"] } },
|
|
223
273
|
...nonEmpty("env", options.variables),
|
|
224
|
-
concurrency: concurrency(spec.name + "-plans-${{ github.event.number }}", true),
|
|
274
|
+
concurrency: concurrency(spec.name + "-plans-${{ github.event_name }}-${{ github.event.pull_request.number }}", true),
|
|
225
275
|
// contents: write pushes the record to chant/lifecycle.
|
|
226
276
|
permissions: { contents: "write" },
|
|
227
|
-
jobsDoc: {
|
|
277
|
+
jobsDoc: {
|
|
278
|
+
"record-plans": { if: "github.event_name == 'pull_request'", "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps },
|
|
279
|
+
"approval-status": {
|
|
280
|
+
needs: ["record-plans"],
|
|
281
|
+
// On a review the record-plans job is skipped, and the status reads the plans an earlier push recorded.
|
|
282
|
+
if: "${{ always() && (needs.record-plans.result == 'success' || needs.record-plans.result == 'skipped') }}",
|
|
283
|
+
"runs-on": githubRunsOn(spec.runner),
|
|
284
|
+
container: options.image ?? OP_WAVES_IMAGE,
|
|
285
|
+
permissions: { contents: "read", "pull-requests": "read", statuses: "write" },
|
|
286
|
+
steps: statusSteps,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
228
289
|
};
|
|
229
290
|
}
|
|
291
|
+
/** The apply-job guard step (#4624): the job's expectation in CHANT_JOB_GUARD, checked by `node -e` from `/`. */
|
|
292
|
+
function guardStep(input) {
|
|
293
|
+
return { name: "Check the job runs where the spec set it to run", env: { [JOB_GUARD_ENV]: JSON.stringify(jobGuardSpec(input)) }, run: jobGuardLine() };
|
|
294
|
+
}
|
|
230
295
|
/** The step that trades the job's OIDC token for `cloud` (#4332), writing the credentials to GITHUB_ENV. */
|
|
231
296
|
function tradeStep(cloud) {
|
|
232
297
|
return { name: "Trade the job's OIDC token for its cloud identities", env: { [TRADE_SPEC_ENV]: JSON.stringify(cloud) }, run: tradeLine("$GITHUB_ENV", "github-env") };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AA2CjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAkoBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
import { resolveComponentGraph } from "@intentius/chant/components/driver";
|
|
59
59
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
60
60
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
61
|
-
import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
|
|
61
|
+
import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER, githubRunsOn } from "@intentius/chant/ci-jobs";
|
|
62
62
|
import { CHECKOUT_TOKEN_ENV, checkoutScript, dropForgeTokens, handoverOutputs, handoverTrap, takeOverScript } from "@intentius/chant/ci-handover";
|
|
63
63
|
import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
|
|
64
|
-
import { prApplyComment, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, PR_LOCK_JOB, PR_UNLOCK_JOB,
|
|
64
|
+
import { prApplyComment, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, PR_LOCK_JOB, PR_UNLOCK_JOB, prUnlockCommand, prLockCommand, prCommentLockJobs, prCommentPreviewJob, PREVIEW_REF_ENV, PREVIEW_HEAD_OUTPUT, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, prLoopJobStage, prStaleJob, PR_STALE_JOB, prLoopRunner, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
|
|
65
65
|
import { forgejoRepoLockLines, REPO_APPLY_GROUP } from "@intentius/chant/components/repo-lock";
|
|
66
66
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
67
67
|
import { productStatusContext, resolveProductWords } from "@intentius/chant/ci-product-words";
|
|
@@ -322,7 +322,25 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
322
322
|
},
|
|
323
323
|
]);
|
|
324
324
|
}
|
|
325
|
-
const post = prLoopPostJob(forgeName, env, member?.name, loop.words ? resolveProductWords(loop.words) : undefined);
|
|
325
|
+
const post = prLoopPostJob(forgeName, env, member?.name, loop.words ? resolveProductWords(loop.words) : undefined, { ...(loop.staleNotes ? { staleNotes: true } : {}), ...(loop.respond ? { respond: loop.respond } : {}), ...(loop.decide ? { decide: loop.decide } : {}) });
|
|
326
|
+
// #4628: on a push to the target branch, the open pull requests' notes posted against another commit are marked stale.
|
|
327
|
+
const stale = prStaleJob(forgeName, env, loop, member?.name);
|
|
328
|
+
const staleJobs = stale
|
|
329
|
+
? {
|
|
330
|
+
[PR_STALE_JOB]: {
|
|
331
|
+
if: "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')",
|
|
332
|
+
"runs-on": GITHUB_RUNNER,
|
|
333
|
+
container: image,
|
|
334
|
+
permissions: { contents: "read", "pull-requests": "write", statuses: "write" },
|
|
335
|
+
steps: [
|
|
336
|
+
// The pushed commit: the target branch's, so none of a pull request's code is here.
|
|
337
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT },
|
|
338
|
+
...runSteps(beforeScript),
|
|
339
|
+
{ name: "Mark the open pull requests' notes stale", ...(rooted ? {} : { "working-directory": "." }), env: stale.env, run: stale.command },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
: {};
|
|
326
344
|
// #4619: with a control repo named, the note job reads its chant/lifecycle, read-only, so pr-post marks the cross-stack edges.
|
|
327
345
|
const controlRead = loop.control ? controlNoteCheckout(forgeName, loop.control.target, loop.words ?? {}) : undefined;
|
|
328
346
|
const noteSteps = [
|
|
@@ -338,6 +356,8 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
338
356
|
const prEnv = loop.prEnvironment ? prEnvGithubJobs(loop.forge ?? "github", image, beforeScript, extraScript, loop, member) : undefined;
|
|
339
357
|
// #4327: with ci.locks: plan the pull_request trigger fires on close too, for the unlock job, and the plan skips that event.
|
|
340
358
|
const locks = loop.locks === "plan";
|
|
359
|
+
// plywood#22: the lock lines run through the product's bin, as its other job lines do.
|
|
360
|
+
const lockBin = resolveProductWords(loop.words).bin;
|
|
341
361
|
const closes = Boolean(prEnv) || locks;
|
|
342
362
|
const lockJobs = locks
|
|
343
363
|
? {
|
|
@@ -356,7 +376,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
356
376
|
{
|
|
357
377
|
name: "Lock the components the plan selected",
|
|
358
378
|
env: { PR_NUMBER: "${{ github.event.pull_request.number }}", HEAD_SHA: "${{ github.event.pull_request.head.sha }}", GITHUB_TOKEN: GITHUB_JOB_TOKEN },
|
|
359
|
-
run: prLockCommand(forgeName),
|
|
379
|
+
run: prLockCommand(forgeName, lockBin),
|
|
360
380
|
},
|
|
361
381
|
],
|
|
362
382
|
},
|
|
@@ -368,7 +388,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
368
388
|
steps: [
|
|
369
389
|
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}", "fetch-depth": 0 } },
|
|
370
390
|
...runSteps(beforeScript),
|
|
371
|
-
{ name: "Release the pull request's locks", env: { PR_NUMBER: "${{ github.event.pull_request.number }}" }, run:
|
|
391
|
+
{ name: "Release the pull request's locks", env: { PR_NUMBER: "${{ github.event.pull_request.number }}" }, run: prUnlockCommand(lockBin) },
|
|
372
392
|
],
|
|
373
393
|
},
|
|
374
394
|
}
|
|
@@ -468,6 +488,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
468
488
|
...lockJobs,
|
|
469
489
|
...commentLockJobs,
|
|
470
490
|
...previewJobs,
|
|
491
|
+
...staleJobs,
|
|
471
492
|
...Object.fromEntries(applyJobs),
|
|
472
493
|
...Object.fromEntries(controlJobs),
|
|
473
494
|
...(prEnv ? prEnv.jobs : {}),
|
|
@@ -487,6 +508,12 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
487
508
|
// #4301: a /chant apply comment starts the apply before merge. #4588: and a lock or unlock comment its job.
|
|
488
509
|
if (loop.applyBeforeMerge || commentLocks.length > 0 || preview)
|
|
489
510
|
on.issue_comment = { types: ["created"] };
|
|
511
|
+
// #4625: each job on its stage's runner, the plan side's, the apply side's, or the default.
|
|
512
|
+
if (loop.runners) {
|
|
513
|
+
for (const [name, job] of Object.entries(jobsDoc)) {
|
|
514
|
+
job["runs-on"] = githubRunsOn(prLoopRunner(loop, prLoopJobStage(name)));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
490
517
|
return {
|
|
491
518
|
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
492
519
|
environment: env,
|
|
@@ -494,10 +521,11 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
494
521
|
...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
|
|
495
522
|
env: { ...options.variables, CHANT_ENV: env },
|
|
496
523
|
jobsDoc,
|
|
497
|
-
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
524
|
+
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...commentLocks.map((j) => j.jobName), ...(preview ? [preview.jobName, preview.noteJobName] : []), ...(stale ? [PR_STALE_JOB] : []), ...applyNames, ...controlJobs.map(([name]) => name), ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
498
525
|
jobs: [
|
|
499
526
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
500
527
|
{ jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
|
|
528
|
+
...(stale ? [{ jobName: PR_STALE_JOB, component: "pull request stale notes", stage: PR_STALE_JOB, needs: [] }] : []),
|
|
501
529
|
...(locks
|
|
502
530
|
? [
|
|
503
531
|
{ jobName: PR_LOCK_JOB, component: "pull request lock", stage: PR_LOCK_JOB, needs: ["plan"] },
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "eab8cbe3ae719023202bcc5ffdbbbd5ca8dd0d263d22ca405024ff9991cf07b6",
|
|
5
5
|
"meta.json": "c50e0ef3845c370c3ad2210b8bba455980417c0f818aa6ed46e06d4fb8b1b9f9",
|
|
6
6
|
"types/index.d.ts": "0ead95536e947ddfb98a5acf0ddb88547913b5a53a342df67711c59266f84b3e",
|
|
7
7
|
"rules/deprecated-action-version.ts": "a9ba7cc924ff1c7055110416d8989273690415bfb5e8dcc32dc9431b9351f5ca",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
|
|
79
79
|
"skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
|
|
80
80
|
},
|
|
81
|
-
"composite": "
|
|
81
|
+
"composite": "c581b6a6d25abd873cb8aba19d5ccd648c178312acdbdac07a30900b9b685b5a"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.57",
|
|
4
4
|
"description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.124.
|
|
64
|
+
"@intentius/chant": "^0.124.57"
|
|
65
65
|
}
|
|
66
66
|
}
|