@intentius/chant-lexicon-github 0.57.0 → 0.58.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.
@@ -4,21 +4,27 @@
4
4
  * The Op counterpart to `./generate-pipeline.ts` (#891): that module
5
5
  * synthesizes a `workflow_dispatch`-triggered pipeline from a deploy-time
6
6
  * component graph, this one synthesizes a cron-triggered workflow per
7
- * stateless Op the CI-native alternative to a Temporal `TemporalSchedule`
8
- * for downstream projects that don't run Temporal (`WorkflowAuditOp`,
9
- * `PipelineAuditOp`, `ReconcileOp`, all accept an optional `schedule`
10
- * precisely for this).
7
+ * stateless Op. An Op's cadence is an `OpSchedule` on the Op itself
8
+ * (`packages/core/src/op/types.ts`), runtime-neutral data each reader
9
+ * interprets; this module is the reader that turns it into a cron a GitHub
10
+ * runner fires (`WorkflowAuditOp`, `PipelineAuditOp`, `ReconcileOp`, … all
11
+ * accept an optional `schedule` precisely for this).
11
12
  *
12
13
  * GitHub Actions' `on.schedule` is workflow-scoped, not job-scoped, so unlike
13
14
  * the component generator (one combined pipeline for the whole graph) this
14
15
  * emits one workflow file per `ScheduledOpSpec`. Each workflow:
15
- * - triggers on `schedule` (the Op's cron) and `workflow_dispatch` (manual
16
- * runs stay available for testing/dry-runs);
17
- * - declares only the `permissions:` its `findingMode` needs `report`
18
- * stays read-only, `issue`/`pull-request` add the write scope the Op's own
19
- * activity uses (`gh issue create` / `gh pr create`, see
20
- * `@intentius/chant-lexicon-temporal`'s `reconcilePr` activity) — never a
21
- * blanket `write-all`;
16
+ * - triggers on its `ScheduledOpSpec`'s trigger (#2084): `cron` (the Op's
17
+ * schedule, plus `workflow_dispatch` so manual runs stay available for
18
+ * testing/dry-runs), `pull_request` (optionally filtered to `branches`,
19
+ * no `workflow_dispatch` a PR event needs no manual escape hatch), or
20
+ * `push` (filtered to `branches`, defaulting to the repository's default
21
+ * branch);
22
+ * - declares only the `permissions:` its `findingMode` (and, for a
23
+ * `pull_request` trigger, whether that mode posts a comment) needs —
24
+ * `report` stays read-only, `issue`/`pull-request` add the write scope
25
+ * the Op's own activity uses (`gh issue create` / `gh pr create`, see
26
+ * `@intentius/chant/op`'s `reconcilePr` activity) — never a blanket
27
+ * `write-all`;
22
28
  * - runs exactly one invocation, `chant run <name>` by default — never
23
29
  * inlined audit/reconcile logic. The finding-mode itself is already baked
24
30
  * into the Op's own activity args at build time by the composite that
@@ -34,7 +40,11 @@ export type { GenerateGithubOpOptions, GenerateGithubOpResult };
34
40
  * `./generate-pipeline.ts`'s `GithubPipelineDoc` split.
35
41
  */
36
42
  export interface GithubOpPipelineDoc {
37
- /** The `on:` trigger mapping (`schedule` + `workflow_dispatch`). */
43
+ /**
44
+ * The `on:` trigger mapping, per {@link ScheduledOpSpec}'s trigger kind
45
+ * (#2084): `{ schedule, workflow_dispatch }` for cron, `{ pull_request }`
46
+ * for `pull_request`, `{ push }` for `push`.
47
+ */
38
48
  on: Record<string, unknown>;
39
49
  /** The `env:` mapping, when `options.variables` is set. */
40
50
  env?: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAC1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AA4BD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CA8C1D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAQnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
1
+ {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EACV,wBAAwB,IAAI,uBAAuB,EAEnD,aAAa,EACb,gBAAgB,IAAI,sBAAsB,EAE1C,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,mBAAmB,CAAC;CAC1B;AA+DD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC;IAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAA;CAAE,CA+C1D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAQnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAMxB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "6ed02140e5bbb599e6a2c400adf6b320e3ea45a57969e4d9d13969b372eb02b9",
4
+ "manifest.json": "dd75f9469a0b8058a7620ebef4b937fd9a6a60e0912e3afd1a0e2a3631685773",
5
5
  "meta.json": "dc3977afc2b6ddc4904de906d1b1e448b786b2aced9d82f8eade2ff915203344",
6
6
  "types/index.d.ts": "f207946d7ab52f712d0c09995366440a6bba8b39358057b0da38ff9c0acca429",
7
7
  "rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
@@ -77,5 +77,5 @@
77
77
  "skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
78
78
  "skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
79
79
  },
80
- "composite": "2eb54e808eb76cc1fc7a94ee7fd6ead00c1d25bb0f776b8a360261224b8ff8e2"
80
+ "composite": "f6bd3cec37abf68646e70479861823f42af9002b807edda16bf4c873c0e3273a"
81
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAA4H,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAEzL,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA6DvD,CAAC"}
1
+ {"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAA4H,KAAK,QAAQ,EAAgB,MAAM,gCAAgC,CAAC;AAGvM,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA6DvD,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Prior art for these audit rules: the open-source tools whose checks cover the
3
+ * same condition, credited per rule. See packages/core/src/audit/prior-art.ts for
4
+ * the registry, the relation vocabulary, and why this is credit rather than
5
+ * authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
6
+ * when a credited tool's index no longer lists a rule cited here.
7
+ */
8
+ import type { Lineage } from "@intentius/chant/audit/catalog";
9
+ export declare const githubAuditLineage: Record<string, Lineage[]>;
10
+ //# sourceMappingURL=audit-lineage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-lineage.d.ts","sourceRoot":"","sources":["../../src/lint/audit-lineage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAuKxD,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.57.0",
3
+ "version": "0.58.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.57.0",
3
+ "version": "0.58.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.57.0",
64
+ "@intentius/chant": "^0.58.0",
65
65
  "typescript": "^5.9.3"
66
66
  }
67
67
  }
