@intentius/chant-lexicon-github 0.117.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.
@@ -18,6 +18,11 @@
18
18
  * runs `chant run resume --op <name>` on that schedule: it re-runs the failed
19
19
  * jobs of a run whose waiting wave has been approved since, with the job's
20
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.
21
26
  */
22
27
  import { type OpWavesSpec } from "@intentius/chant/op/op-waves";
23
28
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
@@ -31,6 +36,7 @@ export declare function buildGithubOpWavesDoc(spec: OpWavesSpec, options: OpWave
31
36
  doc: GithubOpPipelineDoc;
32
37
  jobs: OpWavesPipelineResult["jobs"];
33
38
  resumeDoc?: GithubOpPipelineDoc;
39
+ plansDoc?: GithubOpPipelineDoc;
34
40
  };
35
41
  /** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
36
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;;;;;;;;;;;;;;;;;;;;GAoBG;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,CAAA;CAAE,CA0DpG;AA2BD,4EAA4E;AAC5E,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAKvH"}
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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "80cd8fc60162a6b30fc79202a8992ab61d34b4d245c93553194e489340c32abd",
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": "56d98c73201dd7cf4c5a2ca9153dfd1324ae6cc21adf35f9a956e389f98f51b8"
81
+ "composite": "db3b08fa2fb816149260d6ea9f821c842bbd6317bcb181114380fbb14fd7a734"
82
82
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.117.0",
3
+ "version": "0.118.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.117.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.117.0",
64
+ "@intentius/chant": "^0.118.0",
65
65
  "typescript": "^5.9.3"
66
66
  }
67
67
  }
@@ -41,4 +41,12 @@ describe("generateGithubOpWavesPipeline", () => {
41
41
  expect(yaml).toContain("GITHUB_TOKEN: '${{ github.token }}'");
42
42
  expect(generateGithubOpWavesPipeline(spec, { specFile: "waves.json" }).files).toHaveLength(1);
43
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
+ });
44
52
  });
@@ -18,6 +18,11 @@
18
18
  * runs `chant run resume --op <name>` on that schedule: it re-runs the failed
19
19
  * jobs of a run whose waiting wave has been approved since, with the job's
20
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.
21
26
  */
22
27
 
23
28
  import { opWaveJobs, opWavesResumeCommand, type OpWavesSpec } from "@intentius/chant/op/op-waves";
@@ -47,9 +52,10 @@ function dirOf(path: string): string {
47
52
  export function buildGithubOpWavesDoc(
48
53
  spec: OpWavesSpec,
49
54
  options: OpWavesPipelineOptions,
50
- ): { doc: GithubOpPipelineDoc; jobs: OpWavesPipelineResult["jobs"]; resumeDoc?: GithubOpPipelineDoc } {
55
+ ): { doc: GithubOpPipelineDoc; jobs: OpWavesPipelineResult["jobs"]; resumeDoc?: GithubOpPipelineDoc; plansDoc?: GithubOpPipelineDoc } {
51
56
  const jobs = opWaveJobs(spec, options.specFile);
52
57
  const image = options.image ?? OP_WAVES_IMAGE;
58
+ const byReview = spec.waves.some((w) => w.approval === "pr-review");
53
59
  for (const wave of spec.waves) {
54
60
  assertSetupSteps(`${spec.name}/${wave.name}`, wave.setup ?? []);
55
61
  if (wave.environment) assertEnvironment(`${spec.name}/${wave.name}`, wave.environment);
@@ -101,10 +107,15 @@ export function buildGithubOpWavesDoc(
101
107
  on: { push: { branches: spec.branches?.length ? spec.branches : ["main"] }, workflow_dispatch: {} },
102
108
  ...(options.variables && Object.keys(options.variables).length > 0 ? { env: options.variables } : {}),
103
109
  concurrency: { group: spec.name, "cancel-in-progress": false },
104
- permissions: { contents: "write" },
110
+ permissions: { contents: "write", ...(byReview ? { "pull-requests": "read" } : {}) },
105
111
  jobsDoc,
106
112
  };
107
- return { doc, jobs, ...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}) };
113
+ return {
114
+ doc,
115
+ jobs,
116
+ ...(spec.resume ? { resumeDoc: buildResumeDoc(spec, options) } : {}),
117
+ ...(byReview ? { plansDoc: buildPlansDoc(spec, options) } : {}),
118
+ };
108
119
  }
109
120
 
110
121
  /** The scheduled resume workflow (#3683). */
@@ -126,6 +137,23 @@ function buildResumeDoc(spec: OpWavesSpec, options: OpWavesPipelineOptions): Git
126
137
  };
127
138
  }
128
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
+ };
155
+ }
156
+
129
157
  function describeJob(kind: "wave" | "decide" | "share", wave: number, name: string, share?: number): string {
130
158
  if (kind === "decide") return `Plan wave ${wave} (${name}) and decide its gate`;
131
159
  if (kind === "share") return `Apply share ${share} of wave ${wave} (${name})`;
@@ -134,8 +162,9 @@ function describeJob(kind: "wave" | "decide" | "share", wave: number, name: stri
134
162
 
135
163
  /** Render an Op waves spec as one GitHub Actions workflow, `<name>.yml`. */
136
164
  export function generateGithubOpWavesPipeline(spec: OpWavesSpec, options: OpWavesPipelineOptions): OpWavesPipelineResult {
137
- const { doc, jobs, resumeDoc } = buildGithubOpWavesDoc(spec, options);
165
+ const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
138
166
  const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(doc) }];
139
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) });
140
169
  return { files, jobs };
141
170
  }