@intentius/chant-lexicon-github 0.124.46 → 0.124.49
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/action-pins.d.ts +17 -27
- package/dist/action-pins.d.ts.map +1 -1
- package/dist/action-pins.js +39 -27
- package/dist/components/ci-jobs.d.ts +5 -2
- package/dist/components/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +8 -5
- package/dist/components/generate-op-pipeline.d.ts +7 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +26 -3
- package/dist/components/generate-op-waves-pipeline.d.ts +13 -3
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +24 -5
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +55 -10
- package/dist/components/review-jobs.d.ts +55 -0
- package/dist/components/review-jobs.d.ts.map +1 -0
- package/dist/components/review-jobs.js +130 -0
- package/dist/composites/cache.d.ts.map +1 -1
- package/dist/composites/cache.js +2 -1
- package/dist/composites/checkout.d.ts +3 -3
- package/dist/composites/checkout.js +1 -1
- package/dist/composites/docker-build.js +4 -4
- package/dist/composites/download-artifact.d.ts.map +1 -1
- package/dist/composites/download-artifact.js +2 -1
- package/dist/composites/go-ci.js +4 -4
- package/dist/composites/node-pipeline.js +5 -5
- package/dist/composites/python-ci.js +2 -2
- package/dist/composites/setup-go.d.ts.map +1 -1
- package/dist/composites/setup-go.js +2 -1
- package/dist/composites/setup-node.d.ts +3 -3
- package/dist/composites/setup-node.js +1 -1
- package/dist/composites/setup-python.d.ts.map +1 -1
- package/dist/composites/setup-python.js +2 -1
- package/dist/composites/upload-artifact.d.ts.map +1 -1
- package/dist/composites/upload-artifact.js +2 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/action-pins.test.ts +29 -6
- package/src/action-pins.ts +48 -34
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +8 -9
- package/src/components/__fixtures__/pr-loop-record.github.golden.yml +4 -5
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +16 -17
- package/src/components/__fixtures__/pr-loop.github.golden.yml +8 -9
- package/src/components/__golden__/op-waves-jobs.yml +71 -9
- package/src/components/ci-jobs.ts +8 -5
- package/src/components/generate-op-pipeline.review.test.ts +152 -0
- package/src/components/generate-op-pipeline.test.ts +5 -4
- package/src/components/generate-op-pipeline.ts +33 -3
- package/src/components/generate-op-waves-pipeline.test.ts +2 -1
- package/src/components/generate-op-waves-pipeline.ts +25 -5
- package/src/components/generate-pipeline.test.ts +10 -9
- package/src/components/generate-pipeline.ts +57 -9
- package/src/components/generate-pr-pipeline.locks.test.ts +68 -0
- package/src/components/generate-pr-pipeline.test.ts +6 -4
- package/src/components/generate-pr-waves-pipeline.test.ts +3 -2
- package/src/components/review-jobs.ts +165 -0
- package/src/composites/cache.ts +2 -1
- package/src/composites/checkout.ts +4 -4
- package/src/composites/composites.test.ts +23 -22
- package/src/composites/docker-build.ts +4 -4
- package/src/composites/download-artifact.ts +2 -1
- package/src/composites/go-ci.ts +4 -4
- package/src/composites/node-pipeline.ts +5 -5
- package/src/composites/python-ci.ts +2 -2
- package/src/composites/setup-go.ts +2 -1
- package/src/composites/setup-node.ts +4 -4
- package/src/composites/setup-python.ts +2 -1
- package/src/composites/upload-artifact.ts +2 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The split review (#4326): `ScheduledOpSpec.reviewAgent` makes two jobs, a
|
|
3
|
+
* review job that runs the change's code with no forge write token and a note
|
|
4
|
+
* job that checks nothing out and posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
import { CHECKOUT_TOKEN_ENV, checkoutScript, FORGE_TOKEN_VARS } from "@intentius/chant/ci-handover";
|
|
10
|
+
import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
11
|
+
import { generateGithubOpPipeline } from "./generate-op-pipeline";
|
|
12
|
+
|
|
13
|
+
interface Step {
|
|
14
|
+
id?: string;
|
|
15
|
+
uses?: string;
|
|
16
|
+
run?: string;
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
with?: Record<string, unknown>;
|
|
19
|
+
"working-directory"?: string;
|
|
20
|
+
}
|
|
21
|
+
interface Job {
|
|
22
|
+
needs?: string;
|
|
23
|
+
if?: string;
|
|
24
|
+
permissions?: Record<string, string>;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
outputs?: Record<string, string>;
|
|
27
|
+
steps: Step[];
|
|
28
|
+
}
|
|
29
|
+
interface Doc {
|
|
30
|
+
permissions?: Record<string, string>;
|
|
31
|
+
env?: Record<string, string>;
|
|
32
|
+
jobs: Record<string, Job>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SPEC: ScheduledOpSpec = { name: "pr-review", trigger: { kind: "pull_request" }, reviewAgent: true };
|
|
36
|
+
const gen = (spec: Partial<ScheduledOpSpec> = {}, options = {}) => {
|
|
37
|
+
const out = generateGithubOpPipeline([{ ...SPEC, ...spec }], options);
|
|
38
|
+
return { doc: parseYAML(out.files[0]!.yaml) as unknown as Doc, yaml: out.files[0]!.yaml, name: out.files[0]!.name };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("the review job", () => {
|
|
42
|
+
const { doc } = gen();
|
|
43
|
+
const job = doc.jobs["pr-review"]!;
|
|
44
|
+
|
|
45
|
+
test("holds contents: read and nothing that writes to the forge", () => {
|
|
46
|
+
expect(doc.permissions).toEqual({ contents: "read" });
|
|
47
|
+
expect(job.permissions).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("no step but the checkout's fetches holds a forge token, and none uses actions/checkout", () => {
|
|
51
|
+
for (const step of job.steps) {
|
|
52
|
+
const named = Object.keys(step.env ?? {}).filter((n) => (FORGE_TOKEN_VARS as readonly string[]).includes(n));
|
|
53
|
+
expect(step.uses ?? "").not.toMatch(/^actions\/checkout@/);
|
|
54
|
+
if (step.run?.trimEnd() === checkoutScript(true).trimEnd()) {
|
|
55
|
+
expect(named).toEqual([CHECKOUT_TOKEN_ENV]);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
expect(named).toEqual([]);
|
|
59
|
+
expect(JSON.stringify(step.env ?? {}) + JSON.stringify(step.with ?? {})).not.toMatch(/secrets\.|github\.token/);
|
|
60
|
+
}
|
|
61
|
+
expect(job.env ?? {}).toEqual({});
|
|
62
|
+
expect(doc.env ?? {}).toEqual({});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the checkout carries the base branch's history for the diff", () => {
|
|
66
|
+
expect(job.steps[0]!.run!.trimEnd()).toBe(checkoutScript(true).trimEnd());
|
|
67
|
+
expect(job.steps[0]!.run).toContain("+refs/heads/*:refs/remotes/origin/*");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the Op's step starts without the token variables, names the handoff directory, and hands the review on as the job's outputs", () => {
|
|
71
|
+
const run = job.steps.at(-1)!;
|
|
72
|
+
expect(run.id).toBe("chant-review");
|
|
73
|
+
expect(run.run).toContain("chant run pr-review");
|
|
74
|
+
expect(run.run).toMatch(/^if .*exec env -u CHANT_FORGE_TOKEN/);
|
|
75
|
+
expect(run.run).toContain("trap chant_handover EXIT");
|
|
76
|
+
expect(run.env).toEqual({ CHANT_REVIEW_DIR: ".chant/review" });
|
|
77
|
+
expect(job.outputs).toEqual({ report_0: "${{ steps.chant-review.outputs.report_0 }}" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the Op's step is the last one, so nothing starts beside what the change's code left running", () => {
|
|
81
|
+
expect(job.steps.at(-1)!.id).toBe("chant-review");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("the note job", () => {
|
|
86
|
+
const { doc } = gen();
|
|
87
|
+
const note = doc.jobs["pr-review-note"]!;
|
|
88
|
+
|
|
89
|
+
test("needs the review job and runs when it succeeded", () => {
|
|
90
|
+
expect(note.needs).toBe("pr-review");
|
|
91
|
+
expect(note.if).toBe("needs.pr-review.result == 'success'");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("checks nothing out and holds the pull-request write scope alone", () => {
|
|
95
|
+
expect(note.permissions).toEqual({ "pull-requests": "write" });
|
|
96
|
+
expect(note.steps.some((s) => (s.uses ?? "").startsWith("actions/checkout"))).toBe(false);
|
|
97
|
+
expect(note.steps.some((s) => (s.run ?? "").includes("git "))).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("takes the review as data and posts it with the job's token, naming the Op itself", () => {
|
|
101
|
+
expect(note.steps[0]!.run).toContain("toJSON(needs.pr-review.outputs.report_0)");
|
|
102
|
+
const post = note.steps[1]!;
|
|
103
|
+
expect(post.env).toEqual({ GITHUB_TOKEN: "${{ github.token }}", CHANT_OP: "pr-review" });
|
|
104
|
+
expect(post.run).toContain('import { postReviewNote } from "@intentius/chant/op/activities/review-agent";');
|
|
105
|
+
expect(post.run).toMatch(/npm install .* @intentius\/chant@\d+\.\d+\.\d+/);
|
|
106
|
+
expect(post.run).not.toContain("${{");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the job that runs the change's code has no pull-requests scope the note job lacks, and the note job runs none of the change's code", () => {
|
|
110
|
+
const review = doc.jobs["pr-review"]!;
|
|
111
|
+
expect(JSON.stringify(review)).not.toContain("pull-requests");
|
|
112
|
+
expect(note.steps.map((s) => s.run ?? "").join("\n")).not.toContain("chant run");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("what the shape refuses and carries", () => {
|
|
117
|
+
test("a trigger with no pull request, a finding-mode that writes and a write scope are refused by name", () => {
|
|
118
|
+
expect(() => gen({ trigger: { kind: "push" } })).toThrow(/pr-review.*"push"/);
|
|
119
|
+
expect(() => gen({ findingMode: "comment" })).toThrow(/findingMode "comment".*must be "report"/);
|
|
120
|
+
expect(() => gen({ permissions: { "pull-requests": "write" } })).toThrow(/pull-requests: write/);
|
|
121
|
+
expect(() => gen({ permissions: { "id-token": "write" } })).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("setup steps, per-Op variables, beforeScript and extraScript land in the review job", () => {
|
|
125
|
+
const { doc } = gen(
|
|
126
|
+
{ setup: [{ uses: "aws-actions/configure-aws-credentials@" + "a".repeat(40), with: { "role-to-assume": "arn:aws:iam::1:role/plan" } }], variables: { AWS_REGION: "eu-west-1" }, permissions: { "id-token": "write" } },
|
|
127
|
+
{ beforeScript: ["npm ci"], extraScript: ["echo done"] },
|
|
128
|
+
);
|
|
129
|
+
const job = doc.jobs["pr-review"]!;
|
|
130
|
+
expect(job.env).toEqual({ AWS_REGION: "eu-west-1" });
|
|
131
|
+
expect(job.steps.some((s) => (s.uses ?? "").startsWith("aws-actions/configure-aws-credentials"))).toBe(true);
|
|
132
|
+
expect(job.steps.some((s) => (s.run ?? "").includes("\nnpm ci"))).toBe(true);
|
|
133
|
+
expect(job.steps.at(-1)!.run).toContain("echo done");
|
|
134
|
+
expect(doc.permissions).toEqual({ contents: "read", "id-token": "write" });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a member's directory scopes the review job, and the checkout still runs at the repository root", () => {
|
|
138
|
+
const { doc, name } = gen({}, { member: { name: "network", dir: "infra/network", file: "network.yml" } });
|
|
139
|
+
expect(name).toBe("network-pr-review.yml");
|
|
140
|
+
const job = doc.jobs["pr-review"] as Job & { defaults?: { run?: { "working-directory"?: string } } };
|
|
141
|
+
expect(job.defaults?.run?.["working-directory"]).toBe("infra/network");
|
|
142
|
+
expect(job.steps[0]!["working-directory"]).toBe(".");
|
|
143
|
+
expect((doc.jobs["pr-review-note"] as { defaults?: unknown }).defaults).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("an Op without reviewAgent keeps its one job and its token", () => {
|
|
147
|
+
const out = generateGithubOpPipeline([{ name: "plain", trigger: { kind: "pull_request" } }]);
|
|
148
|
+
const doc = parseYAML(out.files[0]!.yaml) as unknown as Doc;
|
|
149
|
+
expect(Object.keys(doc.jobs)).toEqual(["plain"]);
|
|
150
|
+
expect(out.files[0]!.yaml).toContain("GITHUB_TOKEN");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -16,6 +16,7 @@ import { describe, test, expect } from "vitest";
|
|
|
16
16
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
17
17
|
import { generateGithubOpPipeline } from "./generate-op-pipeline";
|
|
18
18
|
import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
19
|
+
import { ACTION_PINS, usesRef } from "@intentius/chant/action-pins";
|
|
19
20
|
|
|
20
21
|
interface ParsedStep {
|
|
21
22
|
name?: string;
|
|
@@ -73,7 +74,7 @@ describe("generateGithubOpPipeline: one file per scheduled Op", () => {
|
|
|
73
74
|
const job = doc.jobs!["actions-audit"];
|
|
74
75
|
|
|
75
76
|
expect(job["runs-on"]).toBe("ubuntu-latest");
|
|
76
|
-
expect(job.steps[0].uses).toBe(
|
|
77
|
+
expect(job.steps[0].uses).toBe(`${usesRef(ACTION_PINS.checkout7)}`);
|
|
77
78
|
const runStep = job.steps.find((s) => typeof s.run === "string")!;
|
|
78
79
|
expect(runStep.run).toBe("chant run actions-audit");
|
|
79
80
|
});
|
|
@@ -339,7 +340,7 @@ describe("generateGithubOpPipeline: setup steps and additive permissions (#2242)
|
|
|
339
340
|
const steps = doc.jobs!["app-apply"].steps;
|
|
340
341
|
|
|
341
342
|
expect(steps.slice(0, 3).map((s) => s.uses ?? s.run)).toEqual([
|
|
342
|
-
|
|
343
|
+
`${usesRef(ACTION_PINS.checkout7)}`,
|
|
343
344
|
"aws-actions/configure-aws-credentials@v6",
|
|
344
345
|
"install terraform",
|
|
345
346
|
]);
|
|
@@ -397,7 +398,7 @@ describe("generateGithubOpPipeline: setup steps and additive permissions (#2242)
|
|
|
397
398
|
];
|
|
398
399
|
const steps = parseFile(generateGithubOpPipeline(specs).files[0].yaml).jobs!["app-apply"].steps;
|
|
399
400
|
expect(steps.map((s) => s.uses).filter(Boolean)).toEqual([
|
|
400
|
-
|
|
401
|
+
`${usesRef(ACTION_PINS.checkout7)}`,
|
|
401
402
|
"github/codeql-action/upload-sarif@v4",
|
|
402
403
|
"aws-actions/configure-aws-credentials@0e613a0980cbf65ed5b322eb7a1e075d28913a83",
|
|
403
404
|
]);
|
|
@@ -639,7 +640,7 @@ describe("generateGithubOpPipeline: a deployment environment on the Op's job (#2
|
|
|
639
640
|
" runs-on: ubuntu-latest",
|
|
640
641
|
" container: node:22",
|
|
641
642
|
" steps:",
|
|
642
|
-
|
|
643
|
+
` - uses: ${usesRef(ACTION_PINS.checkout7)} # ${ACTION_PINS.checkout7.tag}`,
|
|
643
644
|
" - run: chant run actions-audit",
|
|
644
645
|
" env:",
|
|
645
646
|
" GITHUB_TOKEN: '${{ github.token }}'",
|
|
@@ -96,6 +96,7 @@ import type {
|
|
|
96
96
|
ScheduledOpSpec,
|
|
97
97
|
} from "@intentius/chant/lexicon";
|
|
98
98
|
import { checkoutStep, concurrency, emitWorkflowYAML, jobTokenEnv, onTrigger, opConcurrencyGroup, runSteps } from "./ci-jobs";
|
|
99
|
+
import { assertReviewSplit, reviewJobs, reviewNoteJobName } from "./review-jobs";
|
|
99
100
|
|
|
100
101
|
export type { GenerateGithubOpOptions, GenerateGithubOpResult };
|
|
101
102
|
|
|
@@ -154,6 +155,13 @@ export interface GithubOpPipelineDoc {
|
|
|
154
155
|
* artifact instead (#2256).
|
|
155
156
|
*/
|
|
156
157
|
gatedNoticeDoc?: Record<string, unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* The review note job (#4326), when the spec sets `reviewAgent`. Kept out
|
|
160
|
+
* of {@link jobsDoc} for the reason the gate notice is: a member's
|
|
161
|
+
* `working-directory` default belongs to the job that runs the Op, and a
|
|
162
|
+
* dialect transforms this job on its own terms.
|
|
163
|
+
*/
|
|
164
|
+
reviewNoteDoc?: Record<string, unknown>;
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
/** One generated file: a suggested name plus its pipeline document, pre-emission. */
|
|
@@ -427,7 +435,10 @@ export function buildGithubOpPipelineDocs(
|
|
|
427
435
|
|
|
428
436
|
// A live-resolution read (rate limits) always benefits from a token;
|
|
429
437
|
// creating an issue/PR additionally needs `gh` CLI's own token variable.
|
|
430
|
-
|
|
438
|
+
// A split review (#4326) runs the change's code beside a model: no step of that job holds a forge token.
|
|
439
|
+
const split = spec.reviewAgent === true;
|
|
440
|
+
if (split) assertReviewSplit(spec.name, trigger, findingMode, spec.permissions);
|
|
441
|
+
const stepEnv = split ? {} : jobTokenEnv(findingMode !== "report");
|
|
431
442
|
|
|
432
443
|
const setup = spec.setup ?? [];
|
|
433
444
|
assertSetupSteps(spec.name, setup);
|
|
@@ -450,6 +461,20 @@ export function buildGithubOpPipelineDocs(
|
|
|
450
461
|
);
|
|
451
462
|
steps.push(...runSteps(extraScript));
|
|
452
463
|
|
|
464
|
+
const review = split
|
|
465
|
+
? reviewJobs({
|
|
466
|
+
name: spec.name,
|
|
467
|
+
jobName,
|
|
468
|
+
image,
|
|
469
|
+
invocation,
|
|
470
|
+
setup: setup.map(setupStepDoc),
|
|
471
|
+
beforeScript,
|
|
472
|
+
extraScript,
|
|
473
|
+
...(spec.variables ? { variables: spec.variables } : {}),
|
|
474
|
+
...(options.member ? { member: options.member } : {}),
|
|
475
|
+
})
|
|
476
|
+
: undefined;
|
|
477
|
+
|
|
453
478
|
const doc: GithubOpPipelineDoc = {
|
|
454
479
|
name: spec.name,
|
|
455
480
|
on: onTrigger(trigger),
|
|
@@ -492,6 +517,11 @@ export function buildGithubOpPipelineDocs(
|
|
|
492
517
|
},
|
|
493
518
|
...(gated ? { gatedNoticeDoc: { [`${jobName}-gate-notice`]: gateNoticeJob(jobName, image) } } : {}),
|
|
494
519
|
};
|
|
520
|
+
// A split review (#4326): the Op's job is the review job, and the note job rides beside it.
|
|
521
|
+
if (review) {
|
|
522
|
+
doc.jobsDoc = { [jobName]: { ...review.review, ...(spec.environment ? { environment: environmentDoc(spec.environment) } : {}) } };
|
|
523
|
+
doc.reviewNoteDoc = { [reviewNoteJobName(jobName)]: review.note };
|
|
524
|
+
}
|
|
495
525
|
|
|
496
526
|
files.push({ name: `${spec.name}.yml`, doc });
|
|
497
527
|
}
|
|
@@ -550,8 +580,8 @@ export function emitOpPipelineYAML(doc: GithubOpPipelineDoc): string {
|
|
|
550
580
|
// The gated-apply notice job rides in `jobs:` beside the Op's own job; it
|
|
551
581
|
// is carried separately on the doc so a dialect transforms it on its own.
|
|
552
582
|
// A dialect's `header` note (#2257) is absent on github.
|
|
553
|
-
const { gatedNoticeDoc, ...workflow } = doc;
|
|
554
|
-
return emitWorkflowYAML({ ...workflow, jobsDoc: { ...doc.jobsDoc, ...(gatedNoticeDoc ?? {}) } });
|
|
583
|
+
const { gatedNoticeDoc, reviewNoteDoc, ...workflow } = doc;
|
|
584
|
+
return emitWorkflowYAML({ ...workflow, jobsDoc: { ...doc.jobsDoc, ...(gatedNoticeDoc ?? {}), ...(reviewNoteDoc ?? {}) } });
|
|
555
585
|
}
|
|
556
586
|
|
|
557
587
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
3
3
|
import { generateGithubOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
4
|
+
import { ACTION_PINS, usesRef } from "@intentius/chant/action-pins";
|
|
4
5
|
|
|
5
6
|
const spec: OpWavesSpec = {
|
|
6
7
|
name: "migrations",
|
|
@@ -26,7 +27,7 @@ describe("generateGithubOpWavesPipeline", () => {
|
|
|
26
27
|
expect(yaml).toContain("run: chant run wave --spec waves.json --wave 2 --decide");
|
|
27
28
|
expect(yaml).toMatch(/wave-2-prod-share-2:\n\s+runs-on: ubuntu-latest\n\s+needs:\n\s+- wave-2-prod-decide/);
|
|
28
29
|
expect(yaml).toContain("run: chant run wave --spec waves.json --wave 2 --share 2");
|
|
29
|
-
expect(yaml).toContain(
|
|
30
|
+
expect(yaml).toContain(`uses: ${usesRef(ACTION_PINS.uploadArtifact4)}`);
|
|
30
31
|
expect(yaml).toContain("name: migrations-wave-2-decision");
|
|
31
32
|
expect(yaml).toContain("environment:\n name: production");
|
|
32
33
|
});
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One workflow for the whole spec, on a push to its branches (and by hand),
|
|
5
5
|
* one run at a time. Each job is one of core's `opWaveJobs`: a wave, or a wide
|
|
6
|
-
* wave's deciding job and share jobs, chained by `needs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* wave's deciding job and share jobs, chained by `needs:`. Re-running the
|
|
7
|
+
* failed jobs after `chant approve` carries on from that wave.
|
|
8
|
+
*
|
|
9
|
+
* Each job keeps its outcome file as an artifact whatever its exit code
|
|
10
|
+
* (#4322), and a job with `needs:` downloads every artifact of the run into
|
|
11
|
+
* the outcome directory first. A wave or deciding job after the first runs
|
|
12
|
+
* unless the run was cancelled (`if: !cancelled()`), so it runs after a
|
|
13
|
+
* failure in the wave before: it blocks the runs that depend on a failed one
|
|
14
|
+
* and applies the rest, and it stops by itself when an earlier wave waits at
|
|
15
|
+
* its gate (exit 3) or applied nothing for another reason. A share job still
|
|
16
|
+
* needs its deciding job to succeed.
|
|
9
17
|
*
|
|
10
18
|
* Every job checks out the full history, because the runner reads the gate
|
|
11
19
|
* policy from the spec file at the base commit and records pending gates on
|
|
@@ -43,7 +51,7 @@
|
|
|
43
51
|
* and cannot request one for another group's identity.
|
|
44
52
|
*/
|
|
45
53
|
|
|
46
|
-
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
54
|
+
import { notifyVariables, opWaveGroupOf, opWaveJobs, opWaveOutcomeDir, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
47
55
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
48
56
|
import {
|
|
49
57
|
assertEnvironment,
|
|
@@ -60,7 +68,7 @@ import {
|
|
|
60
68
|
setupStepDoc,
|
|
61
69
|
} from "@intentius/chant/ci-jobs";
|
|
62
70
|
import { OIDC_REQUEST_VARS, TRADE_SPEC_ENV, tradeLine, type CloudTradeSpec } from "@intentius/chant/ci-identity";
|
|
63
|
-
import { checkoutStep, concurrency, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs";
|
|
71
|
+
import { checkoutStep, concurrency, DOWNLOAD_ARTIFACT, downloadArtifactStep, keepRecordStep, runSteps } from "./ci-jobs";
|
|
64
72
|
import { emitOpPipelineYAML, type GithubOpPipelineDoc } from "./generate-op-pipeline";
|
|
65
73
|
|
|
66
74
|
/** The default image: it carries git, which the runner needs to read the base commit. */
|
|
@@ -71,6 +79,11 @@ export function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wave: numb
|
|
|
71
79
|
return `${spec.name}-wave-${wave}-decision`;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
/** The artifact a job keeps its outcome file in (#4322). */
|
|
83
|
+
export function opWaveOutcomeArtifact(spec: Pick<OpWavesSpec, "name">, jobName: string): string {
|
|
84
|
+
return `${spec.name}-${jobName}-outcome`;
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
/** The workflow document, before emission, and the jobs it holds. */
|
|
75
88
|
export function buildGithubOpWavesDoc(
|
|
76
89
|
spec: OpWavesSpec,
|
|
@@ -100,6 +113,10 @@ export function buildGithubOpWavesDoc(
|
|
|
100
113
|
if (job.kind === "share") {
|
|
101
114
|
steps.push(downloadArtifactStep(`Download wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), dirnameOf(job.record!)));
|
|
102
115
|
}
|
|
116
|
+
if (job.needs.length > 0) {
|
|
117
|
+
// #4322: every artifact of the run, so the job reads each earlier job's outcome file.
|
|
118
|
+
steps.push({ name: "Download the earlier jobs' outcomes", uses: DOWNLOAD_ARTIFACT, with: { path: opWaveOutcomeDir(spec) } });
|
|
119
|
+
}
|
|
103
120
|
const later: Array<Record<string, unknown>> = [];
|
|
104
121
|
later.push(...[...(wave.setup ?? []), ...(group?.setup ?? [])].map(setupStepDoc), ...runSteps(options.beforeScript ?? []));
|
|
105
122
|
later.push({ name: describeJob(job.kind, job.wave, wave.name, job.share, job.group), run: job.command.join(" ") });
|
|
@@ -108,12 +125,15 @@ export function buildGithubOpWavesDoc(
|
|
|
108
125
|
if (job.kind === "decide") {
|
|
109
126
|
steps.push(keepRecordStep(`Keep wave ${job.wave}'s decision`, opWaveRecordArtifact(spec, job.wave), job.record!));
|
|
110
127
|
}
|
|
128
|
+
steps.push(keepRecordStep(`Keep the job's outcome`, opWaveOutcomeArtifact(spec, job.jobName), job.outcome));
|
|
111
129
|
const environment = group?.environment ?? wave.environment;
|
|
112
130
|
const idTokens = group?.idTokens ?? wave.idTokens;
|
|
113
131
|
const idToken = idTokens !== undefined || cloud !== undefined;
|
|
114
132
|
jobsDoc[job.jobName] = {
|
|
115
133
|
"runs-on": githubRunsOn(group?.runner ?? wave.runner ?? spec.runner),
|
|
116
134
|
...(job.needs.length > 0 ? { needs: job.needs } : {}),
|
|
135
|
+
// #4322: a wave's jobs run after a failure in the wave before; a share job runs only on its deciding job's success.
|
|
136
|
+
...(job.needs.length > 0 && job.kind !== "share" ? { if: "!cancelled()" } : {}),
|
|
117
137
|
container: image,
|
|
118
138
|
...(environment ? { environment: environmentDoc(environment) } : {}),
|
|
119
139
|
// #3914: a job's own permissions replace the workflow's, so they repeat them.
|
|
@@ -23,6 +23,7 @@ import { artifactRoot, generateGithubPipeline } from "./generate-pipeline";
|
|
|
23
23
|
import { resolveComponentGraph, DependencyCycleError, UnknownDependencyError, type DriverComponent } from "@intentius/chant/components/driver";
|
|
24
24
|
import { searchService } from "@intentius/chant/components/pilots/alb-ecs.pilot";
|
|
25
25
|
import { ordersTable } from "@intentius/chant/components/pilots/dynamodb.pilot";
|
|
26
|
+
import { ACTION_PINS, usesRef } from "@intentius/chant/action-pins";
|
|
26
27
|
|
|
27
28
|
/** Real pilot components (#555) plus the shared-alb infra `search-service` depends on, as a realistic multi-wave input. */
|
|
28
29
|
function pilotComponents(): DriverComponent[] {
|
|
@@ -67,21 +68,21 @@ describe("generateGithubPipeline: cross-stack output threading (artifacts)", ()
|
|
|
67
68
|
// job, so it dumps them and uploads the file as a workflow artifact.
|
|
68
69
|
const shared = jobs["shared-alb"];
|
|
69
70
|
expect(runLines(shared).some((line) => line.includes("--dump-outputs shared-alb.outputs.json"))).toBe(true);
|
|
70
|
-
const uploadStep = shared.steps.find((s) => s.uses ===
|
|
71
|
+
const uploadStep = shared.steps.find((s) => s.uses === `${usesRef(ACTION_PINS.uploadArtifact4)}`);
|
|
71
72
|
expect(uploadStep?.with).toEqual({ name: "shared-alb-outputs", path: "shared-alb.outputs.json" });
|
|
72
73
|
|
|
73
74
|
// Consumer: search-service downloads shared-alb's uploaded artifact and
|
|
74
75
|
// seeds from the dumped file so its stackOutput() references resolve.
|
|
75
76
|
const svc = jobs["search-service"];
|
|
76
|
-
const downloadStep = svc.steps.find((s) => s.uses ===
|
|
77
|
+
const downloadStep = svc.steps.find((s) => s.uses === `${usesRef(ACTION_PINS.downloadArtifact4)}`);
|
|
77
78
|
expect(downloadStep?.with).toEqual({ name: "shared-alb-outputs", path: "." });
|
|
78
79
|
expect(runLines(svc).some((line) => line.includes("--seed-outputs shared-alb.outputs.json"))).toBe(true);
|
|
79
80
|
|
|
80
81
|
// A component nothing depends on neither dumps nor uploads an artifact.
|
|
81
82
|
const orders = jobs["orders-table"];
|
|
82
83
|
expect(runLines(orders).some((line) => line.includes("--dump-outputs"))).toBe(false);
|
|
83
|
-
expect(orders.steps.some((s) => s.uses ===
|
|
84
|
-
expect(orders.steps.some((s) => s.uses ===
|
|
84
|
+
expect(orders.steps.some((s) => s.uses === `${usesRef(ACTION_PINS.uploadArtifact4)}`)).toBe(false);
|
|
85
|
+
expect(orders.steps.some((s) => s.uses === `${usesRef(ACTION_PINS.downloadArtifact4)}`)).toBe(false);
|
|
85
86
|
expect(runLines(orders).some((line) => line.includes("--seed-outputs"))).toBe(false);
|
|
86
87
|
});
|
|
87
88
|
});
|
|
@@ -104,7 +105,7 @@ describe("generateGithubPipeline: structurally valid YAML", () => {
|
|
|
104
105
|
expect(job.steps.length).toBeGreaterThan(0);
|
|
105
106
|
// Every job starts with a checkout step — a job runs on a fresh runner
|
|
106
107
|
// with no repo present, unlike GitLab's auto-cloned workspace.
|
|
107
|
-
expect(job.steps[0].uses).toBe(
|
|
108
|
+
expect(job.steps[0].uses).toBe(`${usesRef(ACTION_PINS.checkout7)}`);
|
|
108
109
|
}
|
|
109
110
|
});
|
|
110
111
|
|
|
@@ -327,7 +328,7 @@ describe("generateGithubPipeline: a promote job (#2575)", () => {
|
|
|
327
328
|
const jobs = parsedJobs(result.yaml);
|
|
328
329
|
|
|
329
330
|
const upload = jobs["api"].steps.find((s) => s.name === "Upload api build archive")!;
|
|
330
|
-
expect(upload.uses).toBe(
|
|
331
|
+
expect(upload.uses).toBe(`${usesRef(ACTION_PINS.uploadArtifact4)}`);
|
|
331
332
|
expect(upload.with).toEqual({
|
|
332
333
|
name: "api-archive",
|
|
333
334
|
path: "dist/images/api.tar",
|
|
@@ -339,7 +340,7 @@ describe("generateGithubPipeline: a promote job (#2575)", () => {
|
|
|
339
340
|
const promote = jobs["promote-prod"];
|
|
340
341
|
expect(promote.needs).toEqual(["api", "shared-alb", "worker"]);
|
|
341
342
|
expect(promote.container).toBe("node:22");
|
|
342
|
-
const downloads = promote.steps.filter((s) => s.uses ===
|
|
343
|
+
const downloads = promote.steps.filter((s) => s.uses === `${usesRef(ACTION_PINS.downloadArtifact4)}`);
|
|
343
344
|
expect(downloads.map((s) => s.with)).toEqual([
|
|
344
345
|
{ name: "api-archive", path: "dist/images" },
|
|
345
346
|
{ name: "worker-archive", path: "." },
|
|
@@ -440,7 +441,7 @@ describe("generateGithubPipeline: gated waves (#3049)", () => {
|
|
|
440
441
|
|
|
441
442
|
const steps = parsed.jobs["wave-2"].steps;
|
|
442
443
|
expect(steps[0].with).toEqual({ "fetch-depth": 0 });
|
|
443
|
-
expect(steps[1]).toMatchObject({ uses:
|
|
444
|
+
expect(steps[1]).toMatchObject({ uses: `${usesRef(ACTION_PINS.downloadArtifact4)}`, with: { name: "fan-out-record-wave-1", path: ".chant" } });
|
|
444
445
|
// The default image is the pr-loop one: node:22, with OpenTofu installed when missing.
|
|
445
446
|
expect(parsed.jobs["wave-2"]).toMatchObject({ container: "node:22" });
|
|
446
447
|
expect(steps[2].run).toMatch(/^command -v tofu /);
|
|
@@ -449,7 +450,7 @@ describe("generateGithubPipeline: gated waves (#3049)", () => {
|
|
|
449
450
|
);
|
|
450
451
|
expect(steps[4]).toMatchObject({ if: "always()", with: { name: "fan-out-record-wave-2", path: ".chant/fan-out.json" } });
|
|
451
452
|
// Wave 1 has nothing to download.
|
|
452
|
-
expect(parsed.jobs["wave-1"].steps.some((s) => s.uses ===
|
|
453
|
+
expect(parsed.jobs["wave-1"].steps.some((s) => s.uses === `${usesRef(ACTION_PINS.downloadArtifact4)}`)).toBe(false);
|
|
453
454
|
});
|
|
454
455
|
|
|
455
456
|
test("a gated-wave pipeline has no promote job", () => {
|
|
@@ -76,6 +76,10 @@ import {
|
|
|
76
76
|
prLoopPostJob,
|
|
77
77
|
prLoopSetup,
|
|
78
78
|
PR_PLAN_NOTE_JOB,
|
|
79
|
+
PR_LOCK_JOB,
|
|
80
|
+
PR_UNLOCK_JOB,
|
|
81
|
+
PR_UNLOCK_COMMAND,
|
|
82
|
+
prLockCommand,
|
|
79
83
|
prLoopWaveJobs,
|
|
80
84
|
prLoopWithWaves,
|
|
81
85
|
prEnvJobs,
|
|
@@ -90,8 +94,7 @@ import type {
|
|
|
90
94
|
ComponentPipelineResult as GenerateGithubResult,
|
|
91
95
|
PipelineMember,
|
|
92
96
|
} from "@intentius/chant/lexicon";
|
|
93
|
-
import { afterEarlierWave, checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs";
|
|
94
|
-
import { actionRef } from "../action-pins";
|
|
97
|
+
import { afterEarlierWave, CACHE_RESTORE, CACHE_SAVE, CHECKOUT, checkoutStep, concurrency, downloadArtifactStep, emitWorkflowYAML, keepRecordStep, runSteps, UPLOAD_ARTIFACT } from "./ci-jobs";
|
|
95
98
|
|
|
96
99
|
export type { GeneratedJob, GenerateGithubOptions, GenerateGithubResult };
|
|
97
100
|
|
|
@@ -270,13 +273,13 @@ function prLoopGithubDoc(
|
|
|
270
273
|
const recordKey = (wave?: number) => `${recordPrefix}\${{ github.run_id }}-\${{ github.run_attempt }}${wave ? `-wave-${wave}` : ""}`;
|
|
271
274
|
const restoreRecord = (wave?: number) => ({
|
|
272
275
|
name: wave ? "Restore the newest record of this commit's apply" : "Restore the record of an earlier attempt at this commit",
|
|
273
|
-
uses:
|
|
276
|
+
uses: CACHE_RESTORE,
|
|
274
277
|
with: { path: recordPath, key: recordKey(wave), "restore-keys": recordPrefix },
|
|
275
278
|
});
|
|
276
279
|
const saveRecord = (wave?: number) => ({
|
|
277
280
|
name: wave ? "Keep the record for the next wave and a re-run" : "Keep the record for a re-run",
|
|
278
281
|
if: "always()",
|
|
279
|
-
uses:
|
|
282
|
+
uses: CACHE_SAVE,
|
|
280
283
|
with: { path: recordPath, key: recordKey(wave) },
|
|
281
284
|
});
|
|
282
285
|
// #4296: the step that trades the job's OIDC token for its cloud identity, before the checkout.
|
|
@@ -380,7 +383,7 @@ function prLoopGithubDoc(
|
|
|
380
383
|
const post = prLoopPostJob(forgeName, env, member?.name);
|
|
381
384
|
const noteSteps: Array<Record<string, unknown>> = [
|
|
382
385
|
// The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
|
|
383
|
-
{ name: "Check out the target branch's commit", uses:
|
|
386
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}" } },
|
|
384
387
|
...runSteps(beforeScript),
|
|
385
388
|
{ name: "Take the plan report from the plan job", run: takeOverScript("plan", PR_LOOP_REPORT_DIR, forgeName) },
|
|
386
389
|
{ name: "Keep the plan report", if: "always()", uses: UPLOAD_ARTIFACT, with: { name: reportName(plan), path: reportPath, "if-no-files-found": "ignore", "include-hidden-files": true } },
|
|
@@ -388,9 +391,46 @@ function prLoopGithubDoc(
|
|
|
388
391
|
];
|
|
389
392
|
// #4298: with a pull request environment the pull_request trigger fires on close too, and the plan skips that event.
|
|
390
393
|
const prEnv = loop.prEnvironment ? prEnvGithubJobs(loop.forge ?? "github", image, beforeScript, extraScript, loop, member) : undefined;
|
|
394
|
+
// #4327: with ci.locks: plan the pull_request trigger fires on close too, for the unlock job, and the plan skips that event.
|
|
395
|
+
const locks = loop.locks === "plan";
|
|
396
|
+
const closes = Boolean(prEnv) || locks;
|
|
397
|
+
const lockJobs: Record<string, unknown> = locks
|
|
398
|
+
? {
|
|
399
|
+
[PR_LOCK_JOB]: {
|
|
400
|
+
// The plan job's outputs carry the report; a plan that was skipped or failed leaves this job skipped.
|
|
401
|
+
needs: ["plan"],
|
|
402
|
+
if: "github.event_name == 'pull_request'",
|
|
403
|
+
"runs-on": GITHUB_RUNNER,
|
|
404
|
+
container: image,
|
|
405
|
+
permissions: { contents: "write", "pull-requests": "read", statuses: "write" },
|
|
406
|
+
steps: [
|
|
407
|
+
// The target branch's commit: this job pushes chant/lifecycle, so none of the change's code runs in it.
|
|
408
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}", "fetch-depth": 0 } },
|
|
409
|
+
...runSteps(beforeScript),
|
|
410
|
+
{ name: "Take the plan report from the plan job", run: takeOverScript("plan", PR_LOOP_REPORT_DIR, forgeName) },
|
|
411
|
+
{
|
|
412
|
+
name: "Lock the components the plan selected",
|
|
413
|
+
env: { PR_NUMBER: "${{ github.event.pull_request.number }}", HEAD_SHA: "${{ github.event.pull_request.head.sha }}", GITHUB_TOKEN: GITHUB_JOB_TOKEN },
|
|
414
|
+
run: prLockCommand(forgeName),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
[PR_UNLOCK_JOB]: {
|
|
419
|
+
if: "github.event_name == 'pull_request' && github.event.action == 'closed'",
|
|
420
|
+
"runs-on": GITHUB_RUNNER,
|
|
421
|
+
container: image,
|
|
422
|
+
permissions: { contents: "write" },
|
|
423
|
+
steps: [
|
|
424
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}", "fetch-depth": 0 } },
|
|
425
|
+
...runSteps(beforeScript),
|
|
426
|
+
{ name: "Release the pull request's locks", env: { PR_NUMBER: "${{ github.event.pull_request.number }}" }, run: PR_UNLOCK_COMMAND },
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
}
|
|
430
|
+
: {};
|
|
391
431
|
const jobsDoc: Record<string, unknown> = {
|
|
392
432
|
plan: {
|
|
393
|
-
if:
|
|
433
|
+
if: closes ? "github.event_name == 'pull_request' && github.event.action != 'closed'" : "github.event_name == 'pull_request'",
|
|
394
434
|
"runs-on": GITHUB_RUNNER,
|
|
395
435
|
container: image,
|
|
396
436
|
permissions: withIdToken({ contents: "read" }),
|
|
@@ -406,6 +446,7 @@ function prLoopGithubDoc(
|
|
|
406
446
|
permissions: { actions: "read", contents: "read", "pull-requests": "write", statuses: "write" },
|
|
407
447
|
steps: noteSteps,
|
|
408
448
|
},
|
|
449
|
+
...lockJobs,
|
|
409
450
|
...Object.fromEntries(applyJobs),
|
|
410
451
|
...(prEnv ? prEnv.jobs : {}),
|
|
411
452
|
};
|
|
@@ -415,6 +456,7 @@ function prLoopGithubDoc(
|
|
|
415
456
|
: forgejo
|
|
416
457
|
? { pull_request: { branches: [branch] }, push: { branches: [branch], tags: [GREEN_TAG_PATTERN] } }
|
|
417
458
|
: { pull_request: { branches: [branch] }, push: { branches: [branch] }, workflow_run: { workflows: [GREEN_TICK_WORKFLOW], types: ["completed"] }, workflow_dispatch: {} };
|
|
459
|
+
if (closes) on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
|
|
418
460
|
if (prEnv) {
|
|
419
461
|
on.pull_request = { branches: [branch], types: ["opened", "synchronize", "reopened", "closed"] };
|
|
420
462
|
on.schedule = [{ cron: PR_ENV_SWEEP_CRON }];
|
|
@@ -428,10 +470,16 @@ function prLoopGithubDoc(
|
|
|
428
470
|
...(rooted ? {} : { defaults: { run: { "working-directory": member!.dir } } }),
|
|
429
471
|
env: { ...options.variables, CHANT_ENV: env },
|
|
430
472
|
jobsDoc,
|
|
431
|
-
stages: ["plan", PR_PLAN_NOTE_JOB, ...applyNames, ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
473
|
+
stages: ["plan", PR_PLAN_NOTE_JOB, ...(locks ? [PR_LOCK_JOB, PR_UNLOCK_JOB] : []), ...applyNames, ...(prEnv ? ["env-up", PR_ENV_POST_JOB, "env-down", "env-sweep"] : [])],
|
|
432
474
|
jobs: [
|
|
433
475
|
{ jobName: "plan", component: "pull request plan", stage: "plan", needs: [] },
|
|
434
476
|
{ jobName: PR_PLAN_NOTE_JOB, component: "pull request plan note", stage: PR_PLAN_NOTE_JOB, needs: ["plan"] },
|
|
477
|
+
...(locks
|
|
478
|
+
? [
|
|
479
|
+
{ jobName: PR_LOCK_JOB, component: "pull request lock", stage: PR_LOCK_JOB, needs: ["plan"] },
|
|
480
|
+
{ jobName: PR_UNLOCK_JOB, component: "pull request unlock", stage: PR_UNLOCK_JOB, needs: [] },
|
|
481
|
+
]
|
|
482
|
+
: []),
|
|
435
483
|
...applyNames.map((name, i) => ({
|
|
436
484
|
jobName: name,
|
|
437
485
|
component: name === "record" ? "merge record" : loop.applyBeforeMerge ? "apply before merge" : loop.waveGate ? `green apply, wave ${i}` : "green apply",
|
|
@@ -519,7 +567,7 @@ function prEnvGithubJobs(
|
|
|
519
567
|
permissions: writes,
|
|
520
568
|
steps: [
|
|
521
569
|
// The target branch's commit: the chant this job runs is the target branch's, and none of the change's code is here.
|
|
522
|
-
{ name: "Check out the target branch's commit", uses:
|
|
570
|
+
{ name: "Check out the target branch's commit", uses: CHECKOUT, with: { ref: "${{ github.event.pull_request.base.sha }}", "fetch-depth": 0 } },
|
|
523
571
|
...runSteps(beforeScript),
|
|
524
572
|
{ name: "Take the environment's report from the env-up job", run: takeOverScript("env-up", PR_ENV_REPORT_DIR, forge) },
|
|
525
573
|
keep("env-up"),
|
|
@@ -534,7 +582,7 @@ function prEnvGithubJobs(
|
|
|
534
582
|
permissions: withIdToken(writes),
|
|
535
583
|
steps: [
|
|
536
584
|
...tradeStep,
|
|
537
|
-
{ uses:
|
|
585
|
+
{ uses: CHECKOUT, with: { "fetch-depth": 0, ...(ref ? { ref } : {}) } },
|
|
538
586
|
...runSteps(prLoopSetup(image)),
|
|
539
587
|
...runSteps(beforeScript),
|
|
540
588
|
{ name, env: j.env, run: j.command },
|