@intentius/chant-lexicon-github 0.124.40 → 0.124.42

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.
@@ -23,6 +23,15 @@ export declare function downloadArtifactStep(name: string, artifact: string, pat
23
23
  * replaces it. Nothing to keep is not an error.
24
24
  */
25
25
  export declare function keepRecordStep(name: string, artifact: string, path: string): Record<string, unknown>;
26
+ /**
27
+ * The `if:` of a wave job after the first (#4291): it runs when the wave
28
+ * before it failed, so a failed member blocks only the members that depend
29
+ * on it, and the next wave's job blocks those and runs the rest. It does not
30
+ * run when the run was cancelled, or when the job before it was skipped
31
+ * (its own condition was false). `condition` is the job's own condition,
32
+ * which still has to hold.
33
+ */
34
+ export declare function afterEarlierWave(previous: string, condition?: string): string;
26
35
  /** A `concurrency:` mapping: one run at a time per group. */
27
36
  export declare function concurrency(group: string, cancelInProgress?: boolean): Record<string, unknown>;
28
37
  /** The job token, as a step's `GITHUB_TOKEN`; `gh` reads `GH_TOKEN` too, for a step that writes. */
@@ -1 +1 @@
1
- {"version":3,"file":"ci-jobs.d.ts","sourceRoot":"","sources":["../../src/components/ci-jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D,qGAAqG;AACrG,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAC5D,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,iIAAiI;AACjI,wBAAgB,YAAY,CAAC,WAAW,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAEjF;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE1G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOpG;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5F;AAED,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,EAAE,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYrE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,MAAM,CAG9E;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,qGAAqG;IACrG,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAW/D"}
1
+ {"version":3,"file":"ci-jobs.d.ts","sourceRoot":"","sources":["../../src/components/ci-jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D,qGAAqG;AACrG,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAC5D,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,iIAAiI;AACjI,wBAAgB,YAAY,CAAC,WAAW,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAEjF;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE1G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOpG;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5F;AAED,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,EAAE,UAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYrE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,MAAM,CAG9E;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,qGAAqG;IACrG,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAW/D"}
@@ -38,6 +38,18 @@ export function keepRecordStep(name, artifact, path) {
38
38
  with: { name: artifact, path, "if-no-files-found": "ignore", "include-hidden-files": true, overwrite: true },
39
39
  };
40
40
  }
41
+ /**
42
+ * The `if:` of a wave job after the first (#4291): it runs when the wave
43
+ * before it failed, so a failed member blocks only the members that depend
44
+ * on it, and the next wave's job blocks those and runs the rest. It does not
45
+ * run when the run was cancelled, or when the job before it was skipped
46
+ * (its own condition was false). `condition` is the job's own condition,
47
+ * which still has to hold.
48
+ */
49
+ export function afterEarlierWave(previous, condition) {
50
+ const runs = `!cancelled() && needs.${previous}.result != 'skipped'`;
51
+ return condition ? `${runs} && (${condition})` : runs;
52
+ }
41
53
  /** A `concurrency:` mapping: one run at a time per group. */
42
54
  export function concurrency(group, cancelInProgress = false) {
43
55
  return { group, "cancel-in-progress": cancelInProgress };
@@ -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;AAmBjG,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;AA+ND;;;;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"}
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;AA4BjG,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;;;;;;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;AA2bD;;;;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,15 @@
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_RUNNER } from "@intentius/chant/ci-jobs";
62
- import { GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
61
+ import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
62
+ import { CHECKOUT_TOKEN_ENV, checkoutScript, dropForgeTokens, handoverOutputs, handoverTrap, takeOverScript } from "@intentius/chant/ci-handover";
63
+ import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine } from "@intentius/chant/ci-identity";
64
+ import { APPLY_COMMENT, GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopPostJob, prLoopSetup, PR_PLAN_NOTE_JOB, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
63
65
  import { memberRepoPath } from "@intentius/chant/lexicon";
64
- import { checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
66
+ import { afterEarlierWave, checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
67
+ import { actionRef } from "../action-pins.js";
68
+ /** The plan job's last step, whose outputs carry its report to the plan-note job (#4295). */
69
+ const PLAN_STEP_ID = "plan";
65
70
  /** The output a gated apply wave hands the green commit it applies through, and the step that sets it (#4221). */
66
71
  const WAVE_GREEN_OUTPUT = "green-sha";
67
72
  const WAVE_STEP_ID = "apply";
@@ -89,6 +94,8 @@ function gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, o
89
94
  steps.push(...runSteps(prLoopSetup(image)), ...runSteps(beforeScript), { run: job.command.join(" ") }, ...runSteps(extraScript));
90
95
  steps.push(keepRecordStep(`Upload the record for wave ${job.wave}`, waveRecordArtifact(job.wave), GATED_WAVE_RECORD));
91
96
  jobsDoc[job.jobName] = {
97
+ // #4291: a wave runs after one that failed, and blocks only what depends on the failure.
98
+ ...(job.needs.length > 0 ? { if: afterEarlierWave(job.needs[0]) } : {}),
92
99
  "runs-on": GITHUB_RUNNER,
93
100
  ...(job.needs.length > 0 ? { needs: job.needs } : {}),
94
101
  container: image,
@@ -133,6 +140,35 @@ function gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, o
133
140
  * for the pushed commit, so wave k reads what wave k-1 left, and a re-run of
134
141
  * a wave that stopped at its gate reads what it left itself. Each keeps its
135
142
  * report as `chant-pr-apply-wave-<k>`.
143
+ *
144
+ * The plan job holds no forge token while the pull request's code runs
145
+ * (#4295, `@intentius/chant/ci-handover`). It checks the change out with a
146
+ * script that gives the job's token to its fetches alone, starts every later
147
+ * step again without the token variables, and ends with the plan step, which
148
+ * hands its report on as the job's outputs. The `plan-note` job then checks
149
+ * out the target branch's commit, takes the report back, keeps it as the
150
+ * `chant-pr-plan` artifact, and posts the note and `chant/plan` with
151
+ * `chant components pr-post`. Only the note job may write to the pull
152
+ * request and its statuses.
153
+ *
154
+ * With `identity` (#4296, `@intentius/chant/ci-identity`) the plan, record
155
+ * and apply jobs get `id-token: write`, and each starts with a step that
156
+ * trades the job's OIDC token for its cloud identity before anything is
157
+ * checked out: the plan and record jobs for the plan identity, the apply
158
+ * jobs for the apply identity. The trade leaves the credentials in
159
+ * `GITHUB_ENV`. Every later step of the plan job starts without the
160
+ * variables a token is requested with, so the pull request's code holds no
161
+ * OIDC token and cannot request one, and the plan job names no apply
162
+ * identity.
163
+ *
164
+ * With `applyBeforeMerge` (#4301, ws-106) the apply job runs on a
165
+ * `/chant apply` comment on a pull request (`issue_comment`), from the
166
+ * default branch's workflow file, so a pull request cannot change the job
167
+ * that applies it. It checks out the pull request's head after the setup
168
+ * and `beforeScript` ran at the default branch's commit, and runs `pr-apply
169
+ * --before-merge`, which asks the forge the requires checks before any of
170
+ * the head's code loads. On GitHub the commenter must be an owner, member
171
+ * or collaborator. A push still records.
136
172
  */
137
173
  function prLoopGithubDoc(components, env, image, beforeScript, extraScript, options) {
138
174
  const loop = options.prLoop;
@@ -158,8 +194,18 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
158
194
  uses: "actions/cache/save@v4",
159
195
  with: { path: recordPath, key: recordKey(wave) },
160
196
  });
197
+ // #4296: the step that trades the job's OIDC token for its cloud identity, before the checkout.
198
+ const identity = loop.identity;
199
+ const tradeStep = (spec, which) => ({
200
+ name: `Trade the job's OIDC token for the ${which} identity`,
201
+ ...(rooted ? {} : { "working-directory": "." }),
202
+ env: { [TRADE_SPEC_ENV]: JSON.stringify(spec) },
203
+ run: tradeLine("$GITHUB_ENV", "github-env"),
204
+ });
205
+ const withIdToken = (permissions) => (identity ? { ...permissions, "id-token": "write" } : permissions);
161
206
  const reportName = (job) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
162
207
  const steps = (job, name) => [
208
+ ...(identity ? [tradeStep(job.jobName === "record" ? identity.plan : identity.apply, job.jobName === "record" ? "plan" : "apply")] : []),
163
209
  checkoutStep(true),
164
210
  ...runSteps(prLoopSetup(image)),
165
211
  ...runSteps(beforeScript),
@@ -189,13 +235,22 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
189
235
  const forgejo = loop.forge === "forgejo";
190
236
  const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
191
237
  const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
238
+ // #4301: a `/chant apply` comment on a pull request starts the apply before merge. GitHub also asks for a commenter who can write.
239
+ const onComment = [
240
+ "github.event_name == 'issue_comment'",
241
+ // Forgejo's payload says so with is_pull (terragucci comment-apply.ts:129 at e6cb431).
242
+ forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
243
+ `startsWith(github.event.comment.body, '${APPLY_COMMENT}')`,
244
+ ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
245
+ ].join(" && ");
192
246
  const applyJob = (job, needs, name) => ({
193
- if: job.jobName === "record" ? onPush : onGreen,
247
+ // #4291: a wave after the first runs after one that failed, and blocks only what depends on the failure.
248
+ if: job.jobName === "record" ? onPush : needs.length > 0 && job.wave ? afterEarlierWave(needs[0], onGreen) : onGreen,
194
249
  "runs-on": GITHUB_RUNNER,
195
250
  ...(needs.length > 0 ? { needs } : {}),
196
251
  container: image,
197
252
  concurrency: concurrency(prApplyGroup(env, member?.name)),
198
- permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
253
+ permissions: withIdToken({ contents: "write", "pull-requests": "write", statuses: "write" }),
199
254
  ...(job.wave ? { outputs: { [WAVE_GREEN_OUTPUT]: `\${{ steps.${WAVE_STEP_ID}.outputs.${WAVE_GREEN_OUTPUT} }}` } } : {}),
200
255
  steps: steps(job, name),
201
256
  });
@@ -208,43 +263,201 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
208
263
  ])
209
264
  : loop.applyOnMerge
210
265
  ? [["apply", applyJob(apply, [], "Apply the newest green commit under the merged changes' approvals")]]
211
- : [];
266
+ : loop.applyBeforeMerge
267
+ ? [["apply", { ...applyJob(apply, [], "Apply the pull request's head under its gate, once it is approved, mergeable, undiverged and green"), if: onComment }]]
268
+ : [];
212
269
  const applyJobs = [recordJob, ...greenJobs];
270
+ // #4295: the plan job runs the pull request's code and holds no forge token while it does.
271
+ const forgeName = loop.forge ?? "github";
272
+ // #4296: once the plan job has traded its OIDC token, its later steps start without the variables a token is requested with.
273
+ const drop = dropForgeTokens("sh", identity ? OIDC_REQUEST_VARS : []);
274
+ const headStep = (line, name) => ({ ...(name ? { name } : {}), run: `${drop}\n${line}` });
275
+ const planSteps = [
276
+ // #4296: the trade runs first, before the change is checked out, with the plan identity alone.
277
+ ...(identity ? [tradeStep(identity.plan, "plan")] : []),
278
+ // A script, not actions/checkout, whose post step runs at the job's end with the token as its input.
279
+ {
280
+ name: "Check out the change",
281
+ ...(rooted ? {} : { "working-directory": "." }),
282
+ env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
283
+ run: checkoutScript(true, true),
284
+ },
285
+ ...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
286
+ ...(plan.build ? [headStep(plan.build[0], "Build the templates"), headStep(plan.build[1], "Lint the project")] : []),
287
+ // The job's last step: a step after it would start beside whatever the change's code left running.
288
+ {
289
+ name: "Plan the members this pull request reaches",
290
+ id: PLAN_STEP_ID,
291
+ env: plan.run.env,
292
+ run: [drop, ...handoverTrap(PR_LOOP_REPORT_DIR, forgeName), ...(plan.checkout ?? []), ...(plan.setup ?? []), plan.run.command, ...extraScript].join("\n"),
293
+ },
294
+ ];
295
+ const post = prLoopPostJob(forgeName, env, member?.name);
296
+ const noteSteps = [
297
+ // The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
298
+ { name: "Check out the target branch's commit", uses: actionRef("actions/checkout"), with: { ref: "${{ github.event.pull_request.base.sha }}" } },
299
+ ...runSteps(beforeScript),
300
+ { name: "Take the plan report from the plan job", run: takeOverScript("plan", PR_LOOP_REPORT_DIR, forgeName) },
301
+ { name: "Keep the plan report", if: "always()", uses: UPLOAD_ARTIFACT, with: { name: reportName(plan), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } },
302
+ { name: "Post the plan note and chant/plan", if: "always()", env: post.env, run: post.command },
303
+ ];
304
+ // #4298: with a pull request environment the pull_request trigger fires on close too, and the plan skips that event.
305
+ const prEnv = loop.prEnvironment ? prEnvGithubJobs(loop.forge ?? "github", image, beforeScript, extraScript, loop, member) : undefined;
213
306
  const jobsDoc = {
214
307
  plan: {
215
- if: "github.event_name == 'pull_request'",
308
+ if: prEnv ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
309
+ "runs-on": GITHUB_RUNNER,
310
+ container: image,
311
+ permissions: withIdToken({ contents: "read" }),
312
+ outputs: handoverOutputs(PLAN_STEP_ID, forgeName),
313
+ steps: planSteps,
314
+ },
315
+ [PR_PLAN_NOTE_JOB]: {
316
+ needs: ["plan"],
317
+ if: "always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')",
216
318
  "runs-on": GITHUB_RUNNER,
217
319
  container: image,
218
- permissions: { contents: "read", "pull-requests": "write", statuses: "write" },
219
- steps: steps(plan, "Plan the members this pull request reaches"),
320
+ // #4289: `actions: read` lets pr-post ask when each scheduled watch or reconcile Op last ran.
321
+ permissions: { actions: "read", contents: "read", "pull-requests": "write", statuses: "write" },
322
+ steps: noteSteps,
220
323
  },
221
324
  ...Object.fromEntries(applyJobs),
325
+ ...(prEnv ? prEnv.jobs : {}),
222
326
  };
223
327
  const applyNames = applyJobs.map(([name]) => name);
328
+ const on = !loop.applyOnMerge
329
+ ? { pull_request: { branches: [branch] }, push: { branches: [branch] } }
330
+ : forgejo
331
+ ? { pull_request: { branches: [branch] }, push: { branches: [branch], tags: [GREEN_TAG_PATTERN] } }
332
+ : { pull_request: { branches: [branch] }, push: { branches: [branch] }, workflow_run: { workflows: [GREEN_TICK_WORKFLOW], types: ["completed"] }, workflow_dispatch: {} };
333
+ if (prEnv) {
334
+ on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
335
+ on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
336
+ }
337
+ // #4301: a /chant apply comment starts the apply before merge.
338
+ if (loop.applyBeforeMerge)
339
+ on.issue_comment = { types: ["created"] };
224
340
  return {
225
341
  name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
226
342
  environment: env,
227
- on: !loop.applyOnMerge
228
- ? { pull_request: { branches: [branch] }, push: { branches: [branch] } }
229
- : forgejo
230
- ? { pull_request: { branches: [branch] }, push: { branches: [branch], tags: [GREEN_TAG_PATTERN] } }
231
- : { pull_request: { branches: [branch] }, push: { branches: [branch] }, workflow_run: { workflows: [GREEN_TICK_WORKFLOW], types: ["completed"] }, workflow_dispatch: {} },
343
+ on,
232
344
  ...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
233
345
  env: { ...options.variables, CHANT_ENV: env },
234
346
  jobsDoc,
235
- stages: ["plan", ...applyNames],
347
+ stages: ["plan", PR_PLAN_NOTE_JOB, ...applyNames, ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
236
348
  jobs: [
237
349
  { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
350
+ { jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
238
351
  ...applyNames.map((name, i) => ({
239
352
  jobName: name,
240
- component: name === "record" ? "merge record" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
353
+ component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
241
354
  stage: name,
242
355
  // The record runs on a push and the apply on a green commit, so only the waves need each other.
243
356
  needs: i > 1 ? [applyNames[i - 1]] : [],
244
357
  })),
358
+ ...(prEnv ? prEnv.listed : []),
359
+ ],
360
+ };
361
+ }
362
+ /**
363
+ * The jobs of a pull request's environment (#4298). `env-up` runs on an
364
+ * opened, updated or reopened pull request and runs the change's code, so it
365
+ * follows the plan job's handover (#4295): a checkout script that alone sees
366
+ * the job's token, every later step started without the token variables,
367
+ * and the `pr-env up` step last, handing its report and record on as the
368
+ * job's outputs. `env-post` checks out the target branch's commit, takes
369
+ * them back, keeps the record on chant/lifecycle and posts the note.
370
+ * `env-down` runs when the pull request closes and `env-sweep` on the
371
+ * schedule, both from the base branch's code: a teardown reads the markers
372
+ * off the live resources and builds nothing of the change. `env-up` and
373
+ * `env-down` run one at a time per pull request, and a close waits for an
374
+ * apply still running rather than cancelling it.
375
+ */
376
+ function prEnvGithubJobs(forge, image, beforeScript, extraScript, loop, member) {
377
+ const [up, post, down, sweep] = prEnvJobs(forge, loop, member?.name);
378
+ const rooted = !member || member.dir === "." || member.dir === "";
379
+ const reportPath = member ? memberRepoPath(member, PR_ENV_REPORT_DIR) : PR_ENV_REPORT_DIR;
380
+ const prNumber = "${{ github.event.pull_request.number }}";
381
+ const group = `chant-pr-env-${member ? `${member.name}-` : ""}${prNumber}`;
382
+ const writes = { contents: "write", "pull-requests": "write", statuses: "write" };
383
+ const keep = (name) => ({ name: `Keep the ${name} report`, if: "always()", uses: UPLOAD_ARTIFACT, with: { name: `chant-pr-${name}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } });
384
+ const opened = "github.event_name == 'pull_request' && github.event.action != 'closed'";
385
+ // #4295: env-up runs the change's code and holds no forge token while it does.
386
+ const drop = dropForgeTokens("sh");
387
+ const headStep = (line, name, env) => ({ ...(name ? { name } : {}), ...(env ? { env } : {}), run: `${drop}\n${line}` });
388
+ const upJob = {
389
+ if: opened,
390
+ "runs-on": GITHUB_RUNNER,
391
+ container: image,
392
+ concurrency: concurrency(group),
393
+ permissions: { contents: "read" },
394
+ outputs: handoverOutputs(PR_ENV_UP_STEP_ID, forge),
395
+ steps: [
396
+ {
397
+ name: "Check out the change",
398
+ ...(rooted ? {} : { "working-directory": "." }),
399
+ env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
400
+ run: checkoutScript(true, true),
401
+ },
402
+ ...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
403
+ ...(up.build ? [headStep(up.build[0], "Build the templates", { CHANT_ENV: up.env.CHANT_ENV }), headStep(up.build[1], "Lint the project")] : []),
404
+ // The job's last step: a step after it would start beside whatever the change's code left running.
405
+ {
406
+ name: "Apply this pull request's environment once its plan is approved",
407
+ id: PR_ENV_UP_STEP_ID,
408
+ env: up.env,
409
+ run: [drop, ...handoverTrap(PR_ENV_REPORT_DIR, forge), up.command, ...extraScript].join("\n"),
410
+ },
411
+ ],
412
+ };
413
+ const postJob = {
414
+ needs: ["env-up"],
415
+ if: `always() && ${opened} && (needs.env-up.result == 'success' || needs.env-up.result == 'failure')`,
416
+ "runs-on": GITHUB_RUNNER,
417
+ container: image,
418
+ concurrency: concurrency(`chant-pr-env-post-${member ? `${member.name}-` : ""}${prNumber}`),
419
+ permissions: writes,
420
+ steps: [
421
+ // The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
422
+ { name: "Check out the target branch's commit", uses: actionRef("actions/checkout"), with: { ref: "${{ github.event.pull_request.base.sha }}", "fetch-depth": 0 } },
423
+ ...runSteps(beforeScript),
424
+ { name: "Take the environment's report from the env-up job", run: takeOverScript("env-up", PR_ENV_REPORT_DIR, forge) },
425
+ keep("env-up"),
426
+ { name: "Keep the environment's record and post its note", if: "always()", env: post.env, run: post.command },
427
+ ],
428
+ };
429
+ const baseJob = (j, condition, ref, name, groupName) => ({
430
+ if: condition,
431
+ "runs-on": GITHUB_RUNNER,
432
+ container: image,
433
+ concurrency: concurrency(groupName),
434
+ permissions: writes,
435
+ steps: [
436
+ { uses: actionRef("actions/checkout"), with: { "fetch-depth": 0, ...(ref ? { ref } : {}) } },
437
+ ...runSteps(prLoopSetup(image)),
438
+ ...runSteps(beforeScript),
439
+ { name, env: j.env, run: j.command },
440
+ ...runSteps(extraScript),
441
+ keep(j.jobName),
442
+ ],
443
+ });
444
+ return {
445
+ jobs: {
446
+ "env-up": upJob,
447
+ [PR_ENV_POST_JOB]: postJob,
448
+ "env-down": baseJob(down, "github.event_name == 'pull_request' && github.event.action == 'closed'", "${{ github.event.pull_request.base.ref }}", "Tear down what this pull request's environment owns", group),
449
+ "env-sweep": baseJob(sweep, "github.event_name == 'schedule'", "", "Tear down the environments whose TTL passed or whose pull request closed", `chant-pr-env-sweep${member ? `-${member.name}` : ""}`),
450
+ },
451
+ listed: [
452
+ { jobName: "env-up", component: "pull request environment", stage: "env-up", needs: [] },
453
+ { jobName: PR_ENV_POST_JOB, component: "pull request environment note", stage: PR_ENV_POST_JOB, needs: ["env-up"] },
454
+ { jobName: "env-down", component: "pull request environment teardown", stage: "env-down", needs: [] },
455
+ { jobName: "env-sweep", component: "pull request environment sweep", stage: "env-sweep", needs: [] },
245
456
  ],
246
457
  };
247
458
  }
459
+ /** The id of the `pr-env up` step, whose outputs carry the report (#4295). */
460
+ const PR_ENV_UP_STEP_ID = "env-up";
248
461
  /** The file a producer dumps its resolved outputs to, and a dependent seeds from — one per component, matching gitlab's naming so both generators produce interchangeable artifacts. */
249
462
  function outputsFile(name) {
250
463
  return `${name}.outputs.json`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "8dd7e87bc797876ca0528fe7a639dd8cc5928c4c1df3ce36a07064b7bf836ee9",
4
+ "manifest.json": "f43fcee2c55ab1b71df96a4010da7ae13b50257d1f5f287ae9eaf4582a4403b9",
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": "1e3b682fbb4407630890ec75fe1ff549f2f28754e543a303bf791bb88be953a8"
81
+ "composite": "20903f358de58d2aa87ad426ed158a792c906737d6064d904af4ff0da466c121"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.124.40",
3
+ "version": "0.124.42",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.124.40",
3
+ "version": "0.124.42",
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.40"
64
+ "@intentius/chant": "^0.124.42"
65
65
  }
66
66
  }
@@ -24,24 +24,82 @@ jobs:
24
24
  container: node:22
25
25
  permissions:
26
26
  contents: read
27
- pull-requests: write
28
- statuses: write
27
+ outputs:
28
+ report_0: '${{ steps.plan.outputs.report_0 }}'
29
29
  steps:
30
- - uses: actions/checkout@v7
31
- with:
32
- fetch-depth: 0
33
- - run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
30
+ - name: Check out the change
31
+ env:
32
+ CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
33
+ run: |
34
+ set -eu
35
+ git config --global --add safe.directory "$PWD" 2>/dev/null || true
36
+ [ -d .git ] || git init -q .
37
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
38
+ if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
39
+ chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
40
+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
41
+ unset chant_auth
42
+ fi
43
+ ref="$GITHUB_REF"
44
+ want="$GITHUB_SHA"
45
+ case "$ref" in
46
+ refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
47
+ refs/tags/*) target="$ref" ;;
48
+ refs/*) target="refs/remotes/${ref#refs/}" ;;
49
+ *) target="refs/remotes/origin/chant-head" ;;
50
+ esac
51
+ if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
52
+ git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
53
+ git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
54
+ case "$ref" in
55
+ refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
56
+ *) git checkout -q --force --detach "$target" ;;
57
+ esac
58
+ - run: |
59
+ if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
60
+ command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
34
61
  - name: Build the templates
35
- run: chant build --env prod
62
+ run: |
63
+ if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
64
+ chant build --env prod
36
65
  - name: Lint the project
37
- run: chant lint
66
+ run: |
67
+ if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
68
+ chant lint
38
69
  - name: Plan the members this pull request reaches
70
+ id: plan
39
71
  env:
40
72
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
41
73
  PR_NUMBER: '${{ github.event.pull_request.number }}'
42
- GITHUB_TOKEN: '${{ github.token }}'
43
74
  CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
44
- run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
75
+ run: |
76
+ if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
77
+ chant_report="$PWD/.chant/pr"
78
+ chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=450000,PARTS=1,NOTE=["pr-plan.json","pr-note.md"];const all={};const walk=(d,r)=>{let n=[];try{n=fs.readdirSync(d).sort()}catch{return}for(const x of n){const f=p.join(d,x),k=r?r+"/"+x:x;let s;try{s=fs.lstatSync(f)}catch{continue}if(s.isDirectory())walk(f,k);else if(s.isFile())all[k]=fs.readFileSync(f).toString("base64")}};walk(dir,"");const pack=(o)=>z.gzipSync(JSON.stringify(o)).toString("base64");let b=pack(all);if(b.length>CHUNK*PARTS){b=pack(Object.fromEntries(Object.entries(all).filter(([k])=>NOTE.includes(k))));if(b.length>CHUNK*PARTS){b=pack({});console.error("chant: the plan report is too large to hand to the note job, even cut to the report and the note; it hands on none of it")}else console.error("chant: the plan report is too large to hand to the note job whole; it hands on pr-plan.json and pr-note.md")}let o="";for(let i=0;i*CHUNK<b.length;i++)o+="report_"+i+"="+b.slice(i*CHUNK,(i+1)*CHUNK)+"\n";fs.appendFileSync(out,o);' "$chant_report" "${GITHUB_OUTPUT:-/dev/null}" || echo "chant: the plan report could not be handed to the note job" >&2; }
79
+ trap chant_handover EXIT
80
+ chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
81
+ plan-note:
82
+ needs:
83
+ - plan
84
+ if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
85
+ runs-on: ubuntu-latest
86
+ container: node:22
87
+ permissions:
88
+ actions: read
89
+ contents: read
90
+ pull-requests: write
91
+ statuses: write
92
+ steps:
93
+ - name: Check out the target branch's commit
94
+ uses: actions/checkout@v7
95
+ with:
96
+ ref: '${{ github.event.pull_request.base.sha }}'
97
+ - name: Take the plan report from the plan job
98
+ run: |
99
+ set -u
100
+ node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const dir=process.argv[1];let b="";for(const l of fs.readFileSync(0,"utf8").split("\n")){if(!l.trim())continue;let s="";try{s=JSON.parse(l)}catch{}if(typeof s==="string")b+=s}if(!b){console.log("chant: the plan job handed on no report");process.exit(0)}let files;try{files=JSON.parse(z.gunzipSync(Buffer.from(b,"base64"),{maxOutputLength:268435456}).toString("utf8"))}catch(e){console.log("chant: the report the plan job handed on does not read: "+e.message);process.exit(0)}if(!files||typeof files!=="object"||Array.isArray(files)){console.log("chant: the report the plan job handed on is not a set of files");process.exit(0)}let n=0;for(const [k,v] of Object.entries(files)){const parts=k.split("/");if(typeof v!=="string"||k.length>1024||parts.some((x)=>!x||x==="."||x===".."||/[\\\x00-\x1f]/.test(x))){console.log("chant: left out "+JSON.stringify(k));continue}try{const f=p.join(dir,...parts);fs.mkdirSync(p.dirname(f),{recursive:true});fs.writeFileSync(f,Buffer.from(v,"base64"));n++}catch(e){console.log("chant: could not write "+JSON.stringify(k)+": "+e.message)}}console.log("chant: took "+n+" report files from the plan job")' .chant/pr <<'END_OF_REPORT'
101
+ ${{ toJSON(needs.plan.outputs.report_0) }}
102
+ END_OF_REPORT
45
103
  - name: Keep the plan report
46
104
  if: always()
47
105
  uses: actions/upload-artifact@v4
@@ -50,6 +108,14 @@ jobs:
50
108
  path: .chant/pr
51
109
  if-no-files-found: ignore
52
110
  include-hidden-files: true
111
+ - name: Post the plan note and chant/plan
112
+ if: always()
113
+ env:
114
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
115
+ HEAD_SHA: '${{ github.sha }}'
116
+ PLAN_RESULT: '${{ needs.plan.result }}'
117
+ GITHUB_TOKEN: '${{ github.token }}'
118
+ run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"
53
119
  record:
54
120
  if: github.event_name == 'push'
55
121
  runs-on: ubuntu-latest