@intentius/chant-lexicon-github 0.124.33 → 0.124.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +33 -10
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +46 -5
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +64 -15
- package/src/components/__fixtures__/pr-loop.github.golden.yml +42 -5
- package/src/components/generate-pipeline.ts +35 -9
- package/src/components/generate-pr-pipeline.test.ts +26 -14
- package/src/components/generate-pr-waves-pipeline.test.ts +27 -5
|
@@ -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;AAmBjG,OAAO,KAAK,EACV,oBAAoB,IAAI,YAAY,EACpC,wBAAwB,IAAI,qBAAqB,EACjD,uBAAuB,IAAI,oBAAoB,EAEhD,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,mIAAmI;IACnI,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AA+ND;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAWpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CA+JnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
|
|
@@ -59,9 +59,12 @@ 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, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
|
|
62
|
+
import { GREEN_TAG_PATTERN, GREEN_TICK_WORKFLOW, PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prApplyGroup, prRecordGroup, prApplyRecordKey, prApplyWaveJobName, prLoopJobs, prLoopSetup, prLoopWaveJobs, prLoopWithWaves, } from "@intentius/chant/components/pr-pipeline";
|
|
63
63
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
64
64
|
import { checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs.js";
|
|
65
|
+
/** The output a gated apply wave hands the green commit it applies through, and the step that sets it (#4221). */
|
|
66
|
+
const WAVE_GREEN_OUTPUT = "green-sha";
|
|
67
|
+
const WAVE_STEP_ID = "apply";
|
|
65
68
|
/** The workflow artifact the gated-wave jobs hand their attempt record through, one per wave. */
|
|
66
69
|
function waveRecordArtifact(wave) {
|
|
67
70
|
return `fan-out-record-wave-${wave}`;
|
|
@@ -164,7 +167,15 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
164
167
|
...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
|
|
165
168
|
// The job's setup lines (#4004) run in the command's shell, so a value they settle reaches it.
|
|
166
169
|
// The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY secret is set.
|
|
167
|
-
|
|
170
|
+
// #4221: a wave job hands the commit it applies to the next wave as the job output `green-sha`.
|
|
171
|
+
job.wave
|
|
172
|
+
? {
|
|
173
|
+
name,
|
|
174
|
+
id: WAVE_STEP_ID,
|
|
175
|
+
env: { ...job.run.env, ...(job.wave > 1 ? { GREEN_SHA: `\${{ needs.${prApplyWaveJobName(job.wave - 1)}.outputs.${WAVE_GREEN_OUTPUT} }}` } : {}) },
|
|
176
|
+
run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), job.run.command].join("\n"),
|
|
177
|
+
}
|
|
178
|
+
: { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), job.run.command].join("\n") },
|
|
168
179
|
...runSteps(extraScript),
|
|
169
180
|
...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
|
|
170
181
|
{
|
|
@@ -174,24 +185,31 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
174
185
|
with: { name: reportName(job), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
|
|
175
186
|
},
|
|
176
187
|
];
|
|
188
|
+
// #4168 step 9: under apply on merge the apply runs on a green signal, and a push to the branch records.
|
|
189
|
+
const forgejo = loop.forge === "forgejo";
|
|
190
|
+
const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
|
|
191
|
+
const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
|
|
177
192
|
const applyJob = (job, needs, name) => ({
|
|
178
|
-
if:
|
|
193
|
+
if: job.jobName === "record" ? onPush : onGreen,
|
|
179
194
|
"runs-on": GITHUB_RUNNER,
|
|
180
195
|
...(needs.length > 0 ? { needs } : {}),
|
|
181
196
|
container: image,
|
|
182
197
|
concurrency: concurrency(prApplyGroup(env, member?.name)),
|
|
183
198
|
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
199
|
+
...(job.wave ? { outputs: { [WAVE_GREEN_OUTPUT]: `\${{ steps.${WAVE_STEP_ID}.outputs.${WAVE_GREEN_OUTPUT} }}` } } : {}),
|
|
184
200
|
steps: steps(job, name),
|
|
185
201
|
});
|
|
186
202
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
187
|
-
const
|
|
203
|
+
const recordJob = ["record", { ...applyJob(record, [], "Record the plan on the merged commit; nothing applies"), concurrency: concurrency(prRecordGroup(env, member?.name)) }];
|
|
204
|
+
const greenJobs = loop.waveGate
|
|
188
205
|
? waveJobs.map((job) => [
|
|
189
206
|
prApplyWaveJobName(job.wave),
|
|
190
|
-
applyJob(job, job.wave > 1 ? [prApplyWaveJobName(job.wave - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
|
|
207
|
+
applyJob(job, job.wave > 1 ? [prApplyWaveJobName(job.wave - 1)] : [], job.wave === 1 ? "Apply wave 1 of the newest green commit, under the approval its plan has" : `Apply wave ${job.wave} of the green commit wave 1 took, under the approval its plan has`),
|
|
191
208
|
])
|
|
192
209
|
: loop.applyOnMerge
|
|
193
|
-
? [["apply", applyJob(apply, [], "Apply the
|
|
194
|
-
: [
|
|
210
|
+
? [["apply", applyJob(apply, [], "Apply the newest green commit under the merged changes' approvals")]]
|
|
211
|
+
: [];
|
|
212
|
+
const applyJobs = [recordJob, ...greenJobs];
|
|
195
213
|
const jobsDoc = {
|
|
196
214
|
plan: {
|
|
197
215
|
if: "github.event_name == 'pull_request'",
|
|
@@ -206,7 +224,11 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
206
224
|
return {
|
|
207
225
|
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
208
226
|
environment: env,
|
|
209
|
-
on:
|
|
227
|
+
on: !loop.applyOnMerge
|
|
228
|
+
? { pull_request: { branches: [branch] }, push: { branches: [branch] } }
|
|
229
|
+
: forgejo
|
|
230
|
+
? { pull_request: { branches: [branch] }, push: { branches: [branch], tags: [GREEN_TAG_PATTERN] } }
|
|
231
|
+
: { pull_request: { branches: [branch] }, push: { branches: [branch] }, workflow_run: { workflows: [GREEN_TICK_WORKFLOW], types: ["completed"] }, workflow_dispatch: {} },
|
|
210
232
|
...(rooted ? {} : { defaults: { run: { "working-directory": member.dir } } }),
|
|
211
233
|
env: { ...options.variables, CHANT_ENV: env },
|
|
212
234
|
jobsDoc,
|
|
@@ -215,9 +237,10 @@ function prLoopGithubDoc(components, env, image, beforeScript, extraScript, opti
|
|
|
215
237
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
216
238
|
...applyNames.map((name, i) => ({
|
|
217
239
|
jobName: name,
|
|
218
|
-
component: loop.waveGate ? `
|
|
240
|
+
component: name === "record" ? "merge record" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
|
219
241
|
stage: name,
|
|
220
|
-
|
|
242
|
+
// The record runs on a push and the apply on a green commit, so only the waves need each other.
|
|
243
|
+
needs: i > 1 ? [applyNames[i - 1]] : [],
|
|
221
244
|
})),
|
|
222
245
|
],
|
|
223
246
|
};
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "3090b1e50a456840a260fd6554142bfa22caa3357a0bbdcbbb52d8cf8852a3bd",
|
|
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": "76bee507e1094451f0435711785b080f9d494a9aacb86319f3f2f40c84d24433"
|
|
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.35",
|
|
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.35"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -7,6 +7,12 @@ on:
|
|
|
7
7
|
push:
|
|
8
8
|
branches:
|
|
9
9
|
- main
|
|
10
|
+
workflow_run:
|
|
11
|
+
workflows:
|
|
12
|
+
- chant-ci-green
|
|
13
|
+
types:
|
|
14
|
+
- completed
|
|
15
|
+
workflow_dispatch: {}
|
|
10
16
|
|
|
11
17
|
env:
|
|
12
18
|
CHANT_ENV: prod
|
|
@@ -44,10 +50,46 @@ jobs:
|
|
|
44
50
|
path: .chant/pr
|
|
45
51
|
if-no-files-found: ignore
|
|
46
52
|
include-hidden-files: true
|
|
47
|
-
|
|
53
|
+
record:
|
|
48
54
|
if: github.event_name == 'push'
|
|
49
55
|
runs-on: ubuntu-latest
|
|
50
56
|
container: node:22
|
|
57
|
+
concurrency:
|
|
58
|
+
group: chant-record-prod
|
|
59
|
+
cancel-in-progress: false
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write
|
|
62
|
+
pull-requests: write
|
|
63
|
+
statuses: write
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v7
|
|
66
|
+
with:
|
|
67
|
+
fetch-depth: 0
|
|
68
|
+
- 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)
|
|
69
|
+
- name: Build the templates
|
|
70
|
+
run: chant build --env prod
|
|
71
|
+
- name: Lint the project
|
|
72
|
+
run: chant lint
|
|
73
|
+
- name: Record the plan on the merged commit; nothing applies
|
|
74
|
+
env:
|
|
75
|
+
BASE_SHA: '${{ github.event.before }}'
|
|
76
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
77
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
78
|
+
run: |
|
|
79
|
+
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
|
|
80
|
+
chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
|
|
81
|
+
- name: Keep the record report
|
|
82
|
+
if: always()
|
|
83
|
+
uses: actions/upload-artifact@v4
|
|
84
|
+
with:
|
|
85
|
+
name: chant-pr-record
|
|
86
|
+
path: .chant/pr
|
|
87
|
+
if-no-files-found: ignore
|
|
88
|
+
include-hidden-files: true
|
|
89
|
+
apply:
|
|
90
|
+
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
container: node:22
|
|
51
93
|
concurrency:
|
|
52
94
|
group: chant-apply-prod
|
|
53
95
|
cancel-in-progress: false
|
|
@@ -70,14 +112,13 @@ jobs:
|
|
|
70
112
|
path: .chant/pr-resume/pr-apply.json
|
|
71
113
|
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
72
114
|
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
73
|
-
- name: Apply the
|
|
115
|
+
- name: Apply the newest green commit under the merged changes' approvals
|
|
74
116
|
env:
|
|
75
|
-
BASE_SHA: '${{ github.event.before }}'
|
|
76
117
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
77
118
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
78
119
|
run: |
|
|
79
|
-
|
|
80
|
-
chant ci run apply --in-place --
|
|
120
|
+
git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
121
|
+
chant ci run apply --in-place --env prod --gate pr-apply --forge github
|
|
81
122
|
- name: Keep the record for a re-run
|
|
82
123
|
if: always()
|
|
83
124
|
uses: actions/cache/save@v4
|
|
@@ -7,6 +7,12 @@ on:
|
|
|
7
7
|
push:
|
|
8
8
|
branches:
|
|
9
9
|
- main
|
|
10
|
+
workflow_run:
|
|
11
|
+
workflows:
|
|
12
|
+
- chant-ci-green
|
|
13
|
+
types:
|
|
14
|
+
- completed
|
|
15
|
+
workflow_dispatch: {}
|
|
10
16
|
|
|
11
17
|
env:
|
|
12
18
|
CHANT_ENV: prod
|
|
@@ -40,10 +46,42 @@ jobs:
|
|
|
40
46
|
path: .chant/pr
|
|
41
47
|
if-no-files-found: ignore
|
|
42
48
|
include-hidden-files: true
|
|
43
|
-
|
|
49
|
+
record:
|
|
44
50
|
if: github.event_name == 'push'
|
|
45
51
|
runs-on: ubuntu-latest
|
|
46
52
|
container: node:22
|
|
53
|
+
concurrency:
|
|
54
|
+
group: chant-record-prod
|
|
55
|
+
cancel-in-progress: false
|
|
56
|
+
permissions:
|
|
57
|
+
contents: write
|
|
58
|
+
pull-requests: write
|
|
59
|
+
statuses: write
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v7
|
|
62
|
+
with:
|
|
63
|
+
fetch-depth: 0
|
|
64
|
+
- 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)
|
|
65
|
+
- name: Record the plan on the merged commit; nothing applies
|
|
66
|
+
env:
|
|
67
|
+
BASE_SHA: '${{ github.event.before }}'
|
|
68
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
69
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
70
|
+
run: |
|
|
71
|
+
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
|
|
72
|
+
chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
|
|
73
|
+
- name: Keep the record report
|
|
74
|
+
if: always()
|
|
75
|
+
uses: actions/upload-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: chant-pr-record
|
|
78
|
+
path: .chant/pr
|
|
79
|
+
if-no-files-found: ignore
|
|
80
|
+
include-hidden-files: true
|
|
81
|
+
apply-wave-1:
|
|
82
|
+
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
container: node:22
|
|
47
85
|
concurrency:
|
|
48
86
|
group: chant-apply-prod
|
|
49
87
|
cancel-in-progress: false
|
|
@@ -51,6 +89,8 @@ jobs:
|
|
|
51
89
|
contents: write
|
|
52
90
|
pull-requests: write
|
|
53
91
|
statuses: write
|
|
92
|
+
outputs:
|
|
93
|
+
green-sha: '${{ steps.apply.outputs.green-sha }}'
|
|
54
94
|
steps:
|
|
55
95
|
- uses: actions/checkout@v7
|
|
56
96
|
with:
|
|
@@ -62,14 +102,15 @@ jobs:
|
|
|
62
102
|
path: .chant/pr-resume/pr-apply.json
|
|
63
103
|
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-1
|
|
64
104
|
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
65
|
-
- name: Apply wave 1, under the approval its plan has
|
|
105
|
+
- name: Apply wave 1 of the newest green commit, under the approval its plan has
|
|
106
|
+
id: apply
|
|
66
107
|
env:
|
|
67
|
-
BASE_SHA: '${{ github.event.before }}'
|
|
68
108
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
69
109
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
70
110
|
run: |
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
112
|
+
echo "green-sha=$GREEN_SHA" >> "$GITHUB_OUTPUT"
|
|
113
|
+
chant ci run apply --in-place --env prod --gate pr-apply --forge github --wave-gate release --wave 1
|
|
73
114
|
- name: Keep the record for the next wave and a re-run
|
|
74
115
|
if: always()
|
|
75
116
|
uses: actions/cache/save@v4
|
|
@@ -85,7 +126,7 @@ jobs:
|
|
|
85
126
|
if-no-files-found: ignore
|
|
86
127
|
include-hidden-files: true
|
|
87
128
|
apply-wave-2:
|
|
88
|
-
if: github.event_name == '
|
|
129
|
+
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
89
130
|
runs-on: ubuntu-latest
|
|
90
131
|
needs:
|
|
91
132
|
- apply-wave-1
|
|
@@ -97,6 +138,8 @@ jobs:
|
|
|
97
138
|
contents: write
|
|
98
139
|
pull-requests: write
|
|
99
140
|
statuses: write
|
|
141
|
+
outputs:
|
|
142
|
+
green-sha: '${{ steps.apply.outputs.green-sha }}'
|
|
100
143
|
steps:
|
|
101
144
|
- uses: actions/checkout@v7
|
|
102
145
|
with:
|
|
@@ -108,14 +151,16 @@ jobs:
|
|
|
108
151
|
path: .chant/pr-resume/pr-apply.json
|
|
109
152
|
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-2
|
|
110
153
|
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
111
|
-
- name: Apply wave 2, under the approval its plan has
|
|
154
|
+
- name: Apply wave 2 of the green commit wave 1 took, under the approval its plan has
|
|
155
|
+
id: apply
|
|
112
156
|
env:
|
|
113
|
-
BASE_SHA: '${{ github.event.before }}'
|
|
114
157
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
115
158
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
159
|
+
GREEN_SHA: '${{ needs.apply-wave-1.outputs.green-sha }}'
|
|
116
160
|
run: |
|
|
117
|
-
|
|
118
|
-
|
|
161
|
+
test -n "$GREEN_SHA" && git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && git checkout --quiet --detach "$GREEN_SHA"
|
|
162
|
+
echo "green-sha=$GREEN_SHA" >> "$GITHUB_OUTPUT"
|
|
163
|
+
chant ci run apply --in-place --env prod --gate pr-apply --forge github --wave-gate release --wave 2
|
|
119
164
|
- name: Keep the record for the next wave and a re-run
|
|
120
165
|
if: always()
|
|
121
166
|
uses: actions/cache/save@v4
|
|
@@ -131,7 +176,7 @@ jobs:
|
|
|
131
176
|
if-no-files-found: ignore
|
|
132
177
|
include-hidden-files: true
|
|
133
178
|
apply-wave-3:
|
|
134
|
-
if: github.event_name == '
|
|
179
|
+
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
135
180
|
runs-on: ubuntu-latest
|
|
136
181
|
needs:
|
|
137
182
|
- apply-wave-2
|
|
@@ -143,6 +188,8 @@ jobs:
|
|
|
143
188
|
contents: write
|
|
144
189
|
pull-requests: write
|
|
145
190
|
statuses: write
|
|
191
|
+
outputs:
|
|
192
|
+
green-sha: '${{ steps.apply.outputs.green-sha }}'
|
|
146
193
|
steps:
|
|
147
194
|
- uses: actions/checkout@v7
|
|
148
195
|
with:
|
|
@@ -154,14 +201,16 @@ jobs:
|
|
|
154
201
|
path: .chant/pr-resume/pr-apply.json
|
|
155
202
|
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-wave-3
|
|
156
203
|
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
157
|
-
- name: Apply wave 3, under the approval its plan has
|
|
204
|
+
- name: Apply wave 3 of the green commit wave 1 took, under the approval its plan has
|
|
205
|
+
id: apply
|
|
158
206
|
env:
|
|
159
|
-
BASE_SHA: '${{ github.event.before }}'
|
|
160
207
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
161
208
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
209
|
+
GREEN_SHA: '${{ needs.apply-wave-2.outputs.green-sha }}'
|
|
162
210
|
run: |
|
|
163
|
-
|
|
164
|
-
|
|
211
|
+
test -n "$GREEN_SHA" && git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && git checkout --quiet --detach "$GREEN_SHA"
|
|
212
|
+
echo "green-sha=$GREEN_SHA" >> "$GITHUB_OUTPUT"
|
|
213
|
+
chant ci run apply --in-place --env prod --gate pr-apply --forge github --wave-gate release --wave 3
|
|
165
214
|
- name: Keep the record for the next wave and a re-run
|
|
166
215
|
if: always()
|
|
167
216
|
uses: actions/cache/save@v4
|
|
@@ -7,6 +7,12 @@ on:
|
|
|
7
7
|
push:
|
|
8
8
|
branches:
|
|
9
9
|
- main
|
|
10
|
+
workflow_run:
|
|
11
|
+
workflows:
|
|
12
|
+
- chant-ci-green
|
|
13
|
+
types:
|
|
14
|
+
- completed
|
|
15
|
+
workflow_dispatch: {}
|
|
10
16
|
|
|
11
17
|
env:
|
|
12
18
|
CHANT_ENV: prod
|
|
@@ -40,10 +46,42 @@ jobs:
|
|
|
40
46
|
path: .chant/pr
|
|
41
47
|
if-no-files-found: ignore
|
|
42
48
|
include-hidden-files: true
|
|
43
|
-
|
|
49
|
+
record:
|
|
44
50
|
if: github.event_name == 'push'
|
|
45
51
|
runs-on: ubuntu-latest
|
|
46
52
|
container: node:22
|
|
53
|
+
concurrency:
|
|
54
|
+
group: chant-record-prod
|
|
55
|
+
cancel-in-progress: false
|
|
56
|
+
permissions:
|
|
57
|
+
contents: write
|
|
58
|
+
pull-requests: write
|
|
59
|
+
statuses: write
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v7
|
|
62
|
+
with:
|
|
63
|
+
fetch-depth: 0
|
|
64
|
+
- 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)
|
|
65
|
+
- name: Record the plan on the merged commit; nothing applies
|
|
66
|
+
env:
|
|
67
|
+
BASE_SHA: '${{ github.event.before }}'
|
|
68
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
69
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
70
|
+
run: |
|
|
71
|
+
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
|
|
72
|
+
chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge github
|
|
73
|
+
- name: Keep the record report
|
|
74
|
+
if: always()
|
|
75
|
+
uses: actions/upload-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: chant-pr-record
|
|
78
|
+
path: .chant/pr
|
|
79
|
+
if-no-files-found: ignore
|
|
80
|
+
include-hidden-files: true
|
|
81
|
+
apply:
|
|
82
|
+
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
container: node:22
|
|
47
85
|
concurrency:
|
|
48
86
|
group: chant-apply-prod
|
|
49
87
|
cancel-in-progress: false
|
|
@@ -62,14 +100,13 @@ jobs:
|
|
|
62
100
|
path: .chant/pr-resume/pr-apply.json
|
|
63
101
|
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
64
102
|
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
65
|
-
- name: Apply the
|
|
103
|
+
- name: Apply the newest green commit under the merged changes' approvals
|
|
66
104
|
env:
|
|
67
|
-
BASE_SHA: '${{ github.event.before }}'
|
|
68
105
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
69
106
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
70
107
|
run: |
|
|
71
|
-
|
|
72
|
-
chant ci run apply --in-place --
|
|
108
|
+
git fetch --quiet --tags origin "+refs/heads/main:refs/remotes/origin/main" && GREEN_SHA="$(chant ci last-green)" && git checkout --quiet --detach "$GREEN_SHA"
|
|
109
|
+
chant ci run apply --in-place --env prod --gate pr-apply --forge github
|
|
73
110
|
- name: Keep the record for a re-run
|
|
74
111
|
if: always()
|
|
75
112
|
uses: actions/cache/save@v4
|
|
@@ -61,6 +61,8 @@ import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components
|
|
|
61
61
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
62
62
|
import { CI_IMAGE, ciJobName, dirnameOf, fillCommand, GITHUB_RUNNER } from "@intentius/chant/ci-jobs";
|
|
63
63
|
import {
|
|
64
|
+
GREEN_TAG_PATTERN,
|
|
65
|
+
GREEN_TICK_WORKFLOW,
|
|
64
66
|
PR_APPLY_RECORD,
|
|
65
67
|
PR_LOOP_REPORT_DIR,
|
|
66
68
|
prApplyGroup,
|
|
@@ -120,6 +122,10 @@ export interface GithubPipelineDoc {
|
|
|
120
122
|
jobs: GeneratedJob[];
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** The output a gated apply wave hands the green commit it applies through, and the step that sets it (#4221). */
|
|
126
|
+
const WAVE_GREEN_OUTPUT = "green-sha";
|
|
127
|
+
const WAVE_STEP_ID = "apply";
|
|
128
|
+
|
|
123
129
|
/** The workflow artifact the gated-wave jobs hand their attempt record through, one per wave. */
|
|
124
130
|
function waveRecordArtifact(wave: number): string {
|
|
125
131
|
return `fan-out-record-wave-${wave}`;
|
|
@@ -238,7 +244,15 @@ function prLoopGithubDoc(
|
|
|
238
244
|
...(job.jobName === "apply" ? [restoreRecord(job.wave)] : []),
|
|
239
245
|
// The job's setup lines (#4004) run in the command's shell, so a value they settle reaches it.
|
|
240
246
|
// The command runs through chant ci run (#4156), which writes the job's run record, signed when the CHANT_RUNNER_KEY secret is set.
|
|
241
|
-
|
|
247
|
+
// #4221: a wave job hands the commit it applies to the next wave as the job output `green-sha`.
|
|
248
|
+
job.wave
|
|
249
|
+
? {
|
|
250
|
+
name,
|
|
251
|
+
id: WAVE_STEP_ID,
|
|
252
|
+
env: { ...job.run.env, ...(job.wave > 1 ? { GREEN_SHA: `\${{ needs.${prApplyWaveJobName(job.wave - 1)}.outputs.${WAVE_GREEN_OUTPUT} }}` } : {}) },
|
|
253
|
+
run: [...(job.checkout ?? []), `echo "${WAVE_GREEN_OUTPUT}=$GREEN_SHA" >> "$GITHUB_OUTPUT"`, ...(job.setup ?? []), job.run.command].join("\n"),
|
|
254
|
+
}
|
|
255
|
+
: { name, env: job.run.env, run: [...(job.checkout ?? []), ...(job.setup ?? []), job.run.command].join("\n") },
|
|
242
256
|
...runSteps(extraScript),
|
|
243
257
|
...(job.jobName === "apply" ? [saveRecord(job.wave)] : []),
|
|
244
258
|
{
|
|
@@ -248,24 +262,31 @@ function prLoopGithubDoc(
|
|
|
248
262
|
with: { name: reportName(job), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
|
|
249
263
|
},
|
|
250
264
|
];
|
|
265
|
+
// #4168 step 9: under apply on merge the apply runs on a green signal, and a push to the branch records.
|
|
266
|
+
const forgejo = loop.forge === "forgejo";
|
|
267
|
+
const onGreen = forgejo ? "startsWith(github.ref, 'refs/tags/ci/green/')" : "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
|
|
268
|
+
const onPush = loop.applyOnMerge && forgejo ? "github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')" : "github.event_name == 'push'";
|
|
251
269
|
const applyJob = (job: typeof plan, needs: string[], name: string) => ({
|
|
252
|
-
if:
|
|
270
|
+
if: job.jobName === "record" ? onPush : onGreen,
|
|
253
271
|
"runs-on": GITHUB_RUNNER,
|
|
254
272
|
...(needs.length > 0 ? { needs } : {}),
|
|
255
273
|
container: image,
|
|
256
274
|
concurrency: concurrency(prApplyGroup(env, member?.name)),
|
|
257
275
|
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
276
|
+
...(job.wave ? { outputs: { [WAVE_GREEN_OUTPUT]: `\${{ steps.${WAVE_STEP_ID}.outputs.${WAVE_GREEN_OUTPUT} }}` } } : {}),
|
|
258
277
|
steps: steps(job, name),
|
|
259
278
|
});
|
|
260
279
|
const waveJobs = loop.waveGate ? prLoopWaveJobs(components, apply) : [];
|
|
261
|
-
const
|
|
280
|
+
const recordJob: [string, Record<string, unknown>] = ["record", { ...applyJob(record, [], "Record the plan on the merged commit; nothing applies"), concurrency: concurrency(prRecordGroup(env, member?.name)) }];
|
|
281
|
+
const greenJobs: Array<[string, Record<string, unknown>]> = loop.waveGate
|
|
262
282
|
? waveJobs.map((job) => [
|
|
263
283
|
prApplyWaveJobName(job.wave!),
|
|
264
|
-
applyJob(job, job.wave! > 1 ? [prApplyWaveJobName(job.wave! - 1)] : [], `Apply wave ${job.wave}, under the approval its plan has`),
|
|
284
|
+
applyJob(job, job.wave! > 1 ? [prApplyWaveJobName(job.wave! - 1)] : [], job.wave === 1 ? "Apply wave 1 of the newest green commit, under the approval its plan has" : `Apply wave ${job.wave} of the green commit wave 1 took, under the approval its plan has`),
|
|
265
285
|
])
|
|
266
286
|
: loop.applyOnMerge
|
|
267
|
-
? [["apply", applyJob(apply, [], "Apply the
|
|
268
|
-
: [
|
|
287
|
+
? [["apply", applyJob(apply, [], "Apply the newest green commit under the merged changes' approvals")]]
|
|
288
|
+
: [];
|
|
289
|
+
const applyJobs: Array<[string, Record<string, unknown>]> = [recordJob, ...greenJobs];
|
|
269
290
|
const jobsDoc: Record<string, unknown> = {
|
|
270
291
|
plan: {
|
|
271
292
|
if: "github.event_name == 'pull_request'",
|
|
@@ -280,7 +301,11 @@ function prLoopGithubDoc(
|
|
|
280
301
|
return {
|
|
281
302
|
name: member ? `chant-pr.${member.name}.${env}` : `chant-pr-${env}`,
|
|
282
303
|
environment: env,
|
|
283
|
-
on:
|
|
304
|
+
on: !loop.applyOnMerge
|
|
305
|
+
? { pull_request: { branches: [branch] }, push: { branches: [branch] } }
|
|
306
|
+
: forgejo
|
|
307
|
+
? { pull_request: { branches: [branch] }, push: { branches: [branch], tags: [GREEN_TAG_PATTERN] } }
|
|
308
|
+
: { pull_request: { branches: [branch] }, push: { branches: [branch] }, workflow_run: { workflows: [GREEN_TICK_WORKFLOW], types: ["completed"] }, workflow_dispatch: {} },
|
|
284
309
|
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
285
310
|
env: { ...options.variables, CHANT_ENV: env },
|
|
286
311
|
jobsDoc,
|
|
@@ -289,9 +314,10 @@ function prLoopGithubDoc(
|
|
|
289
314
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
290
315
|
...applyNames.map((name, i) => ({
|
|
291
316
|
jobName: name,
|
|
292
|
-
component: loop.waveGate ? `
|
|
317
|
+
component: name === "record" ? "merge record" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
|
293
318
|
stage: name,
|
|
294
|
-
|
|
319
|
+
// The record runs on a push and the apply on a green commit, so only the waves need each other.
|
|
320
|
+
needs: i > 1 ? [applyNames[i - 1]] : [],
|
|
295
321
|
})),
|
|
296
322
|
],
|
|
297
323
|
};
|
|
@@ -11,7 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { describe, expect, test } from "vitest";
|
|
12
12
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
13
13
|
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
14
|
-
import { githubBaseFallback, prApplyGroup, prApplyRecordKey, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
14
|
+
import { githubBaseFallback, greenCheckout, prApplyGroup, prApplyRecordKey, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
15
15
|
import { generateGithubPipeline } from "./generate-pipeline";
|
|
16
16
|
|
|
17
17
|
const ESTATE: DriverComponent[] = [
|
|
@@ -54,12 +54,15 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
54
54
|
expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test("plans on a pull request into main
|
|
57
|
+
test("plans on a pull request into main, records on a push to it, and applies once the tick ran (#4168 step 9)", () => {
|
|
58
58
|
expect(doc.name).toBe("chant-pr-prod");
|
|
59
|
-
|
|
59
|
+
// A tag the tick pushes with the job token starts no workflow, so the apply follows the tick's workflow instead.
|
|
60
|
+
expect(doc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] }, workflow_run: { workflows: ["chant-ci-green"], types: ["completed"] }, workflow_dispatch: {} });
|
|
60
61
|
expect(doc.jobs.plan.if).toBe("github.event_name == 'pull_request'");
|
|
61
|
-
expect(doc.jobs.
|
|
62
|
-
expect(
|
|
62
|
+
expect(doc.jobs.record.if).toBe("github.event_name == 'push'");
|
|
63
|
+
expect(doc.jobs.apply.if).toBe("github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'");
|
|
64
|
+
expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "record", "apply"]);
|
|
65
|
+
expect(result.jobs.find((j) => j.jobName === "apply")!.needs).toEqual([]);
|
|
63
66
|
});
|
|
64
67
|
|
|
65
68
|
test("the plan job measures from the pull request's base and cannot push; the apply job can, one at a time", () => {
|
|
@@ -75,26 +78,35 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
75
78
|
// #4168: ci run builds pr-apply without --require-review unless the pipeline asks for it.
|
|
76
79
|
expect(apply.run).not.toContain("--require-review");
|
|
77
80
|
expect(apply.run).not.toContain("--no-push");
|
|
78
|
-
|
|
81
|
+
// #4168 step 9: the apply measures from the last applied green commit, so it reads no push base.
|
|
82
|
+
expect(apply.env).toEqual({ GITHUB_TOKEN: "${{ github.token }}", CHANT_RUNNER_KEY: "${{ secrets.CHANT_RUNNER_KEY }}" });
|
|
79
83
|
expect(doc.jobs.apply.permissions.contents).toBe("write");
|
|
80
84
|
expect(doc.jobs.apply.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
81
85
|
for (const job of Object.values(doc.jobs)) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
82
86
|
});
|
|
83
87
|
|
|
84
|
-
test("a first push's all-zero github.event.before is settled in the
|
|
85
|
-
const
|
|
86
|
-
const lines =
|
|
88
|
+
test("a first push's all-zero github.event.before is settled in the record's own shell, before pr-record reads it (#4004)", () => {
|
|
89
|
+
const record = doc.jobs.record.steps.find((s) => s.run?.includes("ci run record"))!;
|
|
90
|
+
const lines = record.run!.trimEnd().split("\n");
|
|
87
91
|
expect(lines).toHaveLength(2);
|
|
88
92
|
expect(lines[0]).toBe(githubBaseFallback());
|
|
89
93
|
expect(lines[0]).toContain('[ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]');
|
|
90
94
|
expect(lines[0]).toContain('git merge-base FETCH_HEAD "$GITHUB_SHA"');
|
|
91
|
-
expect(lines[1]).toMatch(/^chant ci run
|
|
92
|
-
const trunk = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml) as { jobs: Record<string, Job> };
|
|
93
|
-
expect(trunk.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!.run).toContain('git fetch origin "trunk"');
|
|
95
|
+
expect(lines[1]).toMatch(/^chant ci run record --in-place --base "\$BASE_SHA"/);
|
|
94
96
|
const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
95
97
|
expect(plan.run).not.toContain("0000000000000000000000000000000000000000");
|
|
96
98
|
});
|
|
97
99
|
|
|
100
|
+
test("the apply checks out the newest green commit of the branch before it runs (#4168 step 9)", () => {
|
|
101
|
+
const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
102
|
+
const lines = apply.run!.trimEnd().split("\n");
|
|
103
|
+
expect(lines).toEqual([greenCheckout('"main"'), "chant ci run apply --in-place --env prod --gate pr-apply --forge github"]);
|
|
104
|
+
expect(lines[0]).toContain('GREEN_SHA="$(chant ci last-green)"');
|
|
105
|
+
const trunk = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, branch: "trunk" } }).yaml) as { jobs: Record<string, Job> };
|
|
106
|
+
expect(trunk.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!.run).toContain('"+refs/heads/trunk:refs/remotes/origin/trunk"');
|
|
107
|
+
expect(prLoopJobs("github", "prod")[1]!.command).toMatch(/^chant components pr-apply --green /);
|
|
108
|
+
});
|
|
109
|
+
|
|
98
110
|
test("the apply resumes from a record the cache keeps for the pushed commit across a re-run (#3543)", () => {
|
|
99
111
|
const steps = doc.jobs.apply.steps;
|
|
100
112
|
const restore = steps.findIndex((s) => s.uses === "actions/cache/restore@v4");
|
|
@@ -130,7 +142,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
130
142
|
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, gate: "infra-apply", branch: "trunk", requireReview: true } }).yaml;
|
|
131
143
|
expect(yaml).toContain("--gate infra-apply");
|
|
132
144
|
expect(yaml).toContain("- trunk");
|
|
133
|
-
expect(yaml).toContain("ci run apply --in-place --
|
|
145
|
+
expect(yaml).toContain("ci run apply --in-place --env prod --gate infra-apply --forge github --branch \"trunk\" --require-review");
|
|
134
146
|
expect(yaml).toContain('--branch "trunk"');
|
|
135
147
|
});
|
|
136
148
|
|
|
@@ -163,7 +175,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
163
175
|
};
|
|
164
176
|
expect(mdoc.name).toBe("chant-pr.network.prod");
|
|
165
177
|
// No path filter: a change outside the member can reach it.
|
|
166
|
-
expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] } });
|
|
178
|
+
expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] }, workflow_run: { workflows: ["chant-ci-green"], types: ["completed"] }, workflow_dispatch: {} });
|
|
167
179
|
expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
|
|
168
180
|
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
169
181
|
// ci run finds the member from the directory it runs in and passes --member to pr-plan and pr-apply.
|
|
@@ -59,10 +59,31 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
59
59
|
expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
test("
|
|
63
|
-
|
|
62
|
+
test("wave 1 takes the newest green commit, and each later wave applies the one wave 1 took (#4221)", () => {
|
|
63
|
+
const jobs = doc.jobs as unknown as Record<string, { outputs?: Record<string, string>; steps: Array<{ id?: string; run?: string; env?: Record<string, string> }> }>;
|
|
64
|
+
const waves = Object.keys(jobs).filter((name) => name.startsWith("apply-wave-")).length;
|
|
65
|
+
expect(waves).toBeGreaterThan(1);
|
|
66
|
+
for (let k = 1; k <= waves; k++) {
|
|
67
|
+
const job = jobs[`apply-wave-${k}`]!;
|
|
68
|
+
expect(job.outputs).toEqual({ "green-sha": "${{ steps.apply.outputs.green-sha }}" });
|
|
69
|
+
const step = job.steps.find((s) => s.id === "apply")!;
|
|
70
|
+
expect(step.run).toContain('echo "green-sha=$GREEN_SHA" >> "$GITHUB_OUTPUT"');
|
|
71
|
+
if (k === 1) {
|
|
72
|
+
expect(step.run).toContain('GREEN_SHA="$(chant ci last-green)"');
|
|
73
|
+
expect(step.env?.GREEN_SHA).toBeUndefined();
|
|
74
|
+
} else {
|
|
75
|
+
expect(step.run).not.toContain("chant ci last-green");
|
|
76
|
+
expect(step.run).toContain('test -n "$GREEN_SHA"');
|
|
77
|
+
expect(step.env?.GREEN_SHA).toBe(`\${{ needs.apply-wave-${k - 1}.outputs.green-sha }}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("keeps the plan and record jobs, and writes one apply job per wave, each needing the one before", () => {
|
|
83
|
+
expect(Object.keys(doc.jobs)).toEqual(["plan", "record", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
64
84
|
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
65
85
|
["plan", []],
|
|
86
|
+
["record", []],
|
|
66
87
|
["apply-wave-1", []],
|
|
67
88
|
["apply-wave-2", ["apply-wave-1"]],
|
|
68
89
|
["apply-wave-3", ["apply-wave-2"]],
|
|
@@ -74,12 +95,13 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
74
95
|
expect(plan.run).not.toContain("--wave ");
|
|
75
96
|
for (const k of [1, 2, 3]) {
|
|
76
97
|
const job = doc.jobs[`apply-wave-${k}`]!;
|
|
77
|
-
|
|
98
|
+
// #4168 step 9: the waves apply the newest green commit once the tick ran.
|
|
99
|
+
expect(job.if).toBe("github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'");
|
|
78
100
|
expect(job.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
79
101
|
expect(job.permissions.contents).toBe("write");
|
|
80
102
|
const run = job.steps.find((s) => s.run?.includes("ci run apply"))!.run!;
|
|
81
103
|
// #4156: through chant ci run, which runs pr-apply --wave-gate release (no --require-review since #4168) --resume ... --wave <k>.
|
|
82
|
-
expect(run).toContain(`chant ci run apply --in-place --
|
|
104
|
+
expect(run).toContain(`chant ci run apply --in-place --env prod --gate pr-apply --forge github --wave-gate release --wave ${k}`);
|
|
83
105
|
expect(job.steps.find((s) => s.run?.includes("ci run apply"))!.env?.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
|
|
84
106
|
}
|
|
85
107
|
});
|
|
@@ -99,7 +121,7 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
99
121
|
|
|
100
122
|
test("without --wave-gate the workflow is the one apply job it was", () => {
|
|
101
123
|
const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
102
|
-
expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "apply"]);
|
|
124
|
+
expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "record", "apply"]);
|
|
103
125
|
expect(plain.yaml).not.toContain("--wave");
|
|
104
126
|
});
|
|
105
127
|
|