@@ -108,6 +108,84 @@ describe("generateGithubOpPipeline: least-privilege permissions per finding-mode
108
108
  });
109
109
  });
110
110
 
111
+ describe("generateGithubOpPipeline: trigger kinds (#2084)", () => {
112
+ test("a legacy `{ schedule }` spec with no `trigger` is unchanged", () => {
113
+ const specs: ScheduledOpSpec[] = [{ name: "actions-audit", schedule: "0 6 * * *", findingMode: "issue" }];
114
+ const result = generateGithubOpPipeline(specs);
115
+ const doc = parseFile(result.files[0].yaml);
116
+
117
+ expect(doc.on).toEqual({ schedule: [{ cron: "0 6 * * *" }], workflow_dispatch: {} });
118
+ expect(doc.permissions).toEqual({ contents: "read", issues: "write" });
119
+ expect(result.jobs[0].trigger).toEqual({ kind: "cron", schedule: "0 6 * * *" });
120
+ });
121
+
122
+ test("pull_request trigger: filters to branches, has no workflow_dispatch", () => {
123
+ const specs: ScheduledOpSpec[] = [
124
+ { name: "tf-plan", trigger: { kind: "pull_request", branches: ["main"] } },
125
+ ];
126
+ const result = generateGithubOpPipeline(specs);
127
+ const doc = parseFile(result.files[0].yaml);
128
+
129
+ expect(doc.on).toEqual({ pull_request: { branches: ["main"] } });
130
+ expect(doc.on).not.toHaveProperty("workflow_dispatch");
131
+ expect(result.jobs[0].trigger).toEqual({ kind: "pull_request", branches: ["main"] });
132
+ });
133
+
134
+ test("pull_request trigger with no branches filter triggers on every PR", () => {
135
+ const result = generateGithubOpPipeline([{ name: "tf-plan", trigger: { kind: "pull_request" } }]);
136
+ const doc = parseFile(result.files[0].yaml);
137
+ expect(doc.on).toEqual({ pull_request: {} });
138
+ });
139
+
140
+ test("pull_request trigger with a comment-posting finding mode gets pull-requests: write", () => {
141
+ const specs: ScheduledOpSpec[] = [
142
+ { name: "tf-plan", trigger: { kind: "pull_request" }, findingMode: "issue" },
143
+ ];
144
+ const result = generateGithubOpPipeline(specs);
145
+ const doc = parseFile(result.files[0].yaml);
146
+ expect(doc.permissions).toEqual({ contents: "read", issues: "write", "pull-requests": "write" });
147
+ });
148
+
149
+ test("pull_request trigger stays read-only when findingMode is report", () => {
150
+ const result = generateGithubOpPipeline([{ name: "tf-plan", trigger: { kind: "pull_request" } }]);
151
+ const doc = parseFile(result.files[0].yaml);
152
+ expect(doc.permissions).toEqual({ contents: "read" });
153
+ });
154
+
155
+ test("push trigger: filters to branches", () => {
156
+ const specs: ScheduledOpSpec[] = [
157
+ { name: "tf-apply", trigger: { kind: "push", branches: ["release"] } },
158
+ ];
159
+ const result = generateGithubOpPipeline(specs);
160
+ const doc = parseFile(result.files[0].yaml);
161
+
162
+ expect(doc.on).toEqual({ push: { branches: ["release"] } });
163
+ expect(doc.on).not.toHaveProperty("workflow_dispatch");
164
+ expect(result.jobs[0].trigger).toEqual({ kind: "push", branches: ["release"] });
165
+ });
166
+
167
+ test("push trigger defaults to the repository default branch (main) when branches is omitted", () => {
168
+ const result = generateGithubOpPipeline([{ name: "tf-apply", trigger: { kind: "push" } }]);
169
+ const doc = parseFile(result.files[0].yaml);
170
+ expect(doc.on).toEqual({ push: { branches: ["main"] } });
171
+ });
172
+
173
+ test("push trigger is read-only by default", () => {
174
+ const result = generateGithubOpPipeline([{ name: "tf-apply", trigger: { kind: "push" } }]);
175
+ const doc = parseFile(result.files[0].yaml);
176
+ expect(doc.permissions).toEqual({ contents: "read" });
177
+ });
178
+
179
+ test("push trigger honors an elevated findingMode, same as cron", () => {
180
+ const specs: ScheduledOpSpec[] = [
181
+ { name: "tf-apply", trigger: { kind: "push" }, findingMode: "pull-request" },
182
+ ];
183
+ const result = generateGithubOpPipeline(specs);
184
+ const doc = parseFile(result.files[0].yaml);
185
+ expect(doc.permissions).toEqual({ contents: "write", "pull-requests": "write" });
186
+ });
187
+ });
188
+
111
189
  describe("generateGithubOpPipeline: concurrency guards against overlapping runs", () => {
112
190
  test("each file's concurrency group is scoped to its own job", () => {
113
191
  const result = generateGithubOpPipeline([{ name: "actions-audit", schedule: "0 6 * * *" }]);
@@ -123,7 +201,7 @@ describe("generateGithubOpPipeline: a cross-cutting change is one generator edit
123
201
  { name: "prod-reconcile", schedule: "0 * * * *" },
124
202
  ];
125
203
  const result = generateGithubOpPipeline(specs, {
126
- runCommand: ["chant", "run", "{name}", "--temporal"],
204
+ runCommand: ["chant", "run", "{name}", "--json"],
127
205
  beforeScript: ["npm ci"],
128
206
  extraScript: ["echo done"],
129
207
  });
@@ -133,7 +211,7 @@ describe("generateGithubOpPipeline: a cross-cutting change is one generator edit
133
211
  const jobName = Object.keys(doc.jobs!)[0];
134
212
  const runLines = doc.jobs![jobName].steps.filter((s) => typeof s.run === "string").map((s) => s.run as string);
135
213
  expect(runLines[0]).toBe("npm ci");
136
- expect(runLines[1]).toContain("--temporal");
214
+ expect(runLines[1]).toContain("--json");
137
215
  expect(runLines[2]).toBe("echo done");
138
216
  }
139
217
  });
@@ -144,3 +222,38 @@ describe("generateGithubOpPipeline: a cross-cutting change is one generator edit
144
222
  expect(result.jobs).toEqual([]);
145
223
  });
146
224
  });
