@intentius/chant-lexicon-github 0.45.0 → 0.49.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "e30d7f473b8a2e2f5043a3cb45b1904b7a3863a8a114d534e0fe87147352f632",
4
+ "manifest.json": "f13c6d5badc99a05feb6c2f1172e984576192151b1a62a157d770efa5b1c541c",
5
5
  "meta.json": "798c5f5f37b0174756d2451299acfdc95c014f11d06d32b9153bbdfa9f3a580c",
6
6
  "types/index.d.ts": "39c0791f0e58025bcef82e0f7a92240508f899fc2771d5dd51b2369b4d330f13",
7
7
  "rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
@@ -20,7 +20,7 @@
20
20
  "rules/gha006.ts": "7e2977d34851ce2ed9f91c4dd78ecda8d4de099ceaac1cd9ab74960a193133a3",
21
21
  "rules/gha009.ts": "0f9a261e886309679494da32b72a0d9ea20017e5da75a9d59a04846a0e4d78db",
22
22
  "rules/gha011.ts": "53b7ca79b5bf56e5793a9b7a44ae0ac766eb5acf0669005e7810cb7762bab79c",
23
- "rules/gha013.ts": "47c1b28565aced3370b85d00a03b6110a32e451661e34badd731ffbb24b45d9a",
23
+ "rules/gha013.ts": "cd9119154d7b9de94ae3f4d41ad5f09c5757e25a5effddd3d580ef5d1466a8fc",
24
24
  "rules/gha017.ts": "ec62dac7f9d69f36978cb461c4ba391a7e445d410bbfa497cddcb1b12cfd5904",
25
25
  "rules/gha018.ts": "2c71c5accb96bccfa01075e7f8f4ad66e6d9a68a81b86f89a68cabb9c144e140",
26
26
  "rules/gha019.ts": "d0b33d63812c9cba2ebf9f2d85d8cf22068ea96460cd2143bdfb4d2a4d58fe2e",
@@ -67,5 +67,5 @@
67
67
  "skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
68
68
  "skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
69
69
  },
