@intentius/chant-lexicon-github 0.105.0 → 0.106.1
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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__fixtures__/pr-loop.github.golden.yml +17 -3
- package/src/components/generate-pipeline.test.ts +5 -2
- package/src/components/generate-pipeline.ts +40 -7
- package/src/components/generate-pr-pipeline.test.ts +68 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAKjG,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;
|
|
1
|
+
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAGH,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAKjG,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;AAqMD;;;;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+KnB;AA4CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAU/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAGtB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "86245b91fbd6f3569b7e78ca9b9d4241f4709598483c4b73a55627a46b2c72fd",
|
|
5
5
|
"meta.json": "c50e0ef3845c370c3ad2210b8bba455980417c0f818aa6ed46e06d4fb8b1b9f9",
|
|
6
6
|
"types/index.d.ts": "bdd54a4145a39a9dd4f7bbc8c500191acb6bc9057a87da0040475afac84db397",
|
|
7
7
|
"rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
|
|
@@ -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": "4970f8db5a1ce849770c89a6393a48f46854cc626fc8e458a21afe9c0567e9f3"
|
|
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.
|
|
3
|
+
"version": "0.106.1",
|
|
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,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.
|
|
64
|
+
"@intentius/chant": "^0.106.1",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
plan:
|
|
16
16
|
if: github.event_name == 'pull_request'
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
|
-
container: node:22
|
|
18
|
+
container: node:22
|
|
19
19
|
permissions:
|
|
20
20
|
contents: read
|
|
21
21
|
pull-requests: write
|
|
@@ -24,6 +24,7 @@ jobs:
|
|
|
24
24
|
- uses: actions/checkout@v7
|
|
25
25
|
with:
|
|
26
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)
|
|
27
28
|
- name: Plan the members this pull request reaches
|
|
28
29
|
env:
|
|
29
30
|
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
@@ -41,7 +42,7 @@ jobs:
|
|
|
41
42
|
apply:
|
|
42
43
|
if: github.event_name == 'push'
|
|
43
44
|
runs-on: ubuntu-latest
|
|
44
|
-
container: node:22
|
|
45
|
+
container: node:22
|
|
45
46
|
concurrency:
|
|
46
47
|
group: chant-apply-prod
|
|
47
48
|
cancel-in-progress: false
|
|
@@ -53,11 +54,24 @@ jobs:
|
|
|
53
54
|
- uses: actions/checkout@v7
|
|
54
55
|
with:
|
|
55
56
|
fetch-depth: 0
|
|
57
|
+
- 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)
|
|
58
|
+
- name: Restore the record of an earlier attempt at this commit
|
|
59
|
+
uses: actions/cache/restore@v4
|
|
60
|
+
with:
|
|
61
|
+
path: .chant/pr-resume/pr-apply.json
|
|
62
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
63
|
+
restore-keys: chant-apply-prod-${{ github.sha }}-
|
|
56
64
|
- name: Apply the plan a reviewer approved
|
|
57
65
|
env:
|
|
58
66
|
BASE_SHA: '${{ github.event.before }}'
|
|
59
67
|
GITHUB_TOKEN: '${{ github.token }}'
|
|
60
|
-
run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review
|
|
68
|
+
run: chant components pr-apply --base "$BASE_SHA" --env prod --gate pr-apply --output .chant/pr --forge github --require-review --resume .chant/pr-resume/pr-apply.json
|
|
69
|
+
- name: Keep the record for a re-run
|
|
70
|
+
if: always()
|
|
71
|
+
uses: actions/cache/save@v4
|
|
72
|
+
with:
|
|
73
|
+
path: .chant/pr-resume/pr-apply.json
|
|
74
|
+
key: chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
61
75
|
- name: Keep the apply report
|
|
62
76
|
if: always()
|
|
63
77
|
uses: actions/upload-artifact@v4
|
|
@@ -438,10 +438,13 @@ describe("generateGithubPipeline: gated waves (#3049)", () => {
|
|
|
438
438
|
const steps = parsed.jobs["wave-2"].steps;
|
|
439
439
|
expect(steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
440
440
|
expect(steps[1]).toMatchObject({ uses: "actions/download-artifact@v4", with: { name: "fan-out-record-wave-1", path: ".chant" } });
|
|
441
|
-
|
|
441
|
+
// The default image is the pr-loop one: node:22, with OpenTofu installed when missing.
|
|
442
|
+
expect(parsed.jobs["wave-2"]).toMatchObject({ container: "node:22" });
|
|
443
|
+
expect(steps[2].run).toMatch(/^command -v tofu /);
|
|
444
|
+
expect(steps[3].run).toBe(
|
|
442
445
|
"chant components fan-out --base HEAD~1 --env prod --wave-gate release --wave 2 --resume .chant/fan-out.json",
|
|
443
446
|
);
|
|
444
|
-
expect(steps[
|
|
447
|
+
expect(steps[4]).toMatchObject({ if: "always()", with: { name: "fan-out-record-wave-2", path: ".chant/fan-out.json" } });
|
|
445
448
|
// Wave 1 has nothing to download.
|
|
446
449
|
expect(parsed.jobs["wave-1"].steps.some((s) => s.uses === "actions/download-artifact@v4")).toBe(false);
|
|
447
450
|
});
|
|
@@ -60,7 +60,7 @@ import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
|
|
|
60
60
|
import { resolveComponentGraph, type DriverComponent } from "@intentius/chant/components/driver";
|
|
61
61
|
import { hasPublishStep, promoteArchivePaths } from "@intentius/chant/components/promote";
|
|
62
62
|
import { GATED_WAVE_RECORD, gatedWaveJobs } from "@intentius/chant/components/gated-wave-pipeline";
|
|
63
|
-
import { PR_LOOP_REPORT_DIR, prApplyGroup, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
63
|
+
import { PR_APPLY_RECORD, PR_LOOP_IMAGE, PR_LOOP_REPORT_DIR, prApplyGroup, prApplyRecordKey, prLoopJobs, prLoopSetup } from "@intentius/chant/components/pr-pipeline";
|
|
64
64
|
import { memberRepoPath } from "@intentius/chant/lexicon";
|
|
65
65
|
import type {
|
|
66
66
|
ComponentPipelineJob as GeneratedJob,
|
|
@@ -142,6 +142,7 @@ function gatedWaveGithubDoc(
|
|
|
142
142
|
with: { name: waveRecordArtifact(job.wave - 1), path: dirnameOf(GATED_WAVE_RECORD) },
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
for (const line of prLoopSetup(image)) steps.push({ run: line });
|
|
145
146
|
for (const line of beforeScript) steps.push({ run: line });
|
|
146
147
|
steps.push({ run: job.command.join(" ") });
|
|
147
148
|
for (const line of extraScript) steps.push({ run: line });
|
|
@@ -183,6 +184,15 @@ function gatedWaveGithubDoc(
|
|
|
183
184
|
* job can push, to record a pending fact on `chant/lifecycle`, and runs one
|
|
184
185
|
* at a time per environment. Both keep their report as an artifact, and both
|
|
185
186
|
* check out the full history, since each measures the change with git.
|
|
187
|
+
* The apply job restores its attempt record from the cache before it runs
|
|
188
|
+
* and saves it afterwards, failed or not, so a re-run of a failed apply
|
|
189
|
+
* finishes it under the same approval (#3543).
|
|
190
|
+
*
|
|
191
|
+
* For a workspace member (#3465) the run steps start in the member's
|
|
192
|
+
* directory and the report artifact is kept from there. The triggers take no
|
|
193
|
+
* path filter, since a change outside the member can reach it; the plan
|
|
194
|
+
* selects from the whole change and passes `--member`, which keeps the
|
|
195
|
+
* member's gate, note and statuses apart from the other members'.
|
|
186
196
|
*/
|
|
187
197
|
function prLoopGithubDoc(
|
|
188
198
|
env: string,
|
|
@@ -193,17 +203,40 @@ function prLoopGithubDoc(
|
|
|
193
203
|
): GithubPipelineDoc {
|
|
194
204
|
const loop = options.prLoop!;
|
|
195
205
|
const branch = loop.branch ?? "main";
|
|
196
|
-
const
|
|
206
|
+
const member = options.member;
|
|
207
|
+
const rooted = !member || member.dir === "." || member.dir === "";
|
|
208
|
+
const [plan, apply] = prLoopJobs(loop.forge ?? "github", env, loop, member?.name);
|
|
209
|
+
const reportPath = member ? memberRepoPath(member, PR_LOOP_REPORT_DIR) : PR_LOOP_REPORT_DIR;
|
|
210
|
+
// The apply's attempt record (#3543) lives in the cache under the pushed
|
|
211
|
+
// commit. Cache keys cannot be overwritten, so each attempt saves under
|
|
212
|
+
// its run and attempt, and a re-run restores the newest for the commit.
|
|
213
|
+
const recordPath = member ? memberRepoPath(member, PR_APPLY_RECORD) : PR_APPLY_RECORD;
|
|
214
|
+
const recordPrefix = `${prApplyRecordKey(env, "${{ github.sha }}", member?.name)}-`;
|
|
215
|
+
const recordKey = `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}`;
|
|
216
|
+
const restoreRecord = {
|
|
217
|
+
name: "Restore the record of an earlier attempt at this commit",
|
|
218
|
+
uses: "actions/cache/restore@v4",
|
|
219
|
+
with: { path: recordPath, key: recordKey, "restore-keys": recordPrefix },
|
|
220
|
+
};
|
|
221
|
+
const saveRecord = {
|
|
222
|
+
name: "Keep the record for a re-run",
|
|
223
|
+
if: "always()",
|
|
224
|
+
uses: "actions/cache/save@v4",
|
|
225
|
+
with: { path: recordPath, key: recordKey },
|
|
226
|
+
};
|
|
197
227
|
const steps = (job: typeof plan, name: string): Array<Record<string, unknown>> => [
|
|
198
228
|
{ uses: actionRef("actions/checkout"), with: { "fetch-depth": 0 } },
|
|
229
|
+
...prLoopSetup(image).map((line) => ({ run: line })),
|
|
199
230
|
...beforeScript.map((line) => ({ run: line })),
|
|
231
|
+
...(job.jobName === "apply" ? [restoreRecord] : []),
|
|
200
232
|
{ name, env: job.env, run: job.command },
|
|
201
233
|
...extraScript.map((line) => ({ run: line })),
|
|
234
|
+
...(job.jobName === "apply" ? [saveRecord] : []),
|
|
202
235
|
{
|
|
203
236
|
name: `Keep the ${job.jobName} report`,
|
|
204
237
|
if: "always()",
|
|
205
238
|
uses: "actions/upload-artifact@v4",
|
|
206
|
-
with: { name: `chant-pr-${job.jobName}`, path:
|
|
239
|
+
with: { name: `chant-pr-${job.jobName}`, path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true },
|
|
207
240
|
},
|
|
208
241
|
];
|
|
209
242
|
const jobsDoc: Record<string, unknown> = {
|
|
@@ -218,15 +251,16 @@ function prLoopGithubDoc(
|
|
|
218
251
|
if: "github.event_name == 'push'",
|
|
219
252
|
"runs-on": "ubuntu-latest",
|
|
220
253
|
container: image,
|
|
221
|
-
concurrency: { group: prApplyGroup(env), "cancel-in-progress": false },
|
|
254
|
+
concurrency: { group: prApplyGroup(env, member?.name), "cancel-in-progress": false },
|
|
222
255
|
permissions: { contents: "write", "pull-requests": "write", statuses: "write" },
|
|
223
256
|
steps: steps(apply, "Apply the plan a reviewer approved"),
|
|
224
257
|
},
|
|
225
258
|
};
|
|
226
259
|
return {
|
|
227
|
-
name: `chant-pr-${env}`,
|
|
260
|
+
name: member ? `chant-pr-${member.name}-${env}` : `chant-pr-${env}`,
|
|
228
261
|
environment: env,
|
|
229
262
|
on: { pull_request: { branches: [branch] }, push: { branches: [branch] } },
|
|
263
|
+
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
230
264
|
env: { ...options.variables, CHANT_ENV: env },
|
|
231
265
|
jobsDoc,
|
|
232
266
|
stages: ["plan", "apply"],
|
|
@@ -302,7 +336,7 @@ export function buildGithubPipelineDoc(
|
|
|
302
336
|
options: GenerateGithubOptions = {},
|
|
303
337
|
): GithubPipelineDoc {
|
|
304
338
|
const env = options.env ?? "production";
|
|
305
|
-
const image = options.image ?? DEFAULT_IMAGE;
|
|
339
|
+
const image = options.image ?? (options.prLoop || options.gatedWaves ? PR_LOOP_IMAGE : DEFAULT_IMAGE);
|
|
306
340
|
const runCommand = options.runCommand ?? ["chant", "run", "--components", "{name}", "--env", env];
|
|
307
341
|
const beforeScript = options.beforeScript ?? [];
|
|
308
342
|
const extraScript = options.extraScript ?? [];
|
|
@@ -314,7 +348,6 @@ export function buildGithubPipelineDoc(
|
|
|
314
348
|
if (options.gatedWaves || options.promoteTo !== undefined) {
|
|
315
349
|
throw new Error("a pull-request pipeline has no wave or promote jobs; drop --wave-gate and --promote-to, or --pr-loop");
|
|
316
350
|
}
|
|
317
|
-
if (options.member) throw new Error("a pull-request pipeline is not generated for a workspace member yet; generate it from the project outside the workspace");
|
|
318
351
|
return prLoopGithubDoc(env, image, beforeScript, extraScript, options);
|
|
319
352
|
}
|
|
320
353
|
|
|
@@ -30,6 +30,8 @@ function hasActionlint(): boolean {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
interface Step {
|
|
33
|
+
name?: string;
|
|
34
|
+
if?: string;
|
|
33
35
|
uses?: string;
|
|
34
36
|
run?: string;
|
|
35
37
|
env?: Record<string, string>;
|
|
@@ -73,6 +75,35 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
73
75
|
for (const job of Object.values(doc.jobs)) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
test("the apply resumes from a record the cache keeps for the pushed commit across a re-run (#3543)", () => {
|
|
79
|
+
const steps = doc.jobs.apply.steps;
|
|
80
|
+
const restore = steps.findIndex((s) => s.uses === "actions/cache/restore@v4");
|
|
81
|
+
const run = steps.findIndex((s) => s.run?.includes("pr-apply"));
|
|
82
|
+
const save = steps.findIndex((s) => s.uses === "actions/cache/save@v4");
|
|
83
|
+
expect(restore).toBeGreaterThan(-1);
|
|
84
|
+
expect(restore).toBeLessThan(run);
|
|
85
|
+
expect(save).toBeGreaterThan(run);
|
|
86
|
+
expect(steps[run].run).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
87
|
+
const key = "chant-apply-prod-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}";
|
|
88
|
+
expect(steps[restore].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key, "restore-keys": "chant-apply-prod-${{ github.sha }}-" });
|
|
89
|
+
// Saved even when the apply failed, which is when a re-run needs it.
|
|
90
|
+
expect(steps[save].if).toBe("always()");
|
|
91
|
+
expect(steps[save].with).toEqual({ path: ".chant/pr-resume/pr-apply.json", key });
|
|
92
|
+
// The plan job runs the pull request's code: it neither reads nor writes the record.
|
|
93
|
+
expect(doc.jobs.plan.steps.some((s) => s.uses?.startsWith("actions/cache"))).toBe(false);
|
|
94
|
+
expect(doc.jobs.plan.steps.find((s) => s.run?.includes("pr-plan"))!.run).not.toContain("--resume");
|
|
95
|
+
// The report artifact does not carry the record, which holds the members' outputs.
|
|
96
|
+
const keep = steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
97
|
+
expect(keep.with?.path).toBe(".chant/pr");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("runs in an image with git and installs OpenTofu, unless the caller names an image", () => {
|
|
101
|
+
expect(doc.jobs.plan.steps[1].run).toContain("install-opentofu");
|
|
102
|
+
const own = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: {} }).yaml;
|
|
103
|
+
expect(own).toContain("container: example/ci:1");
|
|
104
|
+
expect(own).not.toContain("install-opentofu");
|
|
105
|
+
});
|
|
106
|
+
|
|
76
107
|
test("the gate, branch and review requirement are options", () => {
|
|
77
108
|
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { gate: "infra-apply", branch: "trunk", requireReview: false } }).yaml;
|
|
78
109
|
expect(yaml).toContain("--gate infra-apply");
|
|
@@ -85,6 +116,43 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
85
116
|
expect(() => generateGithubPipeline(ESTATE, { prLoop: {}, promoteTo: "prod" })).toThrow(/pull-request pipeline has no wave or promote jobs/);
|
|
86
117
|
});
|
|
87
118
|
|
|
119
|
+
test("inside a workspace member: runs in the member's directory and keeps its own gate, note and apply group (#3465)", () => {
|
|
120
|
+
const member = { name: "network", dir: "infra/network", file: ".github/workflows/chant-pr-network-prod.yml" };
|
|
121
|
+
const scoped = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member });
|
|
122
|
+
const mdoc = parseYAML(scoped.yaml) as {
|
|
123
|
+
name: string;
|
|
124
|
+
on: Record<string, unknown>;
|
|
125
|
+
defaults?: { run: Record<string, string> };
|
|
126
|
+
jobs: Record<string, Job>;
|
|
127
|
+
};
|
|
128
|
+
expect(mdoc.name).toBe("chant-pr-network-prod");
|
|
129
|
+
// No path filter: a change outside the member can reach it.
|
|
130
|
+
expect(mdoc.on).toEqual({ pull_request: { branches: ["main"] }, push: { branches: ["main"] } });
|
|
131
|
+
expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
|
|
132
|
+
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("pr-plan"))!;
|
|
133
|
+
expect(plan.run).toBe('chant components pr-plan --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --output .chant/pr --forge github --member network');
|
|
134
|
+
const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("pr-apply"))!;
|
|
135
|
+
expect(apply.run).toContain("--member network");
|
|
136
|
+
expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply-network-prod", "cancel-in-progress": false });
|
|
137
|
+
// The record path is relative to the member for --resume and to the repository for the cache.
|
|
138
|
+
expect(apply.run).toContain("--resume .chant/pr-resume/pr-apply.json");
|
|
139
|
+
const restore = mdoc.jobs.apply.steps.find((s) => s.uses === "actions/cache/restore@v4")!;
|
|
140
|
+
expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply-network-prod-${{ github.sha }}-" });
|
|
141
|
+
for (const job of Object.values(mdoc.jobs)) {
|
|
142
|
+
const keep = job.steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
143
|
+
expect(keep.with?.path).toBe("infra/network/.chant/pr");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a member at the workspace root keeps the root's paths but its own names", () => {
|
|
148
|
+
const yaml = generateGithubPipeline(ESTATE, { env: "prod", prLoop: {}, member: { name: "root", dir: "." } }).yaml;
|
|
149
|
+
const mdoc = parseYAML(yaml) as { name: string; defaults?: unknown; jobs: Record<string, Job> };
|
|
150
|
+
expect(mdoc.name).toBe("chant-pr-root-prod");
|
|
151
|
+
expect(mdoc.defaults).toBeUndefined();
|
|
152
|
+
expect(yaml).toContain("--member root");
|
|
153
|
+
expect(yaml).toContain("path: .chant/pr");
|
|
154
|
+
});
|
|
155
|
+
|
|
88
156
|
test.skipIf(!hasActionlint())("passes actionlint", () => {
|
|
89
157
|
const dir = mkdtempSync(join(tmpdir(), "chant-actionlint-"));
|
|
90
158
|
try {
|