@intentius/chant-lexicon-github 0.124.54 → 0.124.56

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.
Files changed (39) hide show
  1. package/dist/components/agent-jobs.d.ts +67 -0
  2. package/dist/components/agent-jobs.d.ts.map +1 -0
  3. package/dist/components/agent-jobs.js +139 -0
  4. package/dist/components/control-pipeline.d.ts +34 -0
  5. package/dist/components/control-pipeline.d.ts.map +1 -0
  6. package/dist/components/control-pipeline.js +45 -0
  7. package/dist/components/generate-op-pipeline.d.ts +8 -1
  8. package/dist/components/generate-op-pipeline.d.ts.map +1 -1
  9. package/dist/components/generate-op-pipeline.js +144 -16
  10. package/dist/components/generate-op-waves-pipeline.d.ts +27 -1
  11. package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
  12. package/dist/components/generate-op-waves-pipeline.js +135 -16
  13. package/dist/components/generate-pipeline.d.ts.map +1 -1
  14. package/dist/components/generate-pipeline.js +160 -15
  15. package/dist/integrity.json +2 -2
  16. package/dist/manifest.json +1 -1
  17. package/package.json +2 -2
  18. package/src/components/__golden__/op-waves-jobs.yml +20 -0
  19. package/src/components/agent-jobs.ts +177 -0
  20. package/src/components/control-pipeline.ts +65 -0
  21. package/src/components/generate-op-pipeline.agent.test.ts +147 -0
  22. package/src/components/generate-op-pipeline.cloud.test.ts +96 -0
  23. package/src/components/generate-op-pipeline.notice.test.ts +2 -1
  24. package/src/components/generate-op-pipeline.ts +165 -20
  25. package/src/components/generate-op-waves-pipeline.approval.test.ts +46 -0
  26. package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
  27. package/src/components/generate-op-waves-pipeline.guard.test.ts +57 -0
  28. package/src/components/generate-op-waves-pipeline.identity.test.ts +8 -8
  29. package/src/components/generate-op-waves-pipeline.test.ts +17 -0
  30. package/src/components/generate-op-waves-pipeline.ts +157 -15
  31. package/src/components/generate-pipeline.ts +170 -15
  32. package/src/components/generate-pr-pipeline.comment-locks.test.ts +53 -0
  33. package/src/components/generate-pr-pipeline.comment-preview.test.ts +64 -0
  34. package/src/components/generate-pr-pipeline.comment-targets.test.ts +24 -0
  35. package/src/components/generate-pr-pipeline.control.test.ts +94 -0
  36. package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -0
  37. package/src/components/generate-pr-pipeline.respond.test.ts +47 -0
  38. package/src/components/generate-pr-pipeline.runners.test.ts +61 -0
  39. package/src/components/generate-pr-pipeline.stale-notes.test.ts +42 -0
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAAsF,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACpJ,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAiB9F,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,GAC9B;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,CAuEpI;AAoDD,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAMvH"}
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,EAYL,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,CA4FpI;AAoID,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAMvH"}
@@ -22,6 +22,9 @@
22
22
  * downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
23
23
  * and applies its dialect.
24
24
  *
