@intentius/chant-lexicon-github 0.124.54 → 0.124.56

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.
Files changed (39) hide show
  1. package/dist/components/agent-jobs.d.ts +67 -0
  2. package/dist/components/agent-jobs.d.ts.map +1 -0
  3. package/dist/components/agent-jobs.js +139 -0
  4. package/dist/components/control-pipeline.d.ts +34 -0
  5. package/dist/components/control-pipeline.d.ts.map +1 -0
  6. package/dist/components/control-pipeline.js +45 -0
  7. package/dist/components/generate-op-pipeline.d.ts +8 -1
  8. package/dist/components/generate-op-pipeline.d.ts.map +1 -1
  9. package/dist/components/generate-op-pipeline.js +144 -16
  10. package/dist/components/generate-op-waves-pipeline.d.ts +27 -1
  11. package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
  12. package/dist/components/generate-op-waves-pipeline.js +135 -16
  13. package/dist/components/generate-pipeline.d.ts.map +1 -1
  14. package/dist/components/generate-pipeline.js +160 -15
  15. package/dist/integrity.json +2 -2
  16. package/dist/manifest.json +1 -1
  17. package/package.json +2 -2
  18. package/src/components/__golden__/op-waves-jobs.yml +20 -0
  19. package/src/components/agent-jobs.ts +177 -0
  20. package/src/components/control-pipeline.ts +65 -0
  21. package/src/components/generate-op-pipeline.agent.test.ts +147 -0
  22. package/src/components/generate-op-pipeline.cloud.test.ts +96 -0
  23. package/src/components/generate-op-pipeline.notice.test.ts +2 -1
  24. package/src/components/generate-op-pipeline.ts +165 -20
  25. package/src/components/generate-op-waves-pipeline.approval.test.ts +46 -0
  26. package/src/components/generate-op-waves-pipeline.control.test.ts +64 -0
  27. package/src/components/generate-op-waves-pipeline.guard.test.ts +57 -0
  28. package/src/components/generate-op-waves-pipeline.identity.test.ts +8 -8
  29. package/src/components/generate-op-waves-pipeline.test.ts +17 -0
  30. package/src/components/generate-op-waves-pipeline.ts +157 -15
  31. package/src/components/generate-pipeline.ts +170 -15
  32. package/src/components/generate-pr-pipeline.comment-locks.test.ts +53 -0
  33. package/src/components/generate-pr-pipeline.comment-preview.test.ts +64 -0
  34. package/src/components/generate-pr-pipeline.comment-targets.test.ts +24 -0
  35. package/src/components/generate-pr-pipeline.control.test.ts +94 -0
  36. package/src/components/generate-pr-pipeline.pulumi-identity.test.ts +43 -0
  37. package/src/components/generate-pr-pipeline.respond.test.ts +47 -0
  38. package/src/components/generate-pr-pipeline.runners.test.ts +61 -0
  39. package/src/components/generate-pr-pipeline.stale-notes.test.ts +42 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `/<product> <plan word>` (#4620): with `previewComment` a job previews the
3
+ * pull request's head again with a read token, and the job after it posts
4
+ * the note from the target branch's commit, as plan and plan-note do.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { parseYAML } from "@intentius/chant/yaml";
9
+ import type { DriverComponent } from "@intentius/chant/components/driver";
10
+ import { generateGithubPipeline } from "./generate-pipeline";
11
+
12
+ const ESTATE: DriverComponent[] = [
13
+ { name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
14
+ ];
15
+
16
+ const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const }, bin: "npx --no-install plywood" };
17
+
18
+ interface Step { id?: string; name?: string; uses?: string; run?: string; env?: Record<string, string>; with?: Record<string, unknown> }
19
+ interface Job { if?: string; needs?: string[]; permissions: Record<string, string>; outputs?: Record<string, string>; steps: Step[] }
20
+ type Doc = { on: Record<string, unknown>; jobs: Record<string, Job> };
21
+
22
+ const gen = (prLoop: Record<string, unknown>) => parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml) as Doc;
23
+
24
+ describe("the GitHub workflow's preview comment", () => {
25
+ const doc = gen({ previewComment: true, words: WORDS });
26
+ const preview = doc.jobs["comment-preview"]!;
27
+ const note = doc.jobs["comment-preview-note"]!;
28
+
29
+ test("started by issue_comment with the plan word, from a commenter who can write", () => {
30
+ expect(doc.on.issue_comment).toEqual({ types: ["created"] });
31
+ expect(preview.if).toContain("github.event_name == 'issue_comment'");
32
+ expect(preview.if).toContain("startsWith(github.event.comment.body, '/plywood preview')");
33
+ expect(preview.if).toContain("github.event.comment.author_association");
34
+ });
35
+
36
+ test("the preview job holds a read token only, and drops it before the change's code runs", () => {
37
+ expect(preview.permissions).toEqual({ contents: "read" });
38
+ const checkout = preview.steps[0]!;
39
+ expect(checkout.id).toBe("head");
40
+ expect(checkout.env!.CHANT_PREVIEW_REF).toBe("refs/pull/${{ github.event.issue.number }}/head");
41
+ expect(checkout.run).toContain('echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"');
42
+ const plan = preview.steps.at(-1)!;
43
+ expect(plan.run).toContain('BASE_SHA="$(git rev-parse --verify "refs/remotes/origin/main^{commit}")"');
44
+ expect(plan.run).toContain("npx --no-install plywood ci run plan");
45
+ expect(plan.env!.PR_NUMBER).toBe("${{ github.event.issue.number }}");
46
+ expect(plan.env!.GITHUB_TOKEN).toBeUndefined();
47
+ expect(preview.outputs!.head).toBe("${{ steps.head.outputs.head }}");
48
+ });
49
+
50
+ test("the note job posts from the target branch's commit, for the head the preview job read", () => {
51
+ expect(note.needs).toEqual(["comment-preview"]);
52
+ expect(note.permissions).toEqual({ actions: "read", contents: "read", "pull-requests": "write", statuses: "write" });
53
+ const post = note.steps.at(-1)!;
54
+ expect(post.run).toBe('npx --no-install plywood components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"');
55
+ expect(post.env!.HEAD_SHA).toBe("${{ needs.comment-preview.outputs.head }}");
56
+ expect(note.steps.some((s) => s.run?.includes("needs.comment-preview.outputs"))).toBe(true);
57
+ });
58
+
59
+ test("no preview job without previewComment", () => {
60
+ const plain = gen({ words: WORDS });
61
+ expect(plain.jobs["comment-preview"]).toBeUndefined();
62
+ expect(plain.on.issue_comment).toBeUndefined();
63
+ });
64
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The preview comment's --target on GitHub (#4626): the preview step runs
3
+ * `pr-plan --comment-targets` with the comment body in CHANT_PREVIEW_COMMENT,
4
+ * and the body never appears in the shell line. Written for the
5
+ * ci-products post-merge pass.
6
+ */
7
+ import { expect, test } from "vitest";
8
+ import { parseYAML } from "@intentius/chant/yaml";
9
+ import type { DriverComponent } from "@intentius/chant/components/driver";
10
+ import { generateGithubPipeline } from "./generate-pipeline";
11
+
12
+ const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
13
+ const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const }, bin: "npx --no-install plywood" };
14
+
15
+ test("the preview step reads the comment from a variable (#4626)", () => {
16
+ const doc = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: { previewComment: true, words: WORDS } }).yaml) as { jobs: Record<string, { steps: Array<{ name?: string; run?: string; env?: Record<string, string> }> }> };
17
+ const step = doc.jobs["comment-preview"]!.steps.find((s) => s.name === "Preview the members this pull request reaches")!;
18
+ expect(step.run).toContain("npx --no-install plywood ci run plan");
19
+ expect(step.run).toContain("--comment-targets");
20
+ expect(step.env!.CHANT_PREVIEW_COMMENT).toBe("${{ github.event.comment.body }}");
21
+ expect(step.run).not.toContain("comment.body");
22
+ const plan = doc.jobs.plan!.steps.find((s) => s.name === "Plan the members this pull request reaches")!;
23
+ expect(plan.run).not.toContain("--comment-targets");
24
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * #4568: with a control repo, the GitHub and Forgejo pull-request workflows
3
+ * add a `control-dispatch` job after the last apply on merge, which maps the
4
+ * product's secret into CHANT_CONTROL_TOKEN for its one step. The control
5
+ * repo's own workflow runs the member's dependents. Generated YAML only;
6
+ * the request itself is tested against fake forges in
7
+ * packages/core/src/control/control.test.ts.
8
+ */
9
+
10
+ import { describe, expect, test } from "vitest";
11
+ import { parseYAML } from "@intentius/chant/yaml";
12
+ import type { DriverComponent } from "@intentius/chant/components/driver";
13
+ import { resolveControlTarget } from "@intentius/chant/control/dispatch";
14
+ import { generateGithubPipeline } from "./generate-pipeline";
15
+ import { generateControlPipeline } from "./control-pipeline";
16
+
17
+ const root = (name: string, dependsOn: string[] = []): DriverComponent => ({ name, dependsOn, deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] });
18
+ const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"])];
19
+ const control = { target: resolveControlTarget({ repo: "github.com/acme/control", op: "deploy", tokenSecret: "PLYWOOD_CONTROL_TOKEN" }, "network") };
20
+
21
+ interface Job {
22
+ needs?: string[];
23
+ if?: string;
24
+ permissions?: Record<string, string>;
25
+ steps: Array<{ name?: string; run?: string; env?: Record<string, string> }>;
26
+ }
27
+
28
+ for (const forge of ["github", "forgejo"] as const) {
29
+ describe(`the ${forge} workflow's control dispatch (#4568)`, () => {
30
+ test("one job after the apply, reading the dispatch token from the product's secret", () => {
31
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control, words: { product: "plywood" } } });
32
+ const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
33
+ const job = doc.jobs["control-dispatch"]!;
34
+ expect(job.needs).toEqual(["apply"]);
35
+ expect(job.permissions).toEqual({ contents: "read" });
36
+ expect(job.steps[0]!.env!.CHANT_CONTROL_TOKEN).toBe("${{ secrets.PLYWOOD_CONTROL_TOKEN }}");
37
+ expect(job.steps[0]!.run).toContain(forge === "github" ? "plywood-applied" : "/dispatches");
38
+ // No other job holds the dispatch token.
39
+ for (const [name, other] of Object.entries(doc.jobs)) if (name !== "control-dispatch") expect(JSON.stringify(other)).not.toContain("PLYWOOD_CONTROL_TOKEN");
40
+ });
41
+
42
+ test("with a gate per wave, the job follows the last wave and reads the commit it applied", () => {
43
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control }, gatedWaves: { gate: "release" } });
44
+ const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
45
+ const job = doc.jobs["control-dispatch"]!;
46
+ expect(job.needs).toEqual(["apply-wave-2"]);
47
+ expect(job.steps[0]!.env!.CHANT_CONTROL_SHA).toBe("${{ needs.apply-wave-2.outputs.green-sha }}");
48
+ });
49
+
50
+ test("no apply on merge, no dispatch", () => {
51
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { forge, control } });
52
+ expect((parseYAML(yaml) as { jobs: Record<string, Job> }).jobs["control-dispatch"]).toBeUndefined();
53
+ });
54
+ });
55
+ }
56
+
57
+ describe("the control repo's workflow on GitHub (#4568)", () => {
58
+ test("a repository_dispatch of the product's event and a manual dispatch run the dependents", () => {
59
+ const file = generateControlPipeline({ words: { product: "terragucci" } });
60
+ expect(file.path).toBe(".github/workflows/terragucci-dependents.yml");
61
+ const doc = parseYAML(file.content) as { on: Record<string, any>; jobs: Record<string, Job> };
62
+ expect(doc.on.repository_dispatch.types).toEqual(["terragucci-applied"]);
63
+ // #4617: the outcome an op waves dispatch carries is an input too.
64
+ expect(Object.keys(doc.on.workflow_dispatch.inputs)).toEqual(["member", "op", "sha", "outcome"]);
65
+ expect(doc.jobs.dependents!.steps[1]!.run).toContain("workspace run --dependents-of");
66
+ });
67
+ });
68
+
69
+ // #4619: the note job reads the control repo, read-only, so the note marks the cross-stack edges.
70
+ for (const forge of ["github", "forgejo"] as const) {
71
+ describe(`the ${forge} plan-note job's read of the control repo (#4619)`, () => {
72
+ const withRead = { target: resolveControlTarget({ repo: "github.com/acme/control", op: "deploy", tokenSecret: "PLYWOOD_CONTROL_TOKEN", readTokenSecret: "PLYWOOD_CONTROL_READ" }, "network") };
73
+
74
+ test("fetches the control repo's chant/lifecycle with the read secret before it posts", () => {
75
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge, control: withRead, words: { product: "plywood" } } });
76
+ const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
77
+ const steps = doc.jobs["plan-note"]!.steps;
78
+ const read = steps.findIndex((s) => s.run?.includes("refs/heads/chant/lifecycle"));
79
+ const post = steps.findIndex((s) => s.run?.includes("components pr-post"));
80
+ expect(read).toBeGreaterThan(-1);
81
+ expect(read).toBe(post - 1);
82
+ expect(steps[read]!.env).toEqual({ CHANT_CONTROL_READ_TOKEN: "${{ secrets.PLYWOOD_CONTROL_READ || github.token }}" });
83
+ expect(steps[read]!.run).toContain('echo "CHANT_CONTROL_DIR=$chant_control_dir" >>"$GITHUB_ENV"');
84
+ // The plan job runs the change's code and never holds the read secret; no job but the dispatch holds the dispatch token.
85
+ expect(JSON.stringify(doc.jobs.plan)).not.toContain("PLYWOOD_CONTROL_READ");
86
+ expect(JSON.stringify(doc.jobs["plan-note"])).not.toContain("PLYWOOD_CONTROL_TOKEN");
87
+ });
88
+
89
+ test("without a control repo the note job reads none", () => {
90
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, forge } });
91
+ expect(yaml).not.toContain("CHANT_CONTROL_DIR");
92
+ });
93
+ });
94
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * #4549: with a Pulumi Cloud identity, the GitHub and Forgejo pull-request
3
+ * workflows trade the job's OIDC token for a preview token in the plan and
4
+ * record jobs and an up token in the apply job. No job reads a Pulumi token
5
+ * from the forge's secrets, and the plan job names no up token. Generated
6
+ * YAML only; the trade itself is tested against the fake exchange in
7
+ * packages/core/src/ci-identity-pulumi.test.ts.
8
+ */
9
+
10
+ import { describe, expect, test } from "vitest";
11
+ import { parseYAML } from "@intentius/chant/yaml";
12
+ import type { DriverComponent } from "@intentius/chant/components/driver";
13
+ import { resolveCloudIdentity, TRADE_SPEC_ENV } from "@intentius/chant/ci-identity";
14
+ import { generateGithubPipeline } from "./generate-pipeline";
15
+
16
+ const ESTATE: DriverComponent[] = [{ name: "app", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "app" }] }] }];
17
+
18
+ const IDENTITY = resolveCloudIdentity({ pulumi: { organization: "acme", rules: [{ plan: "team:preview", apply: "team:deploy" }] } }, "prod")!;
19
+
20
+ interface Job {
21
+ permissions?: Record<string, string>;
22
+ steps: Array<{ name?: string; run?: string; env?: Record<string, string> }>;
23
+ }
24
+
25
+ for (const forge of ["github", "forgejo"] as const) {
26
+ describe(`the ${forge} workflow's Pulumi Cloud tokens (#4549)`, () => {
27
+ const { yaml } = generateGithubPipeline(ESTATE, { env: "prod", image: "example/ci:1", prLoop: { applyOnMerge: true, forge, identity: IDENTITY } });
28
+ const doc = parseYAML(yaml) as { jobs: Record<string, Job> };
29
+ const tradeOf = (job: Job) => JSON.parse(job.steps[0]!.env![TRADE_SPEC_ENV]!) as Record<string, unknown>;
30
+
31
+ test("the plan and record jobs trade for the preview token first, the apply job for the up token", () => {
32
+ expect(tradeOf(doc.jobs.plan!)).toEqual({ pulumi: { token: "team:preview", organization: "acme", audience: "urn:pulumi:org:acme" } });
33
+ expect(tradeOf(doc.jobs.record!)).toEqual(IDENTITY.plan);
34
+ expect(tradeOf(doc.jobs.apply!)).toEqual(IDENTITY.apply);
35
+ for (const name of ["plan", "record", "apply"]) expect(doc.jobs[name]!.permissions!["id-token"], name).toBe("write");
36
+ });
37
+
38
+ test("the plan job names no up token, and no job reads a Pulumi token from the forge's secrets", () => {
39
+ expect(JSON.stringify(doc.jobs.plan)).not.toContain("team:deploy");
40
+ expect(yaml).not.toMatch(/secrets\.PULUMI_ACCESS_TOKEN/);
41
+ });
42
+ });
43
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The pull request loop's responses, description check and merge on GitHub
3
+ * (#4629, #4636), as `chant build --pr-loop --respond --decide-backend
4
+ * --decide-model --merge auto` sets them: the note job asks the decide
5
+ * service, and the apply job carries CHANT_RESPOND and the merge.
6
+ */
7
+ import { describe, expect, test } from "vitest";
8
+ import { parseYAML } from "@intentius/chant/yaml";
9
+ import type { DriverComponent } from "@intentius/chant/components/driver";
10
+ import { generateGithubPipeline } from "./generate-pipeline";
11
+
12
+ const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
13
+
14
+ type Step = { name?: string; run?: string; env?: Record<string, string> };
15
+ type Doc = { jobs: Record<string, { steps: Step[] }> };
16
+ const gen = (prLoop: Record<string, unknown>) => parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml) as Doc;
17
+
18
+ describe("responses and the merge on GitHub (#4629, #4636)", () => {
19
+ const doc = gen({
20
+ applyBeforeMerge: true,
21
+ applyMerge: { tokenEnv: "CHANT_MERGE_TOKEN", method: "squash" },
22
+ respond: { "wave-refused": "off", description: "check" },
23
+ decide: { backend: "review", model: "m-1" },
24
+ });
25
+
26
+ test("the note job carries the responses and asks the decide service", () => {
27
+ const post = doc.jobs["plan-note"]!.steps.at(-1)!;
28
+ expect(post.run).toMatch(/--decide-backend "review" --decide-model "m-1"$/);
29
+ expect(post.env!.CHANT_RESPOND).toBe("wave-refused=off,description=check");
30
+ });
31
+
32
+ test("the apply job carries the responses and the merge", () => {
33
+ const steps = Object.values(doc.jobs).flatMap((j) => j.steps);
34
+ const apply = steps.find((s) => s.run?.includes("--apply-before-merge"))!;
35
+ expect(apply.run).toContain("--merge auto --merge-token-env CHANT_MERGE_TOKEN --merge-method squash");
36
+ expect(apply.env!.CHANT_RESPOND).toBe("wave-refused=off,description=check");
37
+ expect(apply.env!.CHANT_MERGE_TOKEN).toBe("${{ secrets.CHANT_MERGE_TOKEN }}");
38
+ });
39
+
40
+ test("without them, neither job changes", () => {
41
+ const plain = gen({ applyBeforeMerge: true });
42
+ const post = plain.jobs["plan-note"]!.steps.at(-1)!;
43
+ expect(post.run).not.toContain("--decide-backend");
44
+ expect(post.env!.CHANT_RESPOND).toBeUndefined();
45
+ expect(JSON.stringify(plain)).not.toContain("--merge auto");
46
+ });
47
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The pull request loop's runners, variables and passed secrets on GitHub
3
+ * (#4625): every job on its stage's runner, and the jobs that plan or apply
4
+ * with the variables and the secrets mapped from the repository's. Written
5
+ * for the ci-products post-merge pass.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { parseYAML } from "@intentius/chant/yaml";
10
+ import type { DriverComponent } from "@intentius/chant/components/driver";
11
+ import { generateGithubPipeline } from "./generate-pipeline";
12
+
13
+ const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
14
+ const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const, lock: "lock" as const, unlock: "unlock" as const } };
15
+
16
+ type Step = { name?: string; run?: string; env?: Record<string, string> };
17
+ type Doc = { jobs: Record<string, { "runs-on": unknown; steps: Step[] }> };
18
+
19
+ const doc = parseYAML(
20
+ generateGithubPipeline(ESTATE, {
21
+ env: "prod",
22
+ prLoop: {
23
+ words: WORDS,
24
+ previewComment: true,
25
+ locks: "plan",
26
+ applyOnMerge: true,
27
+ runners: { plan: ["self-hosted", "big"], apply: "deployer", default: "small" },
28
+ variables: { REGION: "eu-west-1" },
29
+ secrets: { DB_PASSWORD: "PROD_DB_PASSWORD" },
30
+ parallelism: 2,
31
+ },
32
+ }).yaml,
33
+ ) as Doc;
34
+
35
+ describe("the GitHub pull request loop's runners and variables (#4625)", () => {
36
+ test("every job runs on its stage's runner", () => {
37
+ expect(doc.jobs.plan!["runs-on"]).toEqual(["self-hosted", "big"]);
38
+ expect(doc.jobs["comment-preview"]!["runs-on"]).toEqual(["self-hosted", "big"]);
39
+ expect(doc.jobs.apply!["runs-on"]).toBe("deployer");
40
+ expect(doc.jobs.record!["runs-on"]).toBe("deployer");
41
+ for (const name of ["plan-note", "comment-preview-note", "lock", "unlock", "comment-lock", "comment-unlock"]) expect(doc.jobs[name]!["runs-on"], name).toBe("small");
42
+ });
43
+
44
+ test("the plan step gets the variable and the secret, and still drops the forge tokens first", () => {
45
+ const step = doc.jobs.plan!.steps.find((s) => s.name === "Plan the members this pull request reaches")!;
46
+ expect(step.env).toMatchObject({ REGION: "eu-west-1", DB_PASSWORD: "${{ secrets.PROD_DB_PASSWORD }}" });
47
+ expect(step.env).not.toHaveProperty("GITHUB_TOKEN");
48
+ expect(step.run!.split("\n")[0]).toContain("unset");
49
+ expect(step.run).toContain("--parallelism 2");
50
+ });
51
+
52
+ test("the apply step gets them too", () => {
53
+ const step = doc.jobs.apply!.steps.find((s) => s.run?.includes("ci run apply"))!;
54
+ expect(step.env).toMatchObject({ REGION: "eu-west-1", DB_PASSWORD: "${{ secrets.PROD_DB_PASSWORD }}" });
55
+ });
56
+
57
+ test("without runners every job keeps the forge's default", () => {
58
+ const plain = parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml) as Doc;
59
+ expect(plain.jobs.plan!["runs-on"]).toBe("ubuntu-latest");
60
+ });
61
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stale marking on GitHub (#4628): with staleNotes the note job passes the
3
+ * target branch's commit, and a stale-notes job on a push to the branch
4
+ * runs `pr-post --stale` at the pushed commit with a token that may edit the
5
+ * notes and set the statuses. Written for the ci-products post-merge pass.
6
+ */
7
+ import { describe, expect, test } from "vitest";
8
+ import { parseYAML } from "@intentius/chant/yaml";
9
+ import type { DriverComponent } from "@intentius/chant/components/driver";
10
+ import { generateGithubPipeline } from "./generate-pipeline";
11
+
12
+ const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
13
+ const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const }, bin: "npx --no-install plywood" };
14
+
15
+ type Step = { name?: string; run?: string; env?: Record<string, string>; uses?: string };
16
+ type Doc = { jobs: Record<string, { if?: string; permissions?: Record<string, string>; steps: Step[] }> };
17
+ const gen = (prLoop: Record<string, unknown>) => parseYAML(generateGithubPipeline(ESTATE, { env: "prod", prLoop }).yaml) as Doc;
18
+
19
+ describe("stale notes on GitHub (#4628)", () => {
20
+ const doc = gen({ words: WORDS, staleNotes: true, previewComment: true });
21
+
22
+ test("the note job passes the target branch's commit", () => {
23
+ const post = doc.jobs["plan-note"]!.steps.at(-1)!;
24
+ expect(post.run).toContain('--tip "$TIP_SHA"');
25
+ expect(post.env!.TIP_SHA).toBe("${{ github.event.pull_request.base.sha }}");
26
+ });
27
+
28
+ test("a stale-notes job on a push marks the open notes stale, naming the preview comment", () => {
29
+ const job = doc.jobs["stale-notes"]!;
30
+ expect(job.if).toBe("github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')");
31
+ expect(job.permissions).toEqual({ contents: "read", "pull-requests": "write", statuses: "write" });
32
+ const step = job.steps.at(-1)!;
33
+ expect(step.run).toBe('npx --no-install plywood components pr-post --stale --tip "$TIP_SHA" --env prod --forge github --preview-comment');
34
+ expect(step.env).toEqual({ TIP_SHA: "${{ github.sha }}", GITHUB_TOKEN: "${{ github.token }}" });
35
+ });
36
+
37
+ test("without staleNotes, no job and no tip", () => {
38
+ const plain = gen({ words: WORDS });
39
+ expect(plain.jobs["stale-notes"]).toBeUndefined();
40
+ expect(plain.jobs["plan-note"]!.steps.at(-1)!.run).not.toContain("--tip");
41
+ });
42
+ });