@intentius/chant-lexicon-github 0.116.0 → 0.118.0
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-op-waves-pipeline.d.ts +12 -0
- package/dist/components/generate-op-waves-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/generate-op-waves-pipeline.test.ts +19 -0
- package/src/components/generate-op-waves-pipeline.ts +61 -6
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* deciding job uploads its decision, even when it waits, and each share job
|
|
14
14
|
* downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
|
|
15
15
|
* and applies its dialect.
|
|
16
|
+
*
|
|
17
|
+
* With `resume` on the spec (#3683), a second workflow, `<name>-resume.yml`,
|
|
18
|
+
* runs `chant run resume --op <name>` on that schedule: it re-runs the failed
|
|
19
|
+
* jobs of a run whose waiting wave has been approved since, with the job's
|
|
20
|
+
* token and `actions: write`.
|
|
21
|
+
*
|
|
22
|
+
* When a wave's approval is `pr-review` (#3684), the workflow also asks for
|
|
23
|
+
* `pull-requests: read`, so a wave can read the merged pull request's
|
|
24
|
+
* reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
|
|
25
|
+
* --record-plans` on each pull request to record the digests its head plans.
|
|
16
26
|
*/
|
|
17
27
|
import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
18
28
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
@@ -25,6 +35,8 @@ export declare function opWaveRecordArtifact(spec: Pick<OpWavesSpec, "name">, wa
|
|
|
25
35
|
export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): {
|
|
26
36
|
doc: GithubOpPipelineDoc;
|
|
27
37
|
jobs: OpWavesPipelineResult["jobs"];
|
|
38
|
+
resumeDoc?: GithubOpPipelineDoc;
|
|
39
|
+
plansDoc?: GithubOpPipelineDoc;
|
|
28
40
|
};
|
|
29
41
|
/** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
|
|
30
42
|
export declare function generateGithubOpWavesPipeline(spec: OpWavesSpec, options: OpWavesPipelineOptions): OpWavesPipelineResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAoC,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAClG,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,yFAAyF;AACzF,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1F;AAOD,qEAAqE;AACrE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,GAC9B;IAAE,GAAG,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAgEpI;AA4CD,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAMvH"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "a0a217bf875f2999a78da91e06dd0fe33165e52b6077800a76654f75c9e8354d",
|
|
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": "db3b08fa2fb816149260d6ea9f821c842bbd6317bcb181114380fbb14fd7a734"
|
|
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.118.0",
|
|
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.118.0",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -30,4 +30,23 @@ describe("generateGithubOpWavesPipeline", () => {
|
|
|
30
30
|
expect(yaml).toContain("name: migrations-wave-2-decision");
|
|
31
31
|
expect(yaml).toContain("environment:\n name: production");
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
test("resume renders a scheduled workflow that re-runs approved waves with actions: write (#3683)", () => {
|
|
35
|
+
const { files } = generateGithubOpWavesPipeline({ ...spec, resume: { schedule: "*/10 * * * *" } }, { specFile: "waves.json" });
|
|
36
|
+
expect(files.map((f) => f.name)).toEqual([`${spec.name}.yml`, `${spec.name}-resume.yml`]);
|
|
37
|
+
const yaml = files[1]!.yaml;
|
|
38
|
+
expect(yaml).toContain("- cron: '*/10 * * * *'");
|
|
39
|
+
expect(yaml).toMatch(/permissions:\n\s+contents: read\n\s+actions: write/);
|
|
40
|
+
expect(yaml).toContain(`run: chant run resume --op ${spec.name}`);
|
|
41
|
+
expect(yaml).toContain("GITHUB_TOKEN: '${{ github.token }}'");
|
|
42
|
+
expect(generateGithubOpWavesPipeline(spec, { specFile: "waves.json" }).files).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a pr-review wave adds a pull request workflow that records the head's plans (#3684)", () => {
|
|
46
|
+
const { files } = generateGithubOpWavesPipeline({ ...spec, waves: spec.waves.map((w, i) => (i === spec.waves.length - 1 ? { ...w, approval: "pr-review" as const } : w)) }, { specFile: "waves.json" });
|
|
47
|
+
expect(files.map((f) => f.name)).toEqual([`${spec.name}.yml`, `${spec.name}-plans.yml`]);
|
|
48
|
+
expect(files[1]!.yaml).toContain("pull_request:");
|
|
49
|
+
expect(files[1]!.yaml).toContain("run: chant run wave --spec waves.json --record-plans");
|
|
50
|
+
expect(files[0]!.yaml).toMatch(/permissions:\n\s+contents: write\n\s+pull-requests: read/);
|
|
51
|
+
});
|
|
33
52
|
});
|
|
@@ -13,9 +13,19 @@
|
|
|
13
13
|
* deciding job uploads its decision, even when it waits, and each share job
|
|
14
14
|
* downloads it. The forgejo generator reuses {@link buildGithubOpWavesDoc}
|
|
15
15
|
* and applies its dialect.
|
|
16
|
+
*
|
|
17
|
+
* With `resume` on the spec (#3683), a second workflow, `<name>-resume.yml`,
|
|
18
|
+
* runs `chant run resume --op <name>` on that schedule: it re-runs the failed
|
|
19
|
+
* jobs of a run whose waiting wave has been approved since, with the job's
|
|
20
|
+
* token and `actions: write`.
|
|
21
|
+
*
|
|
22
|
+
* When a wave's approval is `pr-review` (#3684), the workflow also asks for
|
|
23
|
+
* `pull-requests: read`, so a wave can read the merged pull request's
|
|
24
|
+
* reviews, and a second workflow, `<name>-plans.yml`, runs `chant run wave
|
|
25
|
+
* --record-plans` on each pull request to record the digests its head plans.
|
|
16
26
|
*/
|
|
17
27
|
|
|
18
|
-
import { opWaveJobs, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
28
|
+
import { opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
19
29
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
20
30
|
import { actionRef } from "../action-pins";
|
|
21
31
|
import {
|
|
@@ -42,9 +52,10 @@ function dirOf(path: string): string {
|
|
|
42
52
|
export function buildGithubOpWavesDoc(
|
|
43
53
|
spec: OpWavesSpec,
|
|
44
54
|
options: OpWavesPipelineOptions,
|
|
45
|
-
): { doc: GithubOpPipelineDoc; jobs: OpWavesPipelineResult["jobs"] } {
|
|
55
|
+
): { doc: GithubOpPipelineDoc; jobs: OpWavesPipelineResult["jobs"]; resumeDoc?: GithubOpPipelineDoc; plansDoc?: GithubOpPipelineDoc } {
|
|
46
56
|
const jobs = opWaveJobs(spec, options.specFile);
|
|
47
57
|
const image = options.image ?? OP_WAVES_IMAGE;
|
|
58
|
+
const byReview = spec.waves.some((w) => w.approval === "pr-review");
|
|
48
59
|
for (const wave of spec.waves) {
|
|
49
60
|
assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
|
|
50
61
|
if (wave.environment) assertEnvironment(`${spec.name}/${wave.name}`, wave.environment);
|
|
@@ -96,10 +107,51 @@ export function buildGithubOpWavesDoc(
|
|
|
96
107
|
on: { push: { branches: spec.branches?.length ? spec.branches : ["main"] }, workflow_dispatch: {} },
|
|
97
108
|
...(options.variables && Object.keys(options.variables).length > 0 ? { env: options.variables } : {}),
|
|
98
109
|
concurrency: { group: spec.name, "cancel-in-progress": false },
|
|
99
|
-
permissions: { contents: "write" },
|
|
110
|
+
permissions: { contents: "write", ...(byReview ? { "pull-requests": "read" } : {}) },
|
|
100
111
|
jobsDoc,
|
|
101
112
|
};
|
|
102
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
doc,
|
|
115
|
+
jobs,
|
|
116
|
+
...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}),
|
|
117
|
+
...(byReview ? { plansDoc: buildPlansDoc(spec, options) } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The scheduled resume workflow (#3683). */
|
|
122
|
+
function buildResumeDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): GithubOpPipelineDoc {
|
|
123
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout") }];
|
|
124
|
+
for (const line of options.beforeScript ?? []) steps.push({ run: line });
|
|
125
|
+
steps.push({
|
|
126
|
+
name: `Resume the waves of ${spec.name} whose approval arrived`,
|
|
127
|
+
run: opWavesResumeCommand(spec).join(" "),
|
|
128
|
+
env: { GITHUB_TOKEN: "${{ github.token }}" },
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
name: `${spec.name}-resume`,
|
|
132
|
+
on: { schedule: [{ cron: spec.resume!.schedule }], workflow_dispatch: {} },
|
|
133
|
+
concurrency: { group: `${spec.name}-resume`, "cancel-in-progress": true },
|
|
134
|
+
// contents: read fetches chant/lifecycle; actions: write re-runs a waiting run.
|
|
135
|
+
permissions: { contents: "read", actions: "write" },
|
|
136
|
+
jobsDoc: { resume: { "runs-on": "ubuntu-latest", container: options.image ?? OP_WAVES_IMAGE, steps } },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The pull request workflow a `pr-review` wave needs (#3684): record the digests each head plans. */
|
|
141
|
+
function buildPlansDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): GithubOpPipelineDoc {
|
|
142
|
+
const steps: Array<Record<string, unknown>> = [{ uses: actionRef("actions/checkout"), with: { "fetch-depth": 0 } }];
|
|
143
|
+
for (const step of spec.waves[0]?.setup ?? []) steps.push({ ...step });
|
|
144
|
+
for (const line of options.beforeScript ?? []) steps.push({ run: line });
|
|
145
|
+
steps.push({ name: "Plan every wave at the head and record its digests", run: ["chant", "run", "wave", "--spec", options.specFile, "--record-plans"].join(" ") });
|
|
146
|
+
return {
|
|
147
|
+
name: `${spec.name}-plans`,
|
|
148
|
+
on: { pull_request: { branches: spec.branches?.length ? spec.branches : ["main"] } },
|
|
149
|
+
...(options.variables && Object.keys(options.variables).length > 0 ? { env: options.variables } : {}),
|
|
150
|
+
concurrency: { group: spec.name + "-plans-${{ github.event.number }}", "cancel-in-progress": true },
|
|
151
|
+
// contents: write pushes the record to chant/lifecycle.
|
|
152
|
+
permissions: { contents: "write" },
|
|
153
|
+
jobsDoc: { "record-plans": { "runs-on": "ubuntu-latest", container: options.image ?? OP_WAVES_IMAGE, steps } },
|
|
154
|
+
};
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
function describeJob(kind: "wave" | "decide" | "share", wave: number, name: string, share?: number): string {
|
|
@@ -110,6 +162,9 @@ function describeJob(kind: "wave" | "decide" | "share", wave: number, name: stri
|
|
|
110
162
|
|
|
111
163
|
/** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
|
|
112
164
|
export function generateGithubOpWavesPipeline(spec: OpWavesSpec, options: OpWavesPipelineOptions): OpWavesPipelineResult {
|
|
113
|
-
const { doc, jobs } = buildGithubOpWavesDoc(spec, options);
|
|
114
|
-
|
|
165
|
+
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
|
|
166
|
+
const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(doc) }];
|
|
167
|
+
if (resumeDoc) files.push({ name: `${spec.name}-resume.yml`, yaml: emitOpPipelineYAML(resumeDoc) });
|
|
168
|
+
if (plansDoc) files.push({ name: `${spec.name}-plans.yml`, yaml: emitOpPipelineYAML(plansDoc) });
|
|
169
|
+
return { files, jobs };
|
|
115
170
|
}
|