25
+ * `options.bin` (#4607) is a product's command in place of `chant` in every
26
+ * job line: the wave jobs, the resume job and the plans job.
27
+ *
25
28
  * With `resume` on the spec (#3683), a second workflow, `<name>-resume.yml`,
26
29
  * runs `chant run resume --op <name>` on that schedule: it re-runs the failed
27
30
  * jobs of a run whose waiting wave has been approved since, with the job's
@@ -31,6 +34,8 @@
31
34
  * `pull-requests: read`, so a wave can read the merged pull request's
32
35
  * reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
33
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.
34
39
  *
35
40
  * With `notify` on the spec (#3761), the workflow maps each variable it names
36
41
  * from the repository secret of the same name, so a wave job can post to
@@ -49,10 +54,32 @@
49
54
  * checkout. Every later run step starts with an unset of the variables a
50
55
  * token is requested with, so the code the job runs holds no OIDC token
51
56
  * and cannot request one for another group's identity.
57
+ *
58
+ * With `control` on the spec (#4617), a `control-dispatch` job needs the
59
+ * last wave's jobs and runs after them unless the run was cancelled, so it
60
+ * runs after a failure too. It downloads every job's outcome file, adds
61
+ * them up with `chant run wave --waves-outcome` (through the product's
62
+ * bin) into `CHANT_CONTROL_OUTCOME`, and sends the control repo one
63
+ * dispatch carrying that outcome and the pushed commit. Only the dispatch
64
+ * step holds the control token: the `tokenSecret` secret, mapped into
65
+ * `CHANT_CONTROL_TOKEN` and handed to curl on its stdin
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.
52
76
  */
53
- import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesResumeCommand } from "@intentius/chant/op/op-waves";
77
+ import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesControlTarget, opWavesLastJobs, opWavesApprovalStatusCommand, opWavesPostsApprovalStatus, opWavesOutcomeCommand, opWavesRecordPlansCommand, opWavesResumeCommand, } from "@intentius/chant/op/op-waves";
78
+ import { CONTROL_OUTCOME_VAR, controlDispatchJob } from "@intentius/chant/control/dispatch";
54
79
  import { assertEnvironment, assertSetupSteps, CI_IMAGE, dirnameOf, environmentDoc, GITHUB_JOB_TOKEN, githubRunsOn, githubSecretEnv, idTokenAudienceEnv, nonEmpty, pushBranches, setupStepDoc, } from "@intentius/chant/ci-jobs";
55
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";
56
83
  import { checkoutStep, concurrency, DOWNLOAD_ARTIFACT, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs.js";
57
84
  import { emitOpPipelineYAML } from "./generate-op-pipeline.js";
58
85
  /** The default image: it carries git, which the runner needs to read the base commit. */
@@ -66,10 +93,14 @@ export function opWaveOutcomeArtifact(spec, jobName) {
66
93
  return `${spec.name}-${jobName}-outcome`;
67
94
  }
68
95
  /** The workflow document, before emission, and the jobs it holds. */
69
- export function buildGithubOpWavesDoc(spec, options) {
70
- const jobs = opWaveJobs(spec, options.specFile);
96
+ export function buildGithubOpWavesDoc(spec, options, dialect = {}) {
97
+ const jobs = opWaveJobs(spec, options.specFile, { bin: options.bin });
98
+ const forge = dialect.forge ?? "github";
99
+ const product = resolveProductWords(options.words ?? {}).product;
71
100
  const image = options.image ?? OP_WAVES_IMAGE;
72
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);
73
104
  for (const wave of spec.waves) {
74
105
  assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
75
106
  if (wave.environment)
@@ -88,7 +119,21 @@ export function buildGithubOpWavesDoc(spec, options) {
88
119
  const group = opWaveGroupOf(spec, job);
89
120
  // #4332: the job's cloud identities, traded before the checkout.
90
121
  const cloud = group?.cloud ?? wave.cloud;
91
- const steps = [...(cloud ? [tradeStep(cloud)] : []), checkoutStep(true)];
122
+ const environment = group?.environment ?? wave.environment;
123
+ const idTokens = group?.idTokens ?? wave.idTokens;
124
+ const idToken = idTokens !== undefined || cloud !== undefined;
125
+ const runner = group?.runner ?? wave.runner ?? spec.runner;
126
+ // #4624: the guard first, before the trade and the checkout.
127
+ const guard = guardStep({
128
+ forge,
129
+ job: opWaveJobLabel(spec.name, job),
130
+ product,
131
+ ...(environment && forge === "github" ? { environment: environment.name } : {}),
132
+ branches: pushBranches(spec.branches),
133
+ ...(runner !== undefined ? { runner } : {}),
134
+ idToken,
135
+ });
136
+ const steps = [guard, ...(cloud ? [tradeStep(cloud)] : []), checkoutStep(true)];
92
137
  if (job.kind === "share") {
93
138
  steps.push(downloadArtifactStep(`Download wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), dirnameOf(job.record)));
94
139
  }
@@ -105,11 +150,8 @@ export function buildGithubOpWavesDoc(spec, options) {
105
150
  steps.push(keepRecordStep(`Keep wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), job.record));
106
151
  }
107
152
  steps.push(keepRecordStep(`Keep the job's outcome`, opWaveOutcomeArtifact(spec, job.jobName), job.outcome));
108
- const environment = group?.environment ?? wave.environment;
109
- const idTokens = group?.idTokens ?? wave.idTokens;
110
- const idToken = idTokens !== undefined || cloud !== undefined;
111
153
  jobsDoc[job.jobName] = {
112
- "runs-on": githubRunsOn(group?.runner ?? wave.runner ?? spec.runner),
154
+ "runs-on": githubRunsOn(runner),
113
155
  ...(job.needs.length > 0 ? { needs: job.needs } : {}),
114
156
  // #4322: a wave's jobs run after a failure in the wave before; a share job runs only on its deciding job's success.
115
157
  ...(job.needs.length > 0 && job.kind !== "share" ? { if: "!cancelled()" } : {}),
@@ -121,6 +163,11 @@ export function buildGithubOpWavesDoc(spec, options) {
121
163
  steps,
122
164
  };
123
165
  }
166
+ // #4617: once the last wave ran, one job tells the control repo how the waves ended.
167
+ if (spec.control) {
168
+ const [name, job] = controlJob(spec, options, jobs, image, permissions);
169
+ jobsDoc[name] = job;
170
+ }
124
171
  // #3761: the notify variables come from the secrets of the same name.
125
172
  const env = { ...githubSecretEnv(notifyVariables(spec.notify)), ...(options.variables ?? {}) };
126
173
  const doc = {
@@ -135,15 +182,54 @@ export function buildGithubOpWavesDoc(spec, options) {
135
182
  doc,
136
183
  jobs,
137
184
  ...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}),
138
- ...(byReview ? { plansDoc: buildPlansDoc(spec, options) } : {}),
185
+ ...(statusBeforeMerge ? { plansDoc: buildPlansDoc(spec, options, forge) } : {}),
139
186
  };
140
187
  }
188
+ /**
189
+ * The control dispatch job (#4617): after the last wave, whatever it did,
190
+ * the waves' outcome read from every job's outcome file, then one dispatch
191
+ * carrying it. The pushed commit is the one every wave ran at.
192
+ */
193
+ function controlJob(spec, options, jobs, image, permissions) {
194
+ const control = spec.control;
195
+ const dispatch = controlDispatchJob("github", opWavesControlTarget(control), "${{ github.sha }}", {
196
+ ...(options.words ? { words: options.words } : {}),
197
+ ...(control.sentCheck ? { sentCheck: control.sentCheck } : {}),
198
+ ...(control.markSent ? { markSent: control.markSent } : {}),
199
+ outcome: true,
200
+ });
201
+ const marks = control.sentCheck !== undefined || control.markSent !== undefined;
202
+ const steps = [
203
+ checkoutStep(true),
204
+ { name: "Download the waves' outcomes", uses: DOWNLOAD_ARTIFACT, with: { path: opWaveOutcomeDir(spec) } },
205
+ ...(spec.waves[0]?.setup ?? []).map(setupStepDoc),
206
+ ...runSteps(options.beforeScript ?? []),
207
+ {
208
+ name: "Add up the waves' outcomes",
209
+ run: `outcome="$(${opWavesOutcomeCommand(options.specFile, options.bin).join(" ")})" || exit 1\necho "${CONTROL_OUTCOME_VAR}=$outcome" >> "$GITHUB_ENV"`,
210
+ },
211
+ { name: `Tell the control repo ${control.repo} how ${control.member}'s waves ended`, env: { ...dispatch.env, ...(marks ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}) }, run: dispatch.script },
212
+ ];
213
+ return [
214
+ dispatch.name,
215
+ {
216
+ "runs-on": githubRunsOn(spec.runner),
217
+ needs: opWavesLastJobs(jobs),
218
+ // The waves ran, whatever they did: a failed run is an outcome the control repo is told about.
219
+ if: "!cancelled()",
220
+ container: image,
221
+ // markSent sets a commit status; the rest reads the repository and the run's artifacts.
222
+ permissions: marks ? { ...permissions, statuses: "write" } : permissions,
223
+ steps,
224
+ },
225
+ ];
226
+ }
141
227
  /** The scheduled resume workflow (#3683). */
142
228
  function buildResumeDoc(spec, options) {
143
229
  const steps = [checkoutStep(), ...runSteps(options.beforeScript ?? [])];
144
230
  steps.push({
145
231
  name: `Resume the waves of ${spec.name} whose approval arrived`,
146
- run: opWavesResumeCommand(spec).join(" "),
232
+ run: opWavesResumeCommand(spec, options.bin).join(" "),
147
233
  env: { GITHUB_TOKEN: GITHUB_JOB_TOKEN },
148
234
  });
149
235
  return {
@@ -155,20 +241,53 @@ function buildResumeDoc(spec, options) {
155
241
  jobsDoc: { resume: { "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps } },
156
242
  };
157
243
  }
158
- /** The pull request workflow a `pr-review` wave needs (#3684): record the digests each head plans. */
159
- function buildPlansDoc(spec, options) {
244
+ /**
245
+ * The pull request workflow a `pr-review` wave needs (#3684): record the
246
+ * digests each head plans. The `approval-status` job after it (#4630) posts
247
+ * `<product>/approval` on the head, and runs alone on each review submitted
248
+ * or dismissed. It holds a read token that may write statuses and nothing
249
+ * else. A `pull-request` wave gets the same workflow: the record keeps its
250
+ * change set, and the status reads the pull request's own approval of it.
251
+ */
252
+ function buildPlansDoc(spec, options, forge) {
160
253
  const steps = [checkoutStep(true), ...(spec.waves[0]?.setup ?? []).map(setupStepDoc), ...runSteps(options.beforeScript ?? [])];
161
- steps.push({ name: "Plan every wave at the head and record its digests", run: ["chant", "run", "wave", "--spec", options.specFile, "--record-plans"].join(" ") });
254
+ steps.push({ name: "Plan every wave at the head and record its digests", run: opWavesRecordPlansCommand(options.specFile, options.bin).join(" ") });
255
+ const statusSteps = [checkoutStep(true), ...runSteps(options.beforeScript ?? [])];
256
+ statusSteps.push({
257
+ name: "Post the approval status on the head",
258
+ run: opWavesApprovalStatusCommand(options.specFile, forge, options.bin).join(" "),
259
+ env: {
260
+ GITHUB_TOKEN: GITHUB_JOB_TOKEN,
261
+ PR_NUMBER: "${{ github.event.pull_request.number }}",
262
+ HEAD_SHA: "${{ github.event.pull_request.head.sha }}",
263
+ BASE_SHA: "${{ github.event.pull_request.base.sha }}",
264
+ },
265
+ });
162
266
  return {
163
267
  name: `${spec.name}-plans`,
164
- on: { pull_request: { branches: pushBranches(spec.branches) } },
268
+ on: { pull_request: { branches: pushBranches(spec.branches) }, pull_request_review: { types: ["submitted", "dismissed"] } },
165
269
  ...nonEmpty("env", options.variables),
166
- concurrency: concurrency(spec.name + "-plans-${{ github.event.number }}", true),
270
+ concurrency: concurrency(spec.name + "-plans-${{ github.event_name }}-${{ github.event.pull_request.number }}", true),
167
271
  // contents: write pushes the record to chant/lifecycle.
168
272
  permissions: { contents: "write" },
169
- jobsDoc: { "record-plans": { "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps } },
273
+ jobsDoc: {
274
+ "record-plans": { if: "github.event_name == 'pull_request'", "runs-on": githubRunsOn(spec.runner), container: options.image ?? OP_WAVES_IMAGE, steps },
275
+ "approval-status": {
276
+ needs: ["record-plans"],
277
+ // On a review the record-plans job is skipped, and the status reads the plans an earlier push recorded.
278
+ if: "${{ always() && (needs.record-plans.result == 'success' || needs.record-plans.result == 'skipped') }}",
279
+ "runs-on": githubRunsOn(spec.runner),
280
+ container: options.image ?? OP_WAVES_IMAGE,
281
+ permissions: { contents: "read", "pull-requests": "read", statuses: "write" },
282
+ steps: statusSteps,
283
+ },
284
+ },
170
285
  };
171
286
  }
287
+ /** The apply-job guard step (#4624): the job's expectation in CHANT_JOB_GUARD, checked by `node -e` from `/`. */
288
+ function guardStep(input) {
289
+ return { name: "Check the job runs where the spec set it to run", env: { [JOB_GUARD_ENV]: JSON.stringify(jobGuardSpec(input)) }, run: jobGuardLine() };
290
+ }
172
291
  /** The step that trades the job's OIDC token for `cloud` (#4332), writing the credentials to GITHUB_ENV. */
173
292
  function tradeStep(cloud) {
174
293
  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;AAgCjG,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;AAgfD;;;;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;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;AAgoBD;;;;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,14 +58,19 @@
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 { 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, PR_LOCK_JOB, PR_UNLOCK_JOB, PR_UNLOCK_COMMAND, prLockCommand, prLoopWaveJobs, prLoopWithWaves, prEnvJobs, PR_ENV_REPORT_DIR, PR_ENV_SWEEP_CRON, PR_ENV_POST_JOB, } from "@intentius/chant/components/pr-pipeline";
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, PR_UNLOCK_COMMAND, 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
+ import { forgejoRepoLockLines, REPO_APPLY_GROUP } from "@intentius/chant/components/repo-lock";
65
66
  import { memberRepoPath } from "@intentius/chant/lexicon";
67
+ import { productStatusContext, resolveProductWords } from "@intentius/chant/ci-product-words";
68
+ import { controlDispatchJob, controlNoteCheckout } from "@intentius/chant/control/dispatch";
66
69
  import { afterEarlierWave, CACHE_RESTORE, CACHE_SAVE, CHECKOUT, checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
67
70
  /** The plan job's last step, whose outputs carry its report to the plan-note job (#4295). */
68
71
  const PLAN_STEP_ID = "plan";
72
+ /** The comment preview's checkout step (#4620), whose output names the head it checked out. */
73
+ const PREVIEW_HEAD_STEP_ID = "head";
69
74
  /** The output a gated apply wave hands the green commit it applies through, and the step that sets it (#4221). */
70
75
  const WAVE_GREEN_OUTPUT = "green-sha";
71
76
  const WAVE_STEP_ID = "apply";
@@ -203,10 +208,12 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
203
208
  });
204
209
  const withIdToken = (permissions) => (identity ? { ...permissions, "id-token": "write" } : permissions);
205
210
  const reportName = (job) => `chant-pr-${job.wave ? prApplyWaveJobName(job.wave) : job.jobName}`;
211
+ // #4552: on the repo grain a Forgejo apply takes the lock tag in its own step, before the command, since the runner does not queue on a concurrency group.
212
+ const repoLock = (job) => (loop.lockGrain === "repo" && loop.forge === "forgejo" && job.jobName !== "record" ? forgejoRepoLockLines() : []);
206
213
  const steps = (job, name) => [
207
214
  ...(identity ? [tradeStep(job.jobName === "record" ? identity.plan : identity.apply, job.jobName === "record" ? "plan" : "apply")] : []),
208
215
  checkoutStep(true),
209
- ...runSteps(prLoopSetup(image)),
216
+ ...runSteps(prLoopSetup(image, loop.engineSetup)),
210
217
  ...runSteps(beforeScript),
211
218
  ...(job.build ? [{ name: "Build the templates", run: job.build[0] }, { name: "Lint the project", run: job.build[1] }] : []),
212
219
  ...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
@@ -218,9 +225,9 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
218
225
  name,
219
226
  id: WAVE_STEP_ID,
220
227
  env: { ...job.run.env, ...(job.wave > 1 ? { GREEN_SHA: `\${{ needs.${prApplyWaveJobName(job.wave - 1)}.outputs.${WAVE_GREEN_OUTPUT} }}` } : {}) },
221
- run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), job.run.command].join("\n"),
228
+ run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), ...repoLock(job), job.run.command].join("\n"),
222
229
  }
223
- : { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), job.run.command].join("\n") },
230
+ : { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), ...repoLock(job), job.run.command].join("\n") },
224
231
  ...runSteps(extraScript),