225
+
226
+ describe("generateGithubOpPipeline: the Op's own schedule (#2120)", () => {
227
+ test("an Op that declares its cadence needs no cron on the spec — opSchedule supplies it", () => {
228
+ const specs: ScheduledOpSpec[] = [{ name: "prod-watch", opSchedule: { cron: "*/10 * * * *", overlap: "skip" } }];
229
+ const result = generateGithubOpPipeline(specs);
230
+ const doc = parseFile(result.files[0].yaml);
231
+
232
+ expect(doc.on).toEqual({ schedule: [{ cron: "*/10 * * * *" }], workflow_dispatch: {} });
233
+ expect(result.jobs[0].trigger).toEqual({ kind: "cron", schedule: "*/10 * * * *" });
234
+ });
235
+
236
+ test("an explicit spec `schedule` still wins over the Op's own", () => {
237
+ const specs: ScheduledOpSpec[] = [
238
+ { name: "prod-watch", schedule: "0 6 * * *", opSchedule: { cron: "*/10 * * * *" } },
239
+ ];
240
+ const doc = parseFile(generateGithubOpPipeline(specs).files[0].yaml);
241
+ expect(doc.on).toEqual({ schedule: [{ cron: "0 6 * * *" }], workflow_dispatch: {} });
242
+ });
243
+
244
+ test("an explicit `trigger` overrides the Op's own cadence entirely", () => {
245
+ const specs: ScheduledOpSpec[] = [
246
+ { name: "tf-plan", trigger: { kind: "pull_request" }, opSchedule: { cron: "*/10 * * * *" } },
247
+ ];
248
+ const result = generateGithubOpPipeline(specs);
249
+ const doc = parseFile(result.files[0].yaml);
250
+ expect(doc.on).toEqual({ pull_request: {} });
251
+ expect(result.jobs[0].trigger).toEqual({ kind: "pull_request" });
252
+ });
253
+
254
+ test("an Op with no cadence anywhere is still an error naming the Op", () => {
255
+ expect(() => generateGithubOpPipeline([{ name: "no-cadence" }])).toThrow(
256
+ /Scheduled Op "no-cadence" has neither/,
257
+ );
258
+ });
259
+ });
@@ -4,21 +4,27 @@
4
4
  * The Op counterpart to `./generate-pipeline.ts` (#891): that module
5
5
  * synthesizes a `workflow_dispatch`-triggered pipeline from a deploy-time
6
6
  * component graph, this one synthesizes a cron-triggered workflow per
7
- * stateless Op the CI-native alternative to a Temporal `TemporalSchedule`
8
- * for downstream projects that don't run Temporal (`WorkflowAuditOp`,
9
- * `PipelineAuditOp`, `ReconcileOp`, all accept an optional `schedule`
10
- * precisely for this).
7
+ * stateless Op. An Op's cadence is an `OpSchedule` on the Op itself
8
+ * (`packages/core/src/op/types.ts`), runtime-neutral data each reader
9
+ * interprets; this module is the reader that turns it into a cron a GitHub
10
+ * runner fires (`WorkflowAuditOp`, `PipelineAuditOp`, `ReconcileOp`, … all
11
+ * accept an optional `schedule` precisely for this).
11
12
  *
