@intentius/chant-lexicon-github 0.124.32 → 0.124.34

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.
@@ -35,6 +35,13 @@ export declare function jobTokenEnv(gh?: boolean): Record<string, string>;
35
35
  * names none).
36
36
  */
37
37
  export declare function onTrigger(trigger: OpTrigger): Record<string, unknown>;
38
+ /**
39
+ * A push Op's concurrency group (#4169): one per commit under `build:
40
+ * "every"`, so no push cancels or replaces another's run; one per ref under
41
+ * `newest`, so the newest push waits and each build request's tag runs on its
42
+ * own; and the Op's own name otherwise, one run at a time.
43
+ */
44
+ export declare function opConcurrencyGroup(jobName: string, trigger: OpTrigger): string;
38
45
  /**
39
46
  * A workflow document before emission. Every generator's document is one of
40
47
  * these; each leaves out the sections it has no use for.
@@ -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,CASrE;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,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"}
@@ -8,7 +8,7 @@
8
8
  * core's, in `@intentius/chant/ci-jobs`.
9
9
  */
10
10
  import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
11
- import { GITHUB_JOB_TOKEN, pushBranches } from "@intentius/chant/ci-jobs";
11
+ import { BUILD_REQUEST_TAGS, GITHUB_JOB_TOKEN, pushBranches } from "@intentius/chant/ci-jobs";
12
12
  import { actionRef } from "../action-pins.js";
13
13
  /** The artifact actions every generated job uses, at the major the generators have always pinned. */
14
14
  export const UPLOAD_ARTIFACT = "actions/upload-artifact@v4";
@@ -60,9 +60,23 @@ export function onTrigger(trigger) {
60
60
  case "pull_request":
61
61
  return { pull_request: trigger.branches && trigger.branches.length > 0 ? { branches: trigger.branches } : {} };
62
62
  case "push":
63
- return { push: { branches: pushBranches(trigger.branches) } };
63
+ // #4169: a newest build runs at each build request's tag too, and by hand.
64
+ return trigger.build === "newest"
65
+ ? { push: { branches: pushBranches(trigger.branches), tags: [BUILD_REQUEST_TAGS] }, workflow_dispatch: {} }
66
+ : { push: { branches: pushBranches(trigger.branches) } };
64
67
  }
65
68
  }
69
+ /**
70
+ * A push Op's concurrency group (#4169): one per commit under `build:
71
+ * "every"`, so no push cancels or replaces another's run; one per ref under
72
+ * `newest`, so the newest push waits and each build request's tag runs on its
73
+ * own; and the Op's own name otherwise, one run at a time.
74
+ */
75
+ export function opConcurrencyGroup(jobName, trigger) {
76
+ if (trigger.kind !== "push" || !trigger.build)
77
+ return jobName;
78
+ return trigger.build === "every" ? `${jobName}-\${{ github.sha }}` : `${jobName}-\${{ github.ref }}`;
79
+ }
66
80
  /** Emit a workflow document as YAML, its sections in GitHub's customary order. */
67
81
  export function emitWorkflowYAML(doc) {
68
82
  const sections = [];
@@ -72,7 +72,7 @@
72
72
  import { readFileSync } from "node:fs";
73
73
  import { assertEnvironment, assertSetupSteps, CI_IMAGE, ciJobName, environmentDoc, fillCommand, GITHUB_JOB_TOKEN, GITHUB_RUNNER, mergePermissions, nonEmpty, permissionsFor, setupStepDoc, } from "@intentius/chant/ci-jobs";
74
74
  import { memberPathFilter, resolveOpTrigger } from "@intentius/chant/lexicon";
75
- import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, runSteps } from "./ci-jobs.js";
75
+ import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, opConcurrencyGroup, runSteps } from "./ci-jobs.js";
76
76
  // The setup, environment and permission checks are shared with the Op waves
77
77
  // generator and the forgejo lexicon (#3765); they stay importable from here.
78
78
  export { assertEnvironment, assertSetupSteps, mergePermissions };
