@intentius/chant-lexicon-github 0.124.39 → 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.
@@ -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
- expect(doc.jobs.plan.permissions).toEqual({ contents: "read", "pull-requests": "write", statuses: "write" });
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.values(doc.jobs)) expect(job.steps[0].with).toEqual({ "fetch-depth": 0 });
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
- for (const job of Object.values(mdoc.jobs)) {
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 ?? "").filter(Boolean);
240
- const i = runs.findIndex((r: string) => r.startsWith("chant components pr-"));
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
- expect(job.if).toBe("github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'");
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