225
232
  ...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
226
233
  {
@@ -239,7 +246,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
239
246
  "github.event_name == 'issue_comment'",
240
247
  // Forgejo's payload says so with is_pull (terragucci comment-apply.ts:129 at e6cb431).
241
248
  forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
242
- `startsWith(github.event.comment.body, '${APPLY_COMMENT}')`,
249
+ `startsWith(github.event.comment.body, '${prApplyComment(loop)}')`,
243
250
  ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
244
251
  ].join(" && ");
245
252
  const applyJob = (job, needs, name) => ({
@@ -248,7 +255,8 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
248
255
  "runs-on": GITHUB_RUNNER,
249
256
  ...(needs.length > 0 ? { needs } : {}),
250
257
  container: image,
251
- concurrency: concurrency(prApplyGroup(env, member?.name)),
258
+ // #4552: on the repo grain every apply of the repository shares one group.
259
+ concurrency: concurrency(loop.lockGrain === "repo" ? REPO_APPLY_GROUP : prApplyGroup(env, member?.name)),
252
260
  permissions: withIdToken({ contents: "write", "pull-requests": "write", statuses: "write" }),
253
261
  ...(job.wave ? { outputs: { [WAVE_GREEN_OUTPUT]: `\${{ steps.${WAVE_STEP_ID}.outputs.${WAVE_GREEN_OUTPUT} }}` } } : {}),
254
262
  steps: steps(job, name),
@@ -281,7 +289,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
281
289
  env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
282
290
  run: checkoutScript(true, true),
283
291
  },
284
- ...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
292
+ ...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
285
293
  ...(plan.build ? [headStep(plan.build[0], "Build the templates"), headStep(plan.build[1], "Lint the project")] : []),
286
294
  // The job's last step: a step after it would start beside whatever the change's code left running.
287
295
  {
@@ -291,14 +299,58 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
291
299
  run: [drop, ...handoverTrap(PR_LOOP_REPORT_DIR, forgeName), ...(plan.checkout ?? []), ...(plan.setup ?? []), plan.run.command, ...extraScript].join("\n"),
292
300
  },
293
301
  ];