@@ -339,8 +339,8 @@ export function buildGithubOpPipelineDocs(ops, options = {}) {
339
339
  on: onTrigger(trigger),
340
340
  ...nonEmpty("env", options.variables),
341
341
  // One run at a time per Op — a slow audit must not overlap its own next
342
- // scheduled trigger.
343
- concurrency: concurrency(jobName),
342
+ // scheduled trigger. A push Op's `build` (#4169) widens the group.
343
+ concurrency: concurrency(opConcurrencyGroup(jobName, trigger)),
344
344
  permissions: mergePermissions(spec.name, permissionsFor(findingMode, trigger), spec.permissions ?? {}, trigger),
345
345
  jobsDoc: {
346
346
  [jobName]: {
@@ -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;AAgBjG,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;AAkMD;;;;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;AAiBjG,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;AAuMD;;;;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"}
@@ -59,7 +59,7 @@ 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
61
  import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
62
- import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
62
+ import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
63
63
  import { memberRepoPath } from "@intentius/chant/lexicon";
64
64
  import { checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
65
65
  /** The workflow artifact the gated-wave jobs hand their attempt record through, one per wave. */
@@ -105,7 +105,10 @@ function gatedWaveGithubDoc(components, env, image, beforeScript, extraScript, o
105
105
  }
106
106
  /**
107
107
  * The pull-request workflow (#3183): `plan` on each pull request into the
108
- * target branch, `apply` on each push to it. The plan job can comment and set
108
+ * target branch, and on each push to it `record` (#4168), or `apply` with
109
+ * `applyOnMerge`. The record job plans on the merged commit and writes the
110
+ * merge record, applying nothing, so it pushes to `chant/lifecycle` and runs
111
+ * one at a time per environment as the apply does. The plan job can comment and set
109
112
  * statuses but cannot push, since it runs the pull request's code; the apply
110
113
  * job can push, to record a pending fact on `chant/lifecycle`, and runs one
111
114
  * at a time per environment. Both keep their report as an artifact, and both
@@ -133,7 +136,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
133
136
  const branch = loop.branch ?? "main";
134
137
  const member = options.member;
135
138
  const rooted = !member || member.dir === "." || member.dir === "";
136
- const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
139
+ const [plan, apply, record] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
137
140
  const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
138
141
  // The apply's attempt record (#3543) lives in the cache under the pushed
139
142
  // commit. Cache keys cannot be overwritten, so each attempt saves under
@@ -186,7 +189,9 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
186
189
  prApplyWaveJobName(job.wave),
187
190
  applyJob(job, job.wave > 1 ? [prApplyWaveJobName(job.wave - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
188
191
  ])
189
- : [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]];
192
+ : loop.applyOnMerge
193
+ ? [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]]
194
+ : [["record", { ...applyJob(record, [], "Record the plan on the merged commit; nothing applies"), concurrency: concurrency(prRecordGroup(env, member?.name)) }]];
190
195
  const jobsDoc = {
191
196
  plan: {
192
197
  if: "github.event_name == 'pull_request'",
@@ -210,7 +215,7 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
210
215
  { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
211
216
  ...applyNames.map((name, i) => ({
212
217
  jobName: name,
213
- component: loop.waveGate ? `pull request apply, wave ${i + 1}` : "pull request apply",
218
+ component: loop.waveGate ? `pull request apply, wave ${i + 1}` : loop.applyOnMerge ? "pull request apply" : "merge record",
214
219
  stage: name,
215
220
  needs: i > 0 ? [applyNames[i - 1]] : [],
216
221
  })),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "1ac5c53834d0d9e3f294436372ef80c67d915538899782e8188f48379b1c884d",
4
+ "manifest.json": "5f90d39ad2f505ce370fa896da370421567132336d3404820974cd74a1dfe68f",
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": "fb2f379e45edbc503f525d77958b302d4f546273f1b09ba0f2d5e7d0cdde1e2f"
81
+ "composite": "58ec911a741a048aafa2e77b71308b47d28436478805c484a48867361d3220d0"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.124.32",
3
+ "version": "0.124.34",
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.32",
3
+ "version": "0.124.34",
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.32"
64
+ "@intentius/chant": "^0.124.34"
65
65
  }
66
66
  }
@@ -0,0 +1,74 @@
1
+ name: chant-pr-prod
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ push:
8
+ branches:
9
+ - main
10
+
11
+ env:
12
+ CHANT_ENV: prod
13
+
14
+ jobs:
15
+ plan:
16
+ if: github.event_name == 'pull_request'
17
+ runs-on: ubuntu-latest
18
+ container: node:22
19
+ permissions:
20
+ contents: read
21
+ pull-requests: write
22
+ statuses: write
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ with:
26
+ fetch-depth: 0
27
+ - 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)
28
+ - name: Plan the members this pull request reaches
29
+ env:
30
+ BASE_SHA: '${{ github.event.pull_request.base.sha }}'
31
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
32
+ GITHUB_TOKEN: '${{ github.token }}'
33
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
34
+ run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
35
+ - name: Keep the plan report
36
+ if: always()
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: chant-pr-plan
40
+ path: .chant/pr
41
+ if-no-files-found: ignore
42
+ include-hidden-files: true
43
+ record:
44
+ if: github.event_name == 'push'
45
+ runs-on: ubuntu-latest
46
+ container: node:22
47
+ concurrency:
48
+ group: chant-record-prod
49
+ cancel-in-progress: false
50
+ permissions:
51
+ contents: write
52
+ pull-requests: write
53
+ statuses: write
54
+ steps:
55
+ - uses: actions/checkout@v7
56
+ with:
57
+ fetch-depth: 0
58
+ - 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)
59
+ - name: Record the plan on the merged commit; nothing applies
60
+ env:
61
+ BASE_SHA: '${{ github.event.before }}'
62
+ GITHUB_TOKEN: '${{ github.token }}'
63
+ CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
64
+ run: |
65
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
66
+ chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
67
+ - name: Keep the record report
68
+ if: always()
69
+ uses: actions/upload-artifact@v4
70
+ with:
71
+ name: chant-pr-record
72
+ path: .chant/pr
73
+ if-no-files-found: ignore
74
+ include-hidden-files: true
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
12
- import { GITHUB_JOB_TOKEN, pushBranches } from "@intentius/chant/ci-jobs";
12
+ import { BUILD_REQUEST_TAGS, GITHUB_JOB_TOKEN, pushBranches } from "@intentius/chant/ci-jobs";
13
13
  import type { OpTrigger } from "@intentius/chant/lexicon";
14
14
  import { actionRef } from "../action-pins";
15
15
 
@@ -70,10 +70,24 @@ export function onTrigger(trigger: OpTrigger): Record<string, unknown> {
70
70
  case "pull_request":
71
71
  return { pull_request: trigger.branches && trigger.branches.length > 0 ? { branches: trigger.branches } : {} };
72
72
  case "push":
73
- return { push: { branches: pushBranches(trigger.branches) } };
73
+ // #4169: a newest build runs at each build request's tag too, and by hand.
74
+ return trigger.build === "newest"
75
+ ? { push: { branches: pushBranches(trigger.branches), tags: [BUILD_REQUEST_TAGS] }, workflow_dispatch: {} }
76
+ : { push: { branches: pushBranches(trigger.branches) } };
74
77
  }
75
78
  }
76
79
 
80
+ /**
81
+ * A push Op's concurrency group (#4169): one per commit under `build:
82
+ * "every"`, so no push cancels or replaces another's run; one per ref under
83
+ * `newest`, so the newest push waits and each build request's tag runs on its
84
+ * own; and the Op's own name otherwise, one run at a time.
85
+ */
86
+ export function opConcurrencyGroup(jobName: string, trigger: OpTrigger): string {
87
+ if (trigger.kind !== "push" || !trigger.build) return jobName;
88
+ return trigger.build === "every" ? `${jobName}-\${{ github.sha }}` : `${jobName}-\${{ github.ref }}`;
89
+ }
90
+
77
91
  /**
78
92
  * A workflow document before emission. Every generator's document is one of
79
93
  * these; each leaves out the sections it has no use for.
@@ -208,6 +208,17 @@ describe("generateGithubOpPipeline: trigger kinds (#2084)", () => {
208
208
  expect(result.jobs[0].trigger).toEqual({ kind: "push", branches: ["release"] });
209
209
  });
210
210
 
211
+ test("a push trigger's build (#4169): newest runs at build request tags and by hand, one group per ref; every, one group per commit", () => {
212
+ const newest = parseFile(generateGithubOpPipeline([{ name: "suites", trigger: { kind: "push", build: "newest" } }]).files[0].yaml);
213
+ expect(newest.on).toEqual({ push: { branches: ["main"], tags: ["ci/build/**"] }, workflow_dispatch: {} });
214
+ expect(newest.concurrency).toEqual({ group: "suites-${{ github.ref }}", "cancel-in-progress": false });
215
+ const every = parseFile(generateGithubOpPipeline([{ name: "suites", trigger: { kind: "push", build: "every" } }]).files[0].yaml);
216
+ expect(every.on).toEqual({ push: { branches: ["main"] } });
217
+ expect(every.concurrency).toEqual({ group: "suites-${{ github.sha }}", "cancel-in-progress": false });
218
+ const plain = parseFile(generateGithubOpPipeline([{ name: "suites", trigger: { kind: "push" } }]).files[0].yaml);
219
+ expect(plain.concurrency).toEqual({ group: "suites", "cancel-in-progress": false });
220
+ });
221
+
211
222
  test("push trigger defaults to the repository default branch (main) when branches is omitted", () => {
212
223
  const result = generateGithubOpPipeline([{ name: "tf-apply", trigger: { kind: "push" } }]);
213
224
  const doc = parseFile(result.files[0].yaml);
@@ -95,7 +95,7 @@ import type {
95
95
  PipelineMember,
96
96
  ScheduledOpSpec,
97
97
  } from "@intentius/chant/lexicon";
98
- import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, runSteps } from "./ci-jobs";
98
+ import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, opConcurrencyGroup, runSteps } from "./ci-jobs";
99
99
 
100
100
  export type { GenerateGithubOpOptions, GenerateGithubOpResult };
101
101
 
@@ -455,8 +455,8 @@ export function buildGithubOpPipelineDocs(
455
455
  on: onTrigger(trigger),
456
456
  ...nonEmpty("env", options.variables),
457
457
  // One run at a time per Op — a slow audit must not overlap its own next
458
- // scheduled trigger.
459
- concurrency: concurrency(jobName),
458
+ // scheduled trigger. A push Op's `build` (#4169) widens the group.
459
+ concurrency: concurrency(opConcurrencyGroup(jobName, trigger)),
460
460
  permissions: mergePermissions(
461
461
  spec.name,
462
462
  permissionsFor(findingMode, trigger),
@@ -64,6 +64,7 @@ import {
64
64
  PR_APPLY_RECORD,
65
65
  PR_LOOP_REPORT_DIR,
66
66
  prApplyGroup,
67
+ prRecordGroup,
67
68
  prApplyRecordKey,
68
69
  prApplyWaveJobName,
69
70
  prLoopJobs,
@@ -171,7 +172,10 @@ function gatedWaveGithubDoc(
171
172
 
172
173
  /**
173
174
  * The pull-request workflow (#3183): `plan` on each pull request into the
174
- * target branch, `apply` on each push to it. The plan job can comment and set
175
+ * target branch, and on each push to it `record` (#4168), or `apply` with
176
+ * `applyOnMerge`. The record job plans on the merged commit and writes the
177
+ * merge record, applying nothing, so it pushes to `chant/lifecycle` and runs
178
+ * one at a time per environment as the apply does. The plan job can comment and set
175
179
  * statuses but cannot push, since it runs the pull request's code; the apply
176
180
  * job can push, to record a pending fact on `chant/lifecycle`, and runs one
177
181
  * at a time per environment. Both keep their report as an artifact, and both
@@ -206,7 +210,7 @@ function prLoopGithubDoc(
206
210
  const branch = loop.branch ?? "main";
207
211
  const member = options.member;
208
212
  const rooted = !member || member.dir === "." || member.dir === "";
209
- const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
213
+ const [plan, apply, record] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
210
214
  const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
211
215
  // The apply's attempt record (#3543) lives in the cache under the pushed
212
216
  // commit. Cache keys cannot be overwritten, so each attempt saves under
@@ -259,7 +263,9 @@ function prLoopGithubDoc(
259
263
  prApplyWaveJobName(job.wave!),
260
264
  applyJob(job, job.wave! > 1 ? [prApplyWaveJobName(job.wave! - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
261
265
  ])
262
- : [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]];
266
+ : loop.applyOnMerge
267
+ ? [["apply", applyJob(apply, [], "Apply the plan a reviewer approved")]]
268
+ : [["record", { ...applyJob(record, [], "Record the plan on the merged commit; nothing applies"), concurrency: concurrency(prRecordGroup(env, member?.name)) }]];
263
269
  const jobsDoc: Record<string, unknown> = {
264
270
  plan: {
265
271
  if: "github.event_name == 'pull_request'",
@@ -283,7 +289,7 @@ function prLoopGithubDoc(
283
289
  { jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
284
290
  ...applyNames.map((name, i) => ({
285
291
  jobName: name,
286
- component: loop.waveGate ? `pull request apply, wave ${i + 1}` : "pull request apply",
292
+ component: loop.waveGate ? `pull request apply, wave ${i + 1}` : loop.applyOnMerge ? "pull request apply" : "merge record",
287
293
  stage: name,
288
294
  needs: i > 0 ? [applyNames[i - 1]] : [],
289
295
  })),
@@ -46,7 +46,7 @@ interface Job {
46
46
  }
47
47
 
48
48
  describe("the GitHub pull-request workflow", () => {
49
- const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} });
49
+ const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
50
50
  const doc = parseYAML(result.yaml) as { name: string; on: Record<string, unknown>; jobs: Record<string, Job> };
51
51
 
52
52
  test("matches its golden", () => {
@@ -72,8 +72,8 @@ describe("the GitHub pull-request workflow", () => {
72
72
  expect(doc.jobs.plan.permissions).toEqual({ contents: "read", "pull-requests": "write", statuses: "write" });
73
73
 
74
74
  const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
75
- // ci run builds pr-apply with --require-review unless told --no-require-review.
76
- expect(apply.run).not.toContain("--no-require-review");
75
+ // #4168: ci run builds pr-apply without --require-review unless the pipeline asks for it.
76
+ expect(apply.run).not.toContain("--require-review");
77
77
  expect(apply.run).not.toContain("--no-push");
78
78
  expect(apply.env).toMatchObject({ BASE_SHA: "${{ github.event.before }}", CHANT_RUNNER_KEY: "${{ secrets.CHANT_RUNNER_KEY }}" });
79
79
  expect(doc.jobs.apply.permissions.contents).toBe("write");
@@ -89,7 +89,7 @@ describe("the GitHub pull-request workflow", () => {
89
89
  expect(lines[0]).toContain('[ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]');
90
90
  expect(lines[0]).toContain('git merge-base FETCH_HEAD "$GITHUB_SHA"');
91
91
  expect(lines[1]).toMatch(/^chant ci run apply --in-place --base "\$BASE_SHA"/);
92
- const trunk = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { branch: "trunk" } }).yaml) as { jobs: Record<string, Job> };
92
+ const trunk = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml) as { jobs: Record<string, Job> };
93
93
  expect(trunk.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!.run).toContain('git fetch origin "trunk"');
94
94
  const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
95
95
  expect(plan.run).not.toContain("0000000000000000000000000000000000000000");
@@ -121,27 +121,27 @@ describe("the GitHub pull-request workflow", () => {
121
121
 
122
122
  test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
123
123
  expect(doc.jobs.plan.steps[1].run).toContain("install-opentofu");
124
- const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
124
+ const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true } }).yaml;
125
125
  expect(own).toContain("container: example/ci:1");
126
126
  expect(own).not.toContain("install-opentofu");
127
127
  });
128
128
 
129
129
  test("the gate, branch and review requirement are options", () => {
130
- const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { gate: "infra-apply", branch: "trunk", requireReview: false } }).yaml;
130
+ const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, gate: "infra-apply", branch: "trunk", requireReview: true } }).yaml;
131
131
  expect(yaml).toContain("--gate infra-apply");
132
132
  expect(yaml).toContain("- trunk");
133
- expect(yaml).toContain("--no-require-review");
133
+ expect(yaml).toContain("ci run apply --in-place --base \"$BASE_SHA\" --env prod --gate infra-apply --forge github --branch \"trunk\" --require-review");
134
134
  expect(yaml).toContain('--branch "trunk"');
135
135
  });
136
136
 
137
137
  test("refuses a promote job beside it, and a canary or a fixed base beside --wave-gate (#4003)", () => {
138
- expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
139
- expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
140
- expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, gatedWaves: { gate: "g", base: "HEAD~1" } })).toThrow(/takes no --base/);
138
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, promoteTo: "prod" })).toThrow(/pull-request pipeline has no promote job/);
139
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", canary: ["net"] } })).toThrow(/takes no --canary/);
140
+ expect(() => generateGithubPipeline(ESTATE, { prLoop: { applyOnMerge: true }, gatedWaves: { gate: "g", base: "HEAD~1" } })).toThrow(/takes no --base/);
141
141
  });
142
142
 
143
143
  test("events and reportUrl stream the run's events into the report artifact and copy the reports out (#4007)", () => {
144
- const loud = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { events: true, reportUrl: "s3://chant-reports/acme" } }).yaml) as { jobs: Record<string, Job> };
144
+ const loud = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, events: true, reportUrl: "s3://chant-reports/acme" } }).yaml) as { jobs: Record<string, Job> };
145
145
  for (const job of ["plan", "apply"]) {
146
146
  const run = loud.jobs[job].steps.find((s) => s.run?.includes("chant ci run"))!;
147
147
  expect(run.run).toContain('--events-json --report-url "s3://chant-reports/acme"');
@@ -154,7 +154,7 @@ describe("the GitHub pull-request workflow", () => {
154
154
 
155
155
  test("inside a workspace member: runs in the member's directory and keeps its own gate, note and apply group (#3465)", () => {
156
156
  const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr.network.prod.yml" };
157
- const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member });
157
+ const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member });
158
158
  const mdoc = parseYAML(scoped.yaml) as {
159
159
  name: string;
160
160
  on: Record<string, unknown>;
@@ -192,7 +192,7 @@ describe("the GitHub pull-request workflow", () => {
192
192
  });
193
193
 
194
194
  test("a member at the workspace root keeps the root's paths but its own names", () => {
195
- const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "root", dir: "." } }).yaml;
195
+ const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, member: { name: "root", dir: "." } }).yaml;
196
196
  const mdoc = parseYAML(yaml) as { name: string; defaults?: unknown; jobs: Record<string, Job> };
197
197
  expect(mdoc.name).toBe("chant-pr.root.prod");
198
198
  expect(mdoc.defaults).toBeUndefined();
@@ -213,7 +213,7 @@ describe("the GitHub pull-request workflow", () => {
213
213
  });
214
214
 
215
215
  describe("the github pull-request pipeline with build (#4088)", () => {
216
- const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { build: true } });
216
+ const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, build: true } });
217
217
  const doc = parseYAML(result.yaml) as Record<string, any>;
218
218
  const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-build.github.golden.yml");
219
219
 
@@ -231,14 +231,60 @@ describe("the github pull-request pipeline with build (#4088)", () => {
231
231
  });
232
232
 
233
233
  test("buildCommand replaces the build line and turns the option on", () => {
234
- const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { buildCommand: "npm run build" } }).yaml;
234
+ const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, buildCommand: "npm run build" } }).yaml;
235
235
  expect(yaml).toContain("npm run build");
236
236
  expect(yaml).not.toContain("chant build --env");
237
237
  expect(yaml).toContain("chant lint");
238
238
  });
239
239
 
240
240
  test("the default output carries neither line", () => {
241
- const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml;
241
+ const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } }).yaml;
242
242
  expect(plain).not.toContain("chant lint");
243
243
  });
244
244
  });
245
+
246
+ describe("the GitHub pull-request workflow records on merge by default (#4168)", () => {
247
+ const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} });
248
+ const doc = parseYAML(result.yaml) as { jobs: Record<string, Job> };
249
+ const golden = join(import.meta.dirname, "__fixtures__", "pr-loop-record.github.golden.yml");
250
+
251
+ test("matches its golden", () => {
252
+ if (process.env.UPDATE_GOLDEN) writeFileSync(golden, result.yaml);
253
+ expect(result.yaml).toBe(readFileSync(golden, "utf-8"));
254
+ });
255
+
256
+ test("a push runs the record job, which applies nothing and keeps no resume record", () => {
257
+ expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "record"]);
258
+ expect(doc.jobs.apply).toBeUndefined();
259
+ expect(doc.jobs.record.if).toBe("github.event_name == 'push'");
260
+ const run = doc.jobs.record.steps.find((s) => s.run?.includes("ci run record"))!;
261
+ expect(run.run!.trimEnd().split("\n")).toEqual([githubBaseFallback(), 'chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github']);
262
+ expect(doc.jobs.record.steps.some((s) => s.uses?.startsWith("actions/cache"))).toBe(false);
263
+ // It writes the merge record and the change set to chant/lifecycle, one at a time per environment.
264
+ expect(doc.jobs.record.permissions.contents).toBe("write");
265
+ expect(doc.jobs.record.concurrency).toEqual({ group: "chant-record-prod", "cancel-in-progress": false });
266
+ expect(result.yaml).not.toContain("ci run apply");
267
+ });
268
+
269
+ test("a gate per wave gates the apply, so it needs applyOnMerge", () => {
270
+ expect(() => generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } })).toThrow(/needs --apply-on-merge/);
271
+ });
272
+
273
+ test("the record job runs pr-record with the apply's flags", () => {
274
+ const [, , record] = prLoopJobs("github", "prod", { events: true, reportUrl: "s3://r/acme", waveGate: "w", requireReview: true, branch: "trunk" });
275
+ expect(record.jobName).toBe("record");
276
+ expect(record.command).toBe('chant components pr-record --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --events-json --report-url "s3://r/acme"');
277
+ expect(record.run.command).toBe('chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --branch "trunk" --events-json --report-url "s3://r/acme"');
278
+ });
279
+
280
+ test.skipIf(!hasActionlint())("passes actionlint", () => {
281
+ const dir = mkdtempSync(join(tmpdir(), "chant-pr-record-"));
282
+ try {
283
+ mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
284
+ writeFileSync(join(dir, ".github", "workflows", "chant-pr.yml"), result.yaml);
285
+ execFileSync("actionlint", [join(dir, ".github", "workflows", "chant-pr.yml")], { cwd: dir, stdio: "pipe" });
286
+ } finally {
287
+ rmSync(dir, { recursive: true, force: true });
288
+ }
289
+ });
290
+ });
@@ -51,7 +51,7 @@ function hasActionlint(): boolean {
51
51
  }
52
52
 
53
53
  describe("the GitHub pull-request workflow with a gate per wave (#4003)", () => {
54
- const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, gatedWaves: { gate: "release" } });
54
+ const result = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true }, gatedWaves: { gate: "release" } });
55
55
  const doc = parseYAML(result.yaml) as { jobs: Record<string, Job> };
56
56
 
57
57
  test("matches its golden", () => {
@@ -78,7 +78,7 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
78
78
  expect(job.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
79
79
  expect(job.permissions.contents).toBe("write");
80
80
  const run = job.steps.find((s) => s.run?.includes("ci run apply"))!.run!;
81
- // #4156: through chant ci run, which runs pr-apply --require-review --wave-gate release --resume ... --wave <k>.
81
+ // #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
82
82
  expect(run).toContain(`chant ci run apply --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github --wave-gate release --wave ${k}`);
83
83
  expect(job.steps.find((s) => s.run?.includes("ci run apply"))!.env?.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
84
84
  }
@@ -98,7 +98,7 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
98
98
  });
99
99
 
100
100
  test("without --wave-gate the workflow is the one apply job it was", () => {
101
- const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} });
101
+ const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
102
102
  expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "apply"]);
103
103
  expect(plain.yaml).not.toContain("--wave");
104
104
  });