@intentius/chant-lexicon-github 0.124.40 → 0.124.41
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 +9 -0
- package/dist/components/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +12 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +205 -14
- 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 +76 -10
- package/src/components/__fixtures__/pr-loop-record.github.golden.yml +70 -8
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +72 -10
- package/src/components/__fixtures__/pr-loop.github.golden.yml +70 -8
- package/src/components/ci-jobs.ts +13 -0
- package/src/components/generate-pipeline.test.ts +3 -0
- package/src/components/generate-pipeline.ts +221 -13
- package/src/components/generate-pr-env-pipeline.test.ts +206 -0
- package/src/components/generate-pr-pipeline.handover.test.ts +167 -0
- package/src/components/generate-pr-pipeline.identity.test.ts +219 -0
- package/src/components/generate-pr-pipeline.test.ts +29 -9
- package/src/components/generate-pr-waves-pipeline.test.ts +6 -3
|
@@ -12,6 +12,7 @@ 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
14
|
import { githubBaseFallback, greenCheckout, prApplyGroup, prApplyRecordKey, prLoopJobs } from "@intentius/chant/components/pr-pipeline";
|
|
15
|
+
import { dropForgeTokens } from "@intentius/chant/ci-handover";
|
|
15
16
|
import { generateGithubPipeline } from "./generate-pipeline";
|
|
16
17
|
|
|
17
18
|
const ESTATE: DriverComponent[] = [
|
|
@@ -38,6 +39,14 @@ interface Step {
|
|
|
38
39
|
env?: Record<string, string>;
|
|
39
40
|
with?: Record<string, unknown>;
|
|
40
41
|
}
|
|
42
|
+
/** A plan job's run step without the line that drops the forge tokens (#4295). */
|
|
43
|
+
const headLines = (run: string | undefined): string[] => {
|
|
44
|
+
const lines = (run ?? "").trimEnd().split("\n");
|
|
45
|
+
return lines[0] === dropForgeTokens("sh") ? lines.slice(1) : lines;
|
|
46
|
+
};
|
|
47
|
+
/** The plan step's command: its last line. */
|
|
48
|
+
const lastLine = (run: string | undefined): string | undefined => (run ?? "").trimEnd().split("\n").at(-1);
|
|
49
|
+
|
|
41
50
|
interface Job {
|
|
42
51
|
if: string;
|
|
43
52
|
permissions: Record<string, string>;
|
|
@@ -61,18 +70,20 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
61
70
|
expect(doc.jobs.plan.if).toBe("github.event_name == 'pull_request'");
|
|
62
71
|
expect(doc.jobs.record.if).toBe("github.event_name == 'push'");
|
|
63
72
|
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"]);
|
|
73
|
+
expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "plan-note", "record", "apply"]);
|
|
65
74
|
expect(result.jobs.find((j) => j.jobName === "apply")!.needs).toEqual([]);
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
test("the plan job measures from the pull request's base and cannot push; the apply job can, one at a time", () => {
|
|
69
78
|
const plan = doc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
70
79
|
// #4156: the job runs pr-plan through chant ci run, in its own checkout, and pushes no record from the pull request's code.
|
|
71
|
-
expect(plan.run).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
80
|
+
expect(lastLine(plan.run)).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
72
81
|
expect(plan.env).toMatchObject({ BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}" });
|
|
73
82
|
// The runner key is an optional secret: unset, the expression is empty and the record is unsigned.
|
|
74
83
|
expect(plan.env?.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
|
|
75
|
-
|
|
84
|
+
// #4295: the plan job only reads; the plan-note job writes the note and the status.
|
|
85
|
+
expect(doc.jobs.plan.permissions).toEqual({ contents: "read" });
|
|
86
|
+
expect(doc.jobs["plan-note"].permissions).toEqual({ actions: "read", contents: "read", "pull-requests": "write", statuses: "write" });
|
|
76
87
|
|
|
77
88
|
const apply = doc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
78
89
|
// #4168: ci run builds pr-apply without --require-review unless the pipeline asks for it.
|
|
@@ -82,7 +93,10 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
82
93
|
expect(apply.env).toEqual({ GITHUB_TOKEN: "${{ github.token }}", CHANT_RUNNER_KEY: "${{ secrets.CHANT_RUNNER_KEY }}" });
|
|
83
94
|
expect(doc.jobs.apply.permissions.contents).toBe("write");
|
|
84
95
|
expect(doc.jobs.apply.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
85
|
-
for (const job of Object.
|
|
96
|
+
for (const [name, job] of Object.entries(doc.jobs)) if (!name.startsWith("plan")) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
97
|
+
// #4295: the plan job checks out with a script, and the note job checks out the target branch's commit.
|
|
98
|
+
expect(doc.jobs.plan.steps[0].uses).toBeUndefined();
|
|
99
|
+
expect(doc.jobs["plan-note"].steps[0].with).toEqual({ ref: "${{ github.event.pull_request.base.sha }}" });
|
|
86
100
|
});
|
|
87
101
|
|
|
88
102
|
test("a first push's all-zero github.event.before is settled in the record's own shell, before pr-record reads it (#4004)", () => {
|
|
@@ -179,7 +193,7 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
179
193
|
expect(mdoc.defaults).toEqual({ run: { "working-directory": "infra/network" } });
|
|
180
194
|
const plan = mdoc.jobs.plan.steps.find((s) => s.run?.includes("ci run plan"))!;
|
|
181
195
|
// ci run finds the member from the directory it runs in and passes --member to pr-plan and pr-apply.
|
|
182
|
-
expect(plan.run).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
196
|
+
expect(lastLine(plan.run)).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github');
|
|
183
197
|
expect(prLoopJobs("github", "prod", {}, "network")[0]!.command).toContain("--member network");
|
|
184
198
|
const apply = mdoc.jobs.apply.steps.find((s) => s.run?.includes("ci run apply"))!;
|
|
185
199
|
expect(mdoc.jobs.apply.concurrency).toEqual({ group: "chant-apply.network.prod", "cancel-in-progress": false });
|
|
@@ -187,10 +201,16 @@ describe("the GitHub pull-request workflow", () => {
|
|
|
187
201
|
expect(apply.run).toContain("--in-place");
|
|
188
202
|
const restore = mdoc.jobs.apply.steps.find((s) => s.uses === "actions/cache/restore@v4")!;
|
|
189
203
|
expect(restore.with).toMatchObject({ path: "infra/network/.chant/pr-resume/pr-apply.json", "restore-keys": "chant-apply.network.prod-${{ github.sha }}-" });
|
|
190
|
-
|
|
204
|
+
// #4295: the plan job uploads nothing; the plan-note job keeps its report.
|
|
205
|
+
expect(mdoc.jobs.plan.steps.some((s) => s.uses?.startsWith("actions/upload-artifact"))).toBe(false);
|
|
206
|
+
for (const [name, job] of Object.entries(mdoc.jobs)) {
|
|
207
|
+
if (name === "plan") continue;
|
|
191
208
|
const keep = job.steps.find((s) => s.uses?.startsWith("actions/upload-artifact"))!;
|
|
192
209
|
expect(keep.with?.path).toBe("infra/network/.chant/pr");
|
|
193
210
|
}
|
|
211
|
+
// The checkout runs at the repository's root, before the member's directory exists.
|
|
212
|
+
expect((mdoc.jobs.plan.steps[0] as Record<string, unknown>)["working-directory"]).toBe(".");
|
|
213
|
+
expect(mdoc.jobs["plan-note"].steps.at(-1)!.run).toContain("--member network");
|
|
194
214
|
});
|
|
195
215
|
|
|
196
216
|
test("no two member and environment pairs share an apply group or record key", () => {
|
|
@@ -236,8 +256,8 @@ describe("the github pull-request pipeline with build (#4088)", () => {
|
|
|
236
256
|
|
|
237
257
|
test("builds, then lints, before pr-plan and pr-apply", () => {
|
|
238
258
|
for (const job of ["plan", "apply"]) {
|
|
239
|
-
const runs = doc.jobs[job].steps.map((s: { run?: string }) => s.run
|
|
240
|
-
const i = runs.findIndex((r: string) => r.
|
|
259
|
+
const runs = doc.jobs[job].steps.map((s: { run?: string }) => headLines(s.run).join("\n")).filter(Boolean);
|
|
260
|
+
const i = runs.findIndex((r: string) => r.includes(`chant ci run ${job}`));
|
|
241
261
|
expect(runs.slice(i - 2, i)).toEqual(["chant build --env prod", "chant lint"]);
|
|
242
262
|
}
|
|
243
263
|
});
|
|
@@ -266,7 +286,7 @@ describe("the GitHub pull-request workflow records on merge by default (#4168)",
|
|
|
266
286
|
});
|
|
267
287
|
|
|
268
288
|
test("a push runs the record job, which applies nothing and keeps no resume record", () => {
|
|
269
|
-
expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "record"]);
|
|
289
|
+
expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "plan-note", "record"]);
|
|
270
290
|
expect(doc.jobs.apply).toBeUndefined();
|
|
271
291
|
expect(doc.jobs.record.if).toBe("github.event_name == 'push'");
|
|
272
292
|
const run = doc.jobs.record.steps.find((s) => s.run?.includes("ci run record"))!;
|
|
@@ -80,9 +80,10 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
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"]);
|
|
83
|
+
expect(Object.keys(doc.jobs)).toEqual(["plan", "plan-note", "record", "apply-wave-1", "apply-wave-2", "apply-wave-3"]);
|
|
84
84
|
expect(result.jobs.map((j) => [j.jobName, j.needs])).toEqual([
|
|
85
85
|
["plan", []],
|
|
86
|
+
["plan-note", ["plan"]],
|
|
86
87
|
["record", []],
|
|
87
88
|
["apply-wave-1", []],
|
|
88
89
|
["apply-wave-2", ["apply-wave-1"]],
|
|
@@ -96,7 +97,9 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
96
97
|
for (const k of [1, 2, 3]) {
|
|
97
98
|
const job = doc.jobs[`apply-wave-${k}`]!;
|
|
98
99
|
// #4168 step 9: the waves apply the newest green commit once the tick ran.
|
|
99
|
-
|
|
100
|
+
// #4291: a wave after the first runs after one that failed, so a failure blocks only its dependents.
|
|
101
|
+
const onGreen = "github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'";
|
|
102
|
+
expect(job.if).toBe(k === 1 ? onGreen : `!cancelled() && needs.apply-wave-${k - 1}.result != 'skipped' && (${onGreen})`);
|
|
100
103
|
expect(job.concurrency).toEqual({ group: "chant-apply-prod", "cancel-in-progress": false });
|
|
101
104
|
expect(job.permissions.contents).toBe("write");
|
|
102
105
|
const run = job.steps.find((s) => s.run?.includes("ci run apply"))!.run!;
|
|
@@ -121,7 +124,7 @@ describe("the GitHub pull-request workflow with a gate per wave (#4003)", () =>
|
|
|
121
124
|
|
|
122
125
|
test("without --wave-gate the workflow is the one apply job it was", () => {
|
|
123
126
|
const plain = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true } });
|
|
124
|
-
expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "record", "apply"]);
|
|
127
|
+
expect(plain.jobs.map((j) => j.jobName)).toEqual(["plan", "plan-note", "record", "apply"]);
|
|
125
128
|
expect(plain.yaml).not.toContain("--wave");
|
|
126
129
|
});
|
|
127
130
|
|