294
- const post = prLoopPostJob(forgeName, env, member?.name);
302
+ // #4568: once the last apply on merge succeeded, one job tells the control repo this member applied.
303
+ const lastGreen = loop.control && !loop.applyBeforeMerge && greenJobs.length > 0 ? greenJobs[greenJobs.length - 1][0] : undefined;
304
+ const controlJobs = [];
305
+ if (loop.control && lastGreen) {
306
+ // A wave job hands on the commit it applied; a single apply job applies the green commit its trigger names.
307
+ const sha = loop.waveGate ? `\${{ needs.${lastGreen}.outputs.${WAVE_GREEN_OUTPUT} }}` : forgejo ? "${{ github.sha }}" : "${{ github.event.workflow_run.head_sha || github.sha }}";
308
+ const dispatch = controlDispatchJob(forgeName, loop.control.target, sha, {
309
+ ...(loop.words ? { words: loop.words } : {}),
310
+ ...(loop.control.sentCheck ? { sentCheck: loop.control.sentCheck } : {}),
311
+ ...(loop.control.markSent ? { markSent: loop.control.markSent } : {}),
312
+ });
313
+ controlJobs.push([
314
+ dispatch.name,
315
+ {
316
+ needs: [lastGreen],
317
+ if: `success() && (${onGreen})`,
318
+ "runs-on": GITHUB_RUNNER,
319
+ container: image,
320
+ permissions: loop.control.markSent ? { contents: "read", statuses: "write" } : { contents: "read" },
321
+ steps: [{ name: dispatch.stepName, ...(rooted ? {} : { "working-directory": "." }), env: { ...dispatch.env, ...(loop.control.sentCheck || loop.control.markSent ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}) }, run: dispatch.script }],
322
+ },
323
+ ]);
324
+ }
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
+ : {};
344
+ // #4619: with a control repo named, the note job reads its chant/lifecycle, read-only, so pr-post marks the cross-stack edges.
345
+ const controlRead = loop.control ? controlNoteCheckout(forgeName, loop.control.target, loop.words ?? {}) : undefined;
295
346
  const noteSteps = [
296
347
  // The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
297
348
  { name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}" } },
298
349
  ...runSteps(beforeScript),
299
350
  { name: "Take the plan report from the plan job", run: takeOverScript("plan", PR_LOOP_REPORT_DIR, forgeName) },
300
351
  { 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 } },