12
13
  * GitHub Actions' `on.schedule` is workflow-scoped, not job-scoped, so unlike
13
14
  * the component generator (one combined pipeline for the whole graph) this
14
15
  * emits one workflow file per `ScheduledOpSpec`. Each workflow:
15
- * - triggers on `schedule` (the Op's cron) and `workflow_dispatch` (manual
16
- * runs stay available for testing/dry-runs);
17
- * - declares only the `permissions:` its `findingMode` needs `report`
18
- * stays read-only, `issue`/`pull-request` add the write scope the Op's own
19
- * activity uses (`gh issue create` / `gh pr create`, see
20
- * `@intentius/chant-lexicon-temporal`'s `reconcilePr` activity) — never a
21
- * blanket `write-all`;
16
+ * - triggers on its `ScheduledOpSpec`'s trigger (#2084): `cron` (the Op's
17
+ * schedule, plus `workflow_dispatch` so manual runs stay available for
18
+ * testing/dry-runs), `pull_request` (optionally filtered to `branches`,
19
+ * no `workflow_dispatch` a PR event needs no manual escape hatch), or
20
+ * `push` (filtered to `branches`, defaulting to the repository's default
21
+ * branch);
22
+ * - declares only the `permissions:` its `findingMode` (and, for a
23
+ * `pull_request` trigger, whether that mode posts a comment) needs —
24
+ * `report` stays read-only, `issue`/`pull-request` add the write scope
25
+ * the Op's own activity uses (`gh issue create` / `gh pr create`, see
26
+ * `@intentius/chant/op`'s `reconcilePr` activity) — never a blanket
27
+ * `write-all`;
22
28
  * - runs exactly one invocation, `chant run <name>` by default — never
23
29
  * inlined audit/reconcile logic. The finding-mode itself is already baked
24
30
  * into the Op's own activity args at build time by the composite that
@@ -26,11 +32,13 @@
26
32
  */
27
33
 
28
34
  import { emitYAML } from "@intentius/chant/yaml";
35
+ import { resolveOpTrigger } from "@intentius/chant/lexicon";
29
36
  import type {
30
37
  ComponentPipelineOptions as GenerateGithubOpOptions,
31
38
  OpFindingMode,
32
39
  OpPipelineJob,
33
40
  OpPipelineResult as GenerateGithubOpResult,
41
+ OpTrigger,
34
42
  ScheduledOpSpec,
35
43
  } from "@intentius/chant/lexicon";
36
44
 
@@ -44,7 +52,11 @@ export type { GenerateGithubOpOptions, GenerateGithubOpResult };
44
52
  * `./generate-pipeline.ts`'s `GithubPipelineDoc` split.
45
53
  */
46
54
  export interface GithubOpPipelineDoc {
47
- /** The `on:` trigger mapping (`schedule` + `workflow_dispatch`). */
55
+ /**
56
+ * The `on:` trigger mapping, per {@link ScheduledOpSpec}'s trigger kind
57
+ * (#2084): `{ schedule, workflow_dispatch }` for cron, `{ pull_request }`
58
+ * for `pull_request`, `{ push }` for `push`.
59
+ */
48
60
  on: Record<string, unknown>;
49
61
  /** The `env:` mapping, when `options.variables` is set. */
50
62
  env?: Record<string, unknown>;
@@ -75,13 +87,40 @@ function toJobName(opName: string): string {
75
87
  const DEFAULT_IMAGE = "node:22-slim";
76
88
 
77
89
  /**
78
- * Least-privilege `permissions:` for a scheduled Op's finding-mode. `report`
79
- * needs no write access; `issue` needs only `issues: write`; `pull-request`
80
- * (and `merge-request`, generated the same way when a GitLab-authored spec is
81
- * targeted at github) needs `contents: write` to push the reconcile branch
82
- * plus `pull-requests: write` to open the PR.
90
+ * Default branch assumed for a `push` trigger with no `branches` override
91
+ * (#2084). There is no generator constant for a downstream project's actual
92
+ * default branch this operates on a `ScheduledOpSpec`, not a git checkout
93
+ * so "main" is the documented default; set `trigger.branches` explicitly
94
+ * for a project whose default branch is something else (e.g. "master").
95
+ */
96
+ const DEFAULT_PUSH_BRANCH = "main";
97
+
98
+ /** This trigger's `on:` mapping (#2084): cron unchanged, `pull_request`/`push` new. */
99
+ function onFor(trigger: OpTrigger): Record<string, unknown> {
100
+ switch (trigger.kind) {
101
+ case "cron":
102
+ return { schedule: [{ cron: trigger.schedule }], workflow_dispatch: {} };
103
+ case "pull_request":
104
+ // No `workflow_dispatch`: a PR trigger needs no manual-dispatch escape
105
+ // hatch (revisit in review if that's wrong).
106
+ return { pull_request: trigger.branches ? { branches: trigger.branches } : {} };
107
+ case "push":
108
+ return { push: { branches: trigger.branches ?? [DEFAULT_PUSH_BRANCH] } };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Least-privilege `permissions:` for a scheduled Op's finding-mode and
114
+ * trigger. `report` needs no write access; `issue` needs only `issues:
115
+ * write`; `pull-request` (and `merge-request`, generated the same way when a
116
+ * GitLab-authored spec is targeted at github) needs `contents: write` to
117
+ * push the reconcile branch plus `pull-requests: write` to open the PR. A
118
+ * `pull_request` trigger reports its finding as a comment on the triggering
119
+ * PR itself (#2084): any mode but `report` posts something to act on a
120
+ * finding, so on that trigger every such mode also gets `pull-requests:
121
+ * write` for the comment, whether or not its own scope already included it.
83
122
  */
84
- function permissionsFor(mode: OpFindingMode): Record<string, "read" | "write"> {
123
+ function permissionsForMode(mode: OpFindingMode): Record<string, "read" | "write"> {
85
124
  switch (mode) {
86
125
  case "issue":
87
126
  return { contents: "read", issues: "write" };
@@ -93,6 +132,14 @@ function permissionsFor(mode: OpFindingMode): Record<string, "read" | "write"> {
93
132
  }
94
133
  }
95
134
 
135
+ function permissionsFor(mode: OpFindingMode, trigger: OpTrigger): Record<string, "read" | "write"> {
136
+ const base = permissionsForMode(mode);
137
+ if (trigger.kind === "pull_request" && mode !== "report") {
138
+ return { ...base, "pull-requests": "write" };
139
+ }
140
+ return base;
141
+ }
142
+
96
143
  /**
97
144
  * Build one `GithubOpPipelineDoc` per scheduled Op: cron trigger,
98
145
  * least-privilege `permissions:` for its finding-mode, one job that runs
@@ -113,8 +160,9 @@ export function buildGithubOpPipelineDocs(
113
160
 
114
161
  for (const spec of ops) {
115
162
  const findingMode = spec.findingMode ?? "report";
163
+ const trigger = resolveOpTrigger(spec);
116
164
  const jobName = toJobName(spec.name);
117
- jobs.push({ jobName, op: spec.name, schedule: spec.schedule, findingMode });
165
+ jobs.push({ jobName, op: spec.name, trigger, findingMode });
118
166
 
119
167
  const runParts = runCommand.map((part) => part.replace("{name}", spec.name));
120
168
 
@@ -129,12 +177,12 @@ export function buildGithubOpPipelineDocs(
129
177
  for (const line of extraScript) steps.push({ run: line });
130
178
 
131
179
  const doc: GithubOpPipelineDoc = {
132
- on: { schedule: [{ cron: spec.schedule }], workflow_dispatch: {} },
180
+ on: onFor(trigger),
133
181
  ...(options.variables && Object.keys(options.variables).length > 0 ? { env: options.variables } : {}),
134
182
  // One run at a time per Op — a slow audit must not overlap its own next
135
183
  // scheduled trigger.
136
184
  concurrency: { group: jobName, "cancel-in-progress": false },
137
- permissions: permissionsFor(findingMode),
185
+ permissions: permissionsFor(findingMode, trigger),
138
186
  jobsDoc: {
139
187
  [jobName]: {
140
188
  "runs-on": "ubuntu-latest",
@@ -222,7 +222,7 @@ describe("generateGithubPipeline: a cross-cutting change is one generator edit,
222
222
  test("changing the trigger command (runCommand) updates every job's script uniformly", () => {
223
223
  const components = pilotComponents();
224
224
  const result = generateGithubPipeline(components, {
225
- runCommand: ["chant", "run", "--components", "{name}", "--env", "staging", "--temporal"],
225
+ runCommand: ["chant", "run", "--components", "{name}", "--env", "staging", "--verbose"],
226
226
  });
227
227
  const jobs = parsedJobs(result.yaml);
228
228
 
@@ -230,7 +230,7 @@ describe("generateGithubPipeline: a cross-cutting change is one generator edit,
230
230
  const lines = runLines(jobs[job.jobName]);
231
231
  // The runCommand prefix reflects in every job; output-threading flags
232
232
  // (--seed-outputs/--dump-outputs) may be appended per the dependency graph.
233
- expect(lines[0].startsWith(`chant run --components ${job.component} --env staging --temporal`)).toBe(true);
233
+ expect(lines[0].startsWith(`chant run --components ${job.component} --env staging --verbose`)).toBe(true);
234
234
  }
235
235
  });
236
236
 
@@ -2,7 +2,8 @@
2
2
  * The github lexicon's chant audit catalog — metadata for its post-synth rules
3
3
  * (GHA GitHub Actions rules). Contributed via githubPlugin.auditCatalog() (#687).
4
4
  */
5
- import { auditRule, GH_INJECTION, GH_PWN, GH_SECRETS, GH_THIRD_PARTY, GH_TOKEN, SCORECARD_PINNED, SCORECARD_TOKEN, SCORECARD_VULN, type RuleMeta } from "@intentius/chant/audit/catalog";
5
+ import { auditRule, GH_INJECTION, GH_PWN, GH_SECRETS, GH_THIRD_PARTY, GH_TOKEN, SCORECARD_PINNED, SCORECARD_TOKEN, SCORECARD_VULN, type RuleMeta, applyLineage } from "@intentius/chant/audit/catalog";
6
+ import { githubAuditLineage } from "./audit-lineage";
6
7
 
7
8
  export const githubAuditCatalog: Record<string, RuleMeta> = {
8
9
  GHA006: auditRule("GHA006", "report-only", "guidance", "Duplicate workflow name", "Give each workflow a unique `name:`.", { category: "correctness" }),
@@ -66,3 +67,6 @@ export const githubAuditCatalog: Record<string, RuleMeta> = {
66
67
  GHA067: auditRule("GHA067", "report-only", "guidance", "Unconditional heavy step with no path filter", "Scope the trigger's paths, or add an if: guard to the step.", { category: "efficiency" }),
67
68
  GHA068: auditRule("GHA068", "report-only", "guidance", "Pull-request workflow missing a concurrency group", "Add a concurrency group with cancel-in-progress: true.", { category: "efficiency" }),
68
69
  };
70
+
71
+ // Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
72
+ applyLineage(githubAuditCatalog, githubAuditLineage);
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Prior art for these audit rules: the open-source tools whose checks cover the
3
+ * same condition, credited per rule. See packages/core/src/audit/prior-art.ts for
4
+ * the registry, the relation vocabulary, and why this is credit rather than
5
+ * authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
6
+ * when a credited tool's index no longer lists a rule cited here.
7
+ */
8
+ import type { Lineage } from "@intentius/chant/audit/catalog";
9
+
10
+ export const githubAuditLineage: Record<string, Lineage[]> = {
11
+ GHA009: [
12
+ { tool: "actionlint", rule: "Unexpected empty mappings", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#unexpected-empty-mappings", relation: "overlaps" },
13
+ ],
14
+ GHA011: [
15
+ { tool: "actionlint", rule: "Job dependencies validation", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#job-dependencies-validation", relation: "equivalent" },
16
+ ],
17
+ GHA013: [
18
+ { tool: "poutine", rule: "default_permissions_on_risky_events", url: "https://boostsecurityio.github.io/poutine/rules/default_permissions_on_risky_events/", relation: "overlaps" },
19
+ { tool: "zizmor", rule: "excessive-permissions", url: "https://docs.zizmor.sh/audits/#excessive-permissions", relation: "overlaps" },
20
+ { tool: "scorecard", rule: "Token-Permissions", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions", relation: "overlaps" },
21
+ ],
22
+ GHA017: [
23
+ { tool: "zizmor", rule: "excessive-permissions", url: "https://docs.zizmor.sh/audits/#excessive-permissions", relation: "overlaps" },
24
+ { tool: "scorecard", rule: "Token-Permissions", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions", relation: "overlaps" },
25
+ ],
26
+ GHA018: [
27
+ { tool: "scorecard", rule: "Dangerous-Workflow", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#dangerous-workflow", relation: "overlaps" },
28
+ { tool: "octoscan", rule: "dangerous-checkout", url: "https://github.com/synacktiv/octoscan#dangerous-checkout", relation: "overlaps" },
29
+ { tool: "poutine", rule: "untrusted_checkout_exec", url: "https://boostsecurityio.github.io/poutine/rules/untrusted_checkout_exec/", relation: "overlaps" },
30
+ ],
31
+ GHA019: [
32
+ { tool: "actionlint", rule: "Job dependencies validation", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#job-dependencies-validation", relation: "equivalent" },
33
+ ],
34
+ GHA021: [
35
+ { tool: "zizmor", rule: "unpinned-uses", url: "https://docs.zizmor.sh/audits/#unpinned-uses", relation: "overlaps" },
36
+ { tool: "scorecard", rule: "Pinned-Dependencies", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies", relation: "overlaps" },
37
+ ],
38
+ GHA023: [
39
+ { tool: "actionlint", rule: "Check deprecated workflow commands", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#check-deprecated-workflow-commands", relation: "overlaps" },
40
+ ],
41
+ GHA024: [
42
+ { tool: "zizmor", rule: "concurrency-limits", url: "https://docs.zizmor.sh/audits/#concurrency-limits", relation: "overlaps" },
43
+ ],
44
+ GHA025: [
45
+ { tool: "zizmor", rule: "dangerous-triggers", url: "https://docs.zizmor.sh/audits/#dangerous-triggers", relation: "overlaps" },
46
+ ],
47
+ GHA026: [
48
+ { tool: "zizmor", rule: "secrets-outside-env", url: "https://docs.zizmor.sh/audits/#secrets-outside-env", relation: "equivalent" },
49
+ ],
50
+ GHA028: [
51
+ { tool: "actionlint", rule: "Missing required keys and key duplicates", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#missing-required-keys-and-key-duplicates", relation: "overlaps" },
52
+ ],
53
+ GHA029: [
54
+ { tool: "zizmor", rule: "unpinned-uses", url: "https://docs.zizmor.sh/audits/#unpinned-uses", relation: "overlaps" },
55
+ { tool: "scorecard", rule: "Pinned-Dependencies", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies", relation: "overlaps" },
56
+ ],
57
+ GHA030: [
58
+ { tool: "zizmor", rule: "unpinned-images", url: "https://docs.zizmor.sh/audits/#unpinned-images", relation: "overlaps" },
59
+ ],
60
+ GHA031: [
61
+ { tool: "zizmor", rule: "typosquat-uses", url: "https://docs.zizmor.sh/audits/#typosquat-uses", relation: "equivalent" },
62
+ ],
63
+ GHA032: [
64
+ { tool: "zizmor", rule: "archived-uses", url: "https://docs.zizmor.sh/audits/#archived-uses", relation: "overlaps" },
65
+ { tool: "zizmor", rule: "known-vulnerable-actions", url: "https://docs.zizmor.sh/audits/#known-vulnerable-actions", relation: "overlaps" },
66
+ ],
67
+ GHA033: [
68
+ { tool: "zizmor", rule: "excessive-permissions", url: "https://docs.zizmor.sh/audits/#excessive-permissions", relation: "overlaps" },
69
+ { tool: "scorecard", rule: "Token-Permissions", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions", relation: "overlaps" },
70
+ ],
71
+ GHA034: [
72
+ { tool: "zizmor", rule: "excessive-permissions", url: "https://docs.zizmor.sh/audits/#excessive-permissions", relation: "overlaps" },
73
+ { tool: "scorecard", rule: "Token-Permissions", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions", relation: "overlaps" },
74
+ ],
75
+ GHA036: [
76
+ { tool: "zizmor", rule: "template-injection", url: "https://docs.zizmor.sh/audits/#template-injection", relation: "equivalent" },
77
+ { tool: "poutine", rule: "injection", url: "https://boostsecurityio.github.io/poutine/rules/injection/", relation: "equivalent" },
78
+ { tool: "actionlint", rule: "Script injection by potentially untrusted inputs", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#script-injection-by-potentially-untrusted-inputs", relation: "equivalent" },
79
+ { tool: "octoscan", rule: "expression-injection", url: "https://github.com/synacktiv/octoscan#expression-injection", relation: "equivalent" },
80
+ { tool: "scorecard", rule: "Dangerous-Workflow", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#dangerous-workflow", relation: "overlaps" },
81
+ ],
82
+ GHA037: [
83
+ { tool: "zizmor", rule: "github-env", url: "https://docs.zizmor.sh/audits/#github-env", relation: "equivalent" },
84
+ { tool: "octoscan", rule: "dangerous-write", url: "https://github.com/synacktiv/octoscan#dangerous-write", relation: "overlaps" },
85
+ ],
86
+ GHA038: [
87
+ { tool: "scorecard", rule: "Dangerous-Workflow", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#dangerous-workflow", relation: "overlaps" },
88
+ { tool: "octoscan", rule: "dangerous-checkout", url: "https://github.com/synacktiv/octoscan#dangerous-checkout", relation: "overlaps" },
89
+ ],
90
+ GHA039: [
91
+ { tool: "zizmor", rule: "bot-conditions", url: "https://docs.zizmor.sh/audits/#bot-conditions", relation: "overlaps" },
92
+ { tool: "octoscan", rule: "bot-check", url: "https://github.com/synacktiv/octoscan#bot-check", relation: "overlaps" },
93
+ { tool: "poutine", rule: "confused_deputy_auto_merge", url: "https://boostsecurityio.github.io/poutine/rules/confused_deputy_auto_merge/", relation: "overlaps" },
94
+ ],
95
+ GHA040: [
96
+ { tool: "poutine", rule: "pr_runs_on_self_hosted", url: "https://boostsecurityio.github.io/poutine/rules/pr_runs_on_self_hosted/", relation: "equivalent" },
97
+ { tool: "zizmor", rule: "self-hosted-runner", url: "https://docs.zizmor.sh/audits/#self-hosted-runner", relation: "overlaps" },
98
+ { tool: "octoscan", rule: "runner-label", url: "https://github.com/synacktiv/octoscan#runner-label", relation: "overlaps" },
99
+ ],
100
+ GHA041: [
101
+ { tool: "zizmor", rule: "secrets-inherit", url: "https://docs.zizmor.sh/audits/#secrets-inherit", relation: "equivalent" },
102
+ ],
103
+ GHA042: [
104
+ { tool: "zizmor", rule: "overprovisioned-secrets", url: "https://docs.zizmor.sh/audits/#overprovisioned-secrets", relation: "equivalent" },
105
+ { tool: "poutine", rule: "job_all_secrets", url: "https://boostsecurityio.github.io/poutine/rules/job_all_secrets/", relation: "equivalent" },
106
+ ],
107
+ GHA043: [
108
+ { tool: "zizmor", rule: "secrets-outside-env", url: "https://docs.zizmor.sh/audits/#secrets-outside-env", relation: "equivalent" },
109
+ ],
110
+ GHA044: [
111
+ { tool: "zizmor", rule: "hardcoded-container-credentials", url: "https://docs.zizmor.sh/audits/#hardcoded-container-credentials", relation: "equivalent" },
112
+ { tool: "actionlint", rule: "Hardcoded credentials", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#hardcoded-credentials", relation: "equivalent" },
113
+ { tool: "octoscan", rule: "credentials", url: "https://github.com/synacktiv/octoscan#credentials", relation: "equivalent" },
114
+ ],
115
+ GHA046: [
116
+ { tool: "actionlint", rule: "Constant conditions at `if:`", url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md#constant-conditions-at-if", relation: "equivalent" },
117
+ { tool: "zizmor", rule: "unsound-condition", url: "https://docs.zizmor.sh/audits/#unsound-condition", relation: "overlaps" },
118
+ { tool: "poutine", rule: "if_always_true", url: "https://boostsecurityio.github.io/poutine/rules/if_always_true/", relation: "overlaps" },
119
+ ],
120
+ GHA047: [
121
+ { tool: "zizmor", rule: "unsound-contains", url: "https://docs.zizmor.sh/audits/#unsound-contains", relation: "equivalent" },
122
+ ],
123
+ GHA048: [
124
+ { tool: "zizmor", rule: "obfuscation", url: "https://docs.zizmor.sh/audits/#obfuscation", relation: "overlaps" },
125
+ ],
126
+ GHA049: [
127
+ { tool: "zizmor", rule: "artipacked", url: "https://docs.zizmor.sh/audits/#artipacked", relation: "equivalent" },
128
+ { tool: "octoscan", rule: "dangerous-artefact", url: "https://github.com/synacktiv/octoscan#dangerous-artefact", relation: "overlaps" },
129
+ ],
130
+ GHA050: [
131
+ { tool: "zizmor", rule: "cache-poisoning", url: "https://docs.zizmor.sh/audits/#cache-poisoning", relation: "overlaps" },
132
+ ],
133
+ GHA051: [
134
+ { tool: "zizmor", rule: "use-trusted-publishing", url: "https://docs.zizmor.sh/audits/#use-trusted-publishing", relation: "overlaps" },
135
+ ],
136
+ GHA052: [
137
+ { tool: "poutine", rule: "unverified_script_exec", url: "https://boostsecurityio.github.io/poutine/rules/unverified_script_exec/", relation: "equivalent" },
138
+ { tool: "scorecard", rule: "Pinned-Dependencies", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies", relation: "overlaps" },
139
+ ],
140
+ GHA053: [
141
+ { tool: "zizmor", rule: "insecure-commands", url: "https://docs.zizmor.sh/audits/#insecure-commands", relation: "equivalent" },
142
+ { tool: "octoscan", rule: "unsecure-commands", url: "https://github.com/synacktiv/octoscan#unsecure-commands", relation: "equivalent" },
143
+ ],
144
+ GHA054: [
145
+ { tool: "zizmor", rule: "misfeature", url: "https://docs.zizmor.sh/audits/#misfeature", relation: "overlaps" },
146
+ ],
147
+ GHA055: [
148
+ { tool: "zizmor", rule: "superfluous-actions", url: "https://docs.zizmor.sh/audits/#superfluous-actions", relation: "overlaps" },
149
+ ],
150
+ GHA056: [
151
+ { tool: "zizmor", rule: "anonymous-definition", url: "https://docs.zizmor.sh/audits/#anonymous-definition", relation: "overlaps" },
152
+ ],
153
+ GHA057: [
154
+ { tool: "zizmor", rule: "dependabot-execution", url: "https://docs.zizmor.sh/audits/#dependabot-execution", relation: "equivalent" },
155
+ ],
156
+ GHA058: [
157
+ { tool: "zizmor", rule: "dependabot-cooldown", url: "https://docs.zizmor.sh/audits/#dependabot-cooldown", relation: "equivalent" },
158
+ ],
159
+ GHA059: [
160
+ { tool: "zizmor", rule: "ref-version-mismatch", url: "https://docs.zizmor.sh/audits/#ref-version-mismatch", relation: "equivalent" },
161
+ ],
162
+ GHA060: [
163
+ { tool: "zizmor", rule: "github-app", url: "https://docs.zizmor.sh/audits/#github-app", relation: "overlaps" },
164
+ ],
165
+ GHA061: [
166
+ { tool: "zizmor", rule: "forbidden-uses", url: "https://docs.zizmor.sh/audits/#forbidden-uses", relation: "equivalent" },
167
+ ],
168
+ GHA062: [
169
+ { tool: "zizmor", rule: "known-vulnerable-actions", url: "https://docs.zizmor.sh/audits/#known-vulnerable-actions", relation: "equivalent" },
170
+ { tool: "poutine", rule: "known_vulnerability_in_build_component", url: "https://boostsecurityio.github.io/poutine/rules/known_vulnerability_in_build_component/", relation: "equivalent" },
171
+ { tool: "octoscan", rule: "known-vulnerability", url: "https://github.com/synacktiv/octoscan#known-vulnerability", relation: "equivalent" },
172
+ { tool: "scorecard", rule: "Vulnerabilities", url: "https://github.com/ossf/scorecard/blob/main/docs/checks.md#vulnerabilities", relation: "overlaps" },
173
+ ],
174
+ GHA068: [
175
+ { tool: "zizmor", rule: "concurrency-limits", url: "https://docs.zizmor.sh/audits/#concurrency-limits", relation: "overlaps" },
176
+ ],
177
+ };