@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.
- package/dist/components/ci-jobs.d.ts +7 -0
- package/dist/components/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +16 -2
- package/dist/components/generate-op-pipeline.js +3 -3
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +10 -5
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__fixtures__/pr-loop-record.github.golden.yml +74 -0
- package/src/components/ci-jobs.ts +16 -2
- package/src/components/generate-op-pipeline.test.ts +11 -0
- package/src/components/generate-op-pipeline.ts +3 -3
- package/src/components/generate-pipeline.ts +10 -4
- package/src/components/generate-pr-pipeline.test.ts +62 -16
- package/src/components/generate-pr-waves-pipeline.test.ts +3 -3
|
@@ -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,
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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
|
-
:
|
|
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
|
})),
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
81
|
+
"composite": "58ec911a741a048aafa2e77b71308b47d28436478805c484a48867361d3220d0"
|
|
82
82
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
:
|
|
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
|
|
76
|
-
expect(apply.run).not.toContain("--
|
|
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:
|
|
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("--
|
|
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 --
|
|
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
|
});
|