301
- { name: "Post the plan note and chant/plan", if: "always()", env: post.env, run: post.command },
352
+ ...(controlRead ? [{ name: controlRead.stepName, if: "always()", env: controlRead.env, run: controlRead.script }] : []),
353
+ { name: `Post the plan note and ${productStatusContext(resolveProductWords(loop.words), "plan")}`, if: "always()", env: post.env, run: post.command },
302
354
  ];
303
355
  // #4298: with a pull request environment the pull_request trigger fires on close too, and the plan skips that event.
304
356
  const prEnv = loop.prEnvironment ? prEnvGithubJobs(loop.forge ?? "github", image, beforeScript, extraScript, loop, member) : undefined;
@@ -339,6 +391,80 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
339
391
  },
340
392
  }
341
393
  : {};
394
+ // #4588: `/<product> lock` and `/<product> unlock` comments, at the target branch's commit, so none of the change's code runs.
395
+ const commentLocks = prCommentLockJobs(forgeName, loop);
396
+ const commentLockJobs = Object.fromEntries(commentLocks.map((job) => [
397
+ job.jobName,
398
+ {
399
+ if: [
400
+ "github.event_name == 'issue_comment'",
401
+ forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
402
+ `startsWith(github.event.comment.body, '${job.comment}')`,
403
+ ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
404
+ ].join(" && "),
405
+ "runs-on": GITHUB_RUNNER,
406
+ container: image,
407
+ permissions: { contents: "write", "pull-requests": "read", statuses: "write" },
408
+ steps: [
409
+ { name: "Check out the target branch's commit", uses: CHECKOUT, with: { "fetch-depth": 0 } },
410
+ ...runSteps(beforeScript),
411
+ { name: job.command === "lock" ? "Keep the locks the pull request's plan took" : "Release the pull request's locks", env: job.env, run: job.line },
412
+ ],
413
+ },
414
+ ]));
415
+ // #4620: `/<product> <plan word>` plans the pull request's head again with a read token; the job after it posts the note.
416
+ const preview = prCommentPreviewJob(forgeName, env, loop, member?.name);
417
+ const previewJobs = preview
418
+ ? {
419
+ [preview.jobName]: {
420
+ if: [
421
+ "github.event_name == 'issue_comment'",
422
+ forgejo ? "(github.event.issue.pull_request || github.event.is_pull)" : "github.event.issue.pull_request",
423
+ `startsWith(github.event.comment.body, '${preview.comment}')`,
424
+ ...(forgejo ? [] : ["contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)"]),
425
+ ].join(" && "),
426
+ "runs-on": GITHUB_RUNNER,
427
+ container: image,
428
+ permissions: withIdToken({ contents: "read" }),
429
+ outputs: { ...handoverOutputs(PLAN_STEP_ID, forgeName), [PREVIEW_HEAD_OUTPUT]: `\${{ steps.${PREVIEW_HEAD_STEP_ID}.outputs.${PREVIEW_HEAD_OUTPUT} }}` },
430
+ steps: [
431
+ ...(identity ? [tradeStep(identity.plan, "plan")] : []),
432
+ // The head is read here, before any of the change's code runs, so the post job names the commit this job planned.
433
+ {
434
+ name: "Check out the pull request's head",
435
+ id: PREVIEW_HEAD_STEP_ID,
436
+ ...(rooted ? {} : { "working-directory": "." }),
437
+ env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN, [PREVIEW_REF_ENV]: "ref" in preview.head ? preview.head.ref : "" },
438
+ run: [checkoutScript(true, true, { refEnv: PREVIEW_REF_ENV }), `echo "${PREVIEW_HEAD_OUTPUT}=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"`].join("\n"),
439
+ },
440
+ ...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
441
+ ...(preview.plan.build ? [headStep(preview.plan.build[0], "Build the templates"), headStep(preview.plan.build[1], "Lint the project")] : []),
442
+ {
443
+ name: "Preview the members this pull request reaches",
444
+ id: PLAN_STEP_ID,
445
+ env: preview.plan.run.env,
446
+ run: [drop, ...handoverTrap(PR_LOOP_REPORT_DIR, forgeName), preview.base, ...(preview.plan.setup ?? []), preview.plan.run.command, ...extraScript].join("\n"),
447
+ },
448
+ ],
449
+ },
450
+ [preview.noteJobName]: {
451
+ needs: [preview.jobName],
452
+ if: `always() && github.event_name == 'issue_comment' && (needs.${preview.jobName}.result == 'success' || needs.${preview.jobName}.result == 'failure')`,
453
+ "runs-on": GITHUB_RUNNER,
454
+ container: image,
455
+ permissions: { actions: "read", contents: "read", "pull-requests": "write", statuses: "write" },
456
+ steps: [
457
+ // The target branch's commit, which an issue_comment run checks out: none of the change's code is here.
458
+ { name: "Check out the target branch's commit", uses: CHECKOUT },
459
+ ...runSteps(beforeScript),
460
+ { name: "Take the preview report from the preview job", run: takeOverScript(preview.jobName, PR_LOOP_REPORT_DIR, forgeName) },
461
+ { name: "Keep the preview report", if: "always()", uses: UPLOAD_ARTIFACT, with: { name: `chant-pr-${preview.jobName}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } },
462
+ ...(controlRead ? [{ name: controlRead.stepName, if: "always()", env: controlRead.env, run: controlRead.script }] : []),
463
+ { name: `Post the preview note and ${productStatusContext(resolveProductWords(loop.words), "plan")}`, if: "always()", env: preview.post.env, run: preview.post.command },
464
+ ],
465
+ },
466
+ }
467
+ : {};
342
468
  const jobsDoc = {
343
469
  plan: {
344
470
  if: closes ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
@@ -358,7 +484,11 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
358
484
  steps: noteSteps,
359
485
  },
360
486
  ...lockJobs,
487
+ ...commentLockJobs,
488
+ ...previewJobs,
489
+ ...staleJobs,
361
490
  ...Object.fromEntries(applyJobs),
491
+ ...Object.fromEntries(controlJobs),
362
492
  ...(prEnv ? prEnv.jobs : {}),
363
493
  };
364
494
  const applyNames = applyJobs.map(([name]) => name);
@@ -373,9 +503,15 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
373
503
  on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
374
504
  on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
375
505
  }
376
- // #4301: a /chant apply comment starts the apply before merge.
377
- if (loop.applyBeforeMerge)
506
+ // #4301: a /chant apply comment starts the apply before merge. #4588: and a lock or unlock comment its job.
507
+ if (loop.applyBeforeMerge || commentLocks.length > 0 || preview)
378
508
  on.issue_comment = { types: ["created"] };
509
+ // #4625: each job on its stage's runner, the plan side's, the apply side's, or the default.
510
+ if (loop.runners) {
511
+ for (const [name, job] of Object.entries(jobsDoc)) {
512
+ job["runs-on"] = githubRunsOn(prLoopRunner(loop, prLoopJobStage(name)));
513
+ }
514
+ }
379
515
  return {
380
516
  name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
381
517
  environment: env,
@@ -383,16 +519,24 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
383
519
  ...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
384
520
  env: { ...options.variables, CHANT_ENV: env },
385
521
  jobsDoc,
386
- stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...applyNames, ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
522
+ 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"] : [])],
387
523
  jobs: [
388
524
  { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
389
525
  { jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
526
+ ...(stale ? [{ jobName: PR_STALE_JOB, component: "pull request stale notes", stage: PR_STALE_JOB, needs: [] }] : []),
390
527
  ...(locks
391
528
  ? [
392
529
  { jobName: PR_LOCK_JOB, component: "pull request lock", stage: PR_LOCK_JOB, needs: ["plan"] },
393
530
  { jobName: PR_UNLOCK_JOB, component: "pull request unlock", stage: PR_UNLOCK_JOB, needs: [] },
394
531
  ]
395
532
  : []),
533
+ ...commentLocks.map((j) => ({ jobName: j.jobName, component: `pull request ${j.command} comment`, stage: j.jobName, needs: [] })),
534
+ ...(preview
535
+ ? [
536
+ { jobName: preview.jobName, component: "pull request preview comment", stage: preview.jobName, needs: [] },
537
+ { jobName: preview.noteJobName, component: "pull request preview comment note", stage: preview.noteJobName, needs: [preview.jobName] },
538
+ ]
539
+ : []),
396
540
  ...applyNames.map((name, i) => ({
397
541
  jobName: name,
398
542
  component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
@@ -400,6 +544,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
400
544
  // The record runs on a push and the apply on a green commit, so only the waves need each other.
401
545
  needs: i > 1 ? [applyNames[i - 1]] : [],
402
546
  })),
547
+ ...controlJobs.map(([name]) => ({ jobName: name, component: "control repo dispatch", stage: name, needs: [lastGreen] })),
403
548
  ...(prEnv ? prEnv.listed : []),
404
549
  ],
405
550
  };
@@ -451,7 +596,7 @@ function prEnvGithubJobs(forge, image, beforeScript, extraScript, loop, member)
451
596
  env: { [CHECKOUT_TOKEN_ENV]: GITHUB_JOB_TOKEN },
452
597
  run: checkoutScript(true, true),
453
598
  },
454
- ...[...prLoopSetup(image), ...beforeScript].map((line) => headStep(line)),
599
+ ...[...prLoopSetup(image, loop.engineSetup), ...beforeScript].map((line) => headStep(line)),
455
600
  ...(up.build ? [headStep(up.build[0], "Build the templates", { CHANT_ENV: up.env.CHANT_ENV }), headStep(up.build[1], "Lint the project")] : []),
456
601
  // The job's last step: a step after it would start beside whatever the change's code left running.
457
602
  {
@@ -487,7 +632,7 @@ function prEnvGithubJobs(forge, image, beforeScript, extraScript, loop, member)
487
632
  steps: [
488
633
  ...tradeStep,
489
634
  { uses: CHECKOUT, with: { "fetch-depth": 0, ...(ref ? { ref } : {}) } },
490
- ...runSteps(prLoopSetup(image)),
635
+ ...runSteps(prLoopSetup(image, loop.engineSetup)),
491
636
  ...runSteps(beforeScript),
492
637
  { name, env: j.env, run: j.command },
493
638
  ...runSteps(extraScript),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4e53c568b6c55a08741d8b4f83c7965d1b0d710e540b4efb66ac88735de02380",
4
+ "manifest.json": "16478bfa3a3a6b6a573c54713b9d6edcef575a934df2b6a2daba331607d0d132",
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": "63d4e69414d1e6bfbf5968e7fcdcc1930ecca6d07b3915bdd013f9d39f77d2d6"
81
+ "composite": "9eb8c4e72039a3e7c014959fe55b6846823a49f4097e41c3d4b2b9baebe12ad4"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.124.54",
3
+ "version": "0.124.56",
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.54",
3
+ "version": "0.124.56",
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.54"
64
+ "@intentius/chant": "^0.124.56"
65
65
  }
66
66
  }