70
- "composite": "77a265cdec5d2163e1ce49b12ef260aaa7e23b3d8989fcb6de23a90ce9612227"
70
+ "composite": "b77a589c318cd23862c320a4e9ba5459696b8192700a8f9a23ff49519149d90b"
71
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gha013.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/gha013.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAG9G,eAAO,MAAM,MAAM,EAAE,cA6BpB,CAAC"}
1
+ {"version":3,"file":"gha013.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/gha013.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAG9G,eAAO,MAAM,MAAM,EAAE,cAoCpB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.45.0",
3
+ "version": "0.49.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
- import { getPrimaryOutput, extractJobs, extractTriggers } from "./yaml-helpers";
9
+ import { getPrimaryOutput, extractJobs, extractTriggers, extractJobPermissions } from "./yaml-helpers";
10
10
 
11
11
  export const gha013: PostSynthCheck = {
12
12
  id: "GHA013",
@@ -21,9 +21,16 @@ export const gha013: PostSynthCheck = {
21
21
 
22
22
  if (!triggers["pull_request_target"] && !triggers["workflow_dispatch"]) continue;
23
23
 
24
+ // chant #1544 — `extractJobs`' `ParsedJob.permissions` field is never
25
+ // populated (its regex-based per-job scan only fills `needs`/`steps`),
26
+ // so this rule read `job.permissions` as always undefined and warned
27
+ // on every job regardless of whether it declared one. `extractJobPermissions`
28
+ // (below) is the structural, `parseYAML`-backed extractor that other
29
+ // permissions-aware checks already use — it actually sees the block.
24
30
  const jobs = extractJobs(yaml);
25
- for (const [jobName, job] of jobs) {
26
- if (!job.permissions) {
31
+ const jobPermissions = extractJobPermissions(yaml);
32
+ for (const [jobName] of jobs) {
33
+ if (!jobPermissions.has(jobName)) {
27
34
  diagnostics.push({
28
35
  checkId: "GHA013",
29
36
  severity: "warning",
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AA6MhF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAwD9B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AAuNhF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAwD9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.45.0",
3
+ "version": "0.49.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.45.0",
64
+ "@intentius/chant": "^0.49.0",
65
65
  "typescript": "^5.9.3"
66
66
  }
67
67
  }
@@ -50,6 +50,23 @@ jobs:
50
50
  expect(diags[0].message).toContain("deploy");
51
51
  });
52
52
 
53
+ test("does not flag a job that declares job-level permissions (#1544)", () => {
54
+ const yaml = `name: Deploy
55
+ on:
56
+ workflow_dispatch:
57
+ jobs:
58
+ deploy:
59
+ runs-on: ubuntu-latest
60
+ permissions:
61
+ contents: read
62
+ id-token: write
63
+ steps:
64
+ - run: echo deploy
65
+ `;
66
+ const diags = gha013.check(makeCtx(yaml));
67
+ expect(diags).toHaveLength(0);
68
+ });
69
+
53
70
  test("does not flag when trigger is not sensitive", () => {
54
71
  const yaml = `name: CI
55
72
  on:
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
- import { getPrimaryOutput, extractJobs, extractTriggers } from "./yaml-helpers";
9
+ import { getPrimaryOutput, extractJobs, extractTriggers, extractJobPermissions } from "./yaml-helpers";
10
10
 
11
11
  export const gha013: PostSynthCheck = {
12
12
  id: "GHA013",
@@ -21,9 +21,16 @@ export const gha013: PostSynthCheck = {
21
21
 
22
22
  if (!triggers["pull_request_target"] && !triggers["workflow_dispatch"]) continue;
23
23
 
24
+ // chant #1544 — `extractJobs`' `ParsedJob.permissions` field is never
25
+ // populated (its regex-based per-job scan only fills `needs`/`steps`),
26
+ // so this rule read `job.permissions` as always undefined and warned
27
+ // on every job regardless of whether it declared one. `extractJobPermissions`
28
+ // (below) is the structural, `parseYAML`-backed extractor that other
29
+ // permissions-aware checks already use — it actually sees the block.
24
30
  const jobs = extractJobs(yaml);
25
- for (const [jobName, job] of jobs) {
26
- if (!job.permissions) {
31
+ const jobPermissions = extractJobPermissions(yaml);
32
+ for (const [jobName] of jobs) {
33
+ if (!jobPermissions.has(jobName)) {
27
34
  diagnostics.push({
28
35
  checkId: "GHA013",
29
36
  severity: "warning",
@@ -180,6 +180,36 @@ describe("githubSerializer.serialize", () => {
180
180
  expect(output).toContain("continue-on-error: true");
181
181
  });
182
182
 
183
+ test("env / with map keys pass through verbatim (chant #1223)", () => {
184
+ const entities = new Map<string, Declarable>();
185
+ entities.set("workflow", new MockWorkflow({
186
+ name: "CI",
187
+ on: { push: null },
188
+ }));
189
+ entities.set("job", new MockJob({
190
+ "runs-on": "ubuntu-latest",
191
+ env: { CHANT_ENV: "pr-1", kubeConfigPath: "/tmp/kc" },
192
+ steps: [
193
+ new MockStep({
194
+ uses: "actions/setup-node@abc",
195
+ with: { "node-version": "22", checkLatest: "true" },
196
+ env: { GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
197
+ }),
198
+ ],
199
+ }));
200
+
201
+ const output = githubSerializer.serialize(entities) as string;
202
+ // Env var names are user identifiers — kebab-casing renames what the
203
+ // script reads. They must survive exactly as written.
204
+ expect(output).toContain("CHANT_ENV: pr-1");
205
+ expect(output).toContain("kubeConfigPath: /tmp/kc");
206
+ expect(output).toContain("GH_TOKEN:");
207
+ // Action inputs are the action's declared names, also not chant's to touch.
208
+ expect(output).toContain("checkLatest: 'true'");
209
+ expect(output).not.toContain("gh-token");
210
+ expect(output).not.toContain("chant-env");
211
+ });
212
+
183
213
  test("serializes trigger entities", () => {
184
214
  const entities = new Map<string, Declarable>();
185
215
  entities.set("workflow", new MockWorkflow({
package/src/serializer.ts CHANGED
@@ -183,6 +183,15 @@ function buildStandaloneJobsSection(
183
183
 
184
184
  // ── Key conversion for YAML output ────────────────────────────────
185
185
 
186
+ /**
187
+ * Map keys whose entries are user-named, not schema properties. `env` names
188
+ * environment variables (`KUBECONFIG_DATA` must stay `KUBECONFIG_DATA`, not
189
+ * become `kubeconfig_data`), `with` names an action's declared inputs,
190
+ * `outputs`/`secrets`/`variables` name user identifiers referenced elsewhere
191
+ * by exact spelling. Kebab-casing inside these silently renames them.
192
+ */
193
+ const VERBATIM_KEY_MAPS = new Set(["env", "with", "outputs", "secrets", "variables"]);
194
+
186
195
  /**
187
196
  * Convert a props object keys from camelCase to kebab-case for job/step properties.
188
197
  */
@@ -192,21 +201,22 @@ function convertKeys(obj: Record<string, unknown>): Record<string, unknown> {
192
201
  if (value === undefined || value === null) continue;
193
202
  // Zero and false are valid values, but undefined/null should be omitted
194
203
  const yamlKey = toKebabCase(key);
195
- result[yamlKey] = convertValueKeys(value);
204
+ result[yamlKey] = convertValueKeys(value, VERBATIM_KEY_MAPS.has(yamlKey));
196
205
  }
197
206
  return result;
198
207
  }
199
208
 
200
- function convertValueKeys(value: unknown): unknown {
209
+ function convertValueKeys(value: unknown, verbatim = false): unknown {
201
210
  if (value === null || value === undefined) return value;
202
211
  if (typeof value !== "object") return value;
203
- if (Array.isArray(value)) return value.map(convertValueKeys);
212
+ if (Array.isArray(value)) return value.map((v) => convertValueKeys(v, verbatim));
204
213
 
205
214
  const obj = value as Record<string, unknown>;
206
215
  const result: Record<string, unknown> = {};
207
216
  for (const [key, v] of Object.entries(obj)) {
208
217
  if (v === undefined || v === null) continue;
209
- result[toKebabCase(key)] = convertValueKeys(v);
218
+ const yamlKey = verbatim ? key : toKebabCase(key);
219
+ result[yamlKey] = convertValueKeys(v, verbatim || VERBATIM_KEY_MAPS.has(yamlKey));
210
220
  }
211
221
  return result;
212
222
  }