@intentius/chant-lexicon-forgejo 0.104.2 → 0.104.3

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/README.md CHANGED
@@ -31,9 +31,11 @@ export const build = new Job({
31
31
 
32
32
  On build, the Forgejo dialect:
33
33
 
34
- - **Drops keys Forgejo ignores** — `permissions` and `continue-on-error` are
35
- silently ignored by the Forgejo runner, so they are removed from the output
36
- and reported as build warnings (emitting them is misleading).
34
+ - **Drops keys Forgejo ignores** — `continue-on-error` is silently ignored by
35
+ the Forgejo runner, so it is removed from the output and reported as a build
36
+ warning (emitting it is misleading). `permissions` is kept, so a job can
37
+ declare `id-token: write` to request an OIDC token; a Forgejo version that
38
+ does not honour it ignores the key.
37
39
  - **Maps runner labels** — GitHub-hosted labels like `ubuntu-latest` have no
38
40
  fixed meaning on Forgejo. They are mapped to a default Forgejo label
39
41
  (`docker`), overridable per project. Unmapped labels pass through with a
@@ -75,8 +77,8 @@ fate across the edge:
75
77
  |---|---|
76
78
  | `translated` | carried across as-is |
77
79
  | `approximated` | carried with a close equivalent |
78
- | `needs-review` | confirm/adjust on Forgejo (unresolved `uses:`, unmapped runner label) |
79
- | `lost` | the Forgejo runner ignores it (`permissions`, `continue-on-error`) |
80
+ | `needs-review` | confirm/adjust on Forgejo (`permissions`, unresolved `uses:`, unmapped runner label) |
81
+ | `lost` | the Forgejo runner ignores it (`continue-on-error`) |
80
82
 
81
83
  The same view is available to agents as the `forgejo:compare` MCP tool, which
82
84
  takes a workflow file and returns per-property fates plus summary counts —
package/dist/dialect.d.ts CHANGED
@@ -6,8 +6,11 @@
6
6
  * the right shape. The dialect differs in three small ways, all handled here as
7
7
  * a pre-pass over the resolved entity graph before the github serializer runs:
8
8
  *
9
- * 1. `permissions` and `continue-on-error` are silently ignored by the Forgejo
10
- * runner — we drop them from the output and warn per occurrence.
9
+ * 1. `continue-on-error` is silently ignored by the Forgejo runner — we drop
10
+ * it from the output and warn per occurrence. `permissions` is kept: a
11
+ * Forgejo version that honours it (15 and later may) needs `id-token: write`
12
+ * on a job to request an OIDC token, and an older runner ignores the key.
13
+ * There is no version setting in the dialect, so the key always passes.
11
14
  * 2. GitHub-hosted runner labels (`ubuntu-latest`, …) have no fixed meaning on
12
15
  * Forgejo — we map them to a default Forgejo label, overridable per project.
13
16
  * 3. Anything we can't place (an unmapped runner label) passes through with a
@@ -1 +1 @@
1
- {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAgBlG;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKxD,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmFD,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,qBAA0B,GAClC,qBAAqB,CAKvB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAWtB"}
1
+ {"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAgBlG;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKxD,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmFD,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,qBAA0B,GAClC,qBAAqB,CAKvB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAWtB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4923e81f247efa7bdf9fe02229e3a4277493a31bb83cea66f481f626e5a180b9",
4
+ "manifest.json": "3efa9f634994c7652da36dee4a453109250ff4fd8aaa04c6c692e3d21b820105",
5
5
  "meta.json": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
6
6
  "types/index.d.ts": "66204549b2a864ab5489e3f02c1878e73ab4b3c9d938cfb480b66f71543a43de",
7
7
  "rules/delegate-to-github.ts": "1060cda40f4b73d6cca3ba3fa13b80b4886147d4034c88584c2d80c5510dbe5e",
@@ -9,5 +9,5 @@
9
9
  "rules/wfj011.ts": "fcb8bf6685d744af20ad804ce929488257995c74b9a3353b2eacae3768d1c83e",
10
10
  "skills/chant-forgejo.md": "a1a560429db736c187e0b34cf8dd9efc6ad771afefcbf6f802d160d3d5274257"
11
11
  },
12
- "composite": "5f5b1891acd42d2f02ce67a15bf829e6a575cc7c0be6193d97b942f29f71655d"
12
+ "composite": "ad0c26a7ad3b5e2b71d9534eaf271c064124ce9ec5a6da3309a6672d9ee0bc81"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.104.2",
3
+ "version": "0.104.3",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Forgejo",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-forgejo",
3
- "version": "0.104.2",
3
+ "version": "0.104.3",
4
4
  "description": "Forgejo / Codeberg / Gitea Actions lexicon for chant — a thin GitHub Actions dialect",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "zod": "^4.3.6",
57
- "@intentius/chant": "^0.104.2",
58
- "@intentius/chant-lexicon-github": "^0.104.2"
57
+ "@intentius/chant": "^0.104.3",
58
+ "@intentius/chant-lexicon-github": "^0.104.3"
59
59
  },
60
60
  "scripts": {
61
61
  "generate": "tsx src/codegen/generate-cli.ts",
@@ -16,6 +16,10 @@ jobs:
16
16
  if: github.event_name == 'pull_request'
17
17
  runs-on: docker
18
18
  container: node:22-slim
19
+ permissions:
20
+ contents: read
21
+ pull-requests: write
22
+ statuses: write
19
23
  steps:
20
24
  - uses: https://code.forgejo.org/actions/checkout@v7
21
25
  with:
@@ -41,6 +45,10 @@ jobs:
41
45
  concurrency:
42
46
  group: chant-apply-prod
43
47
  cancel-in-progress: false
48
+ permissions:
49
+ contents: write
50
+ pull-requests: write
51
+ statuses: write
44
52
  steps:
45
53
  - uses: https://code.forgejo.org/actions/checkout@v7
46
54
  with:
@@ -38,11 +38,11 @@ describe("the Forgejo pull-request workflow", () => {
38
38
  expect(result.yaml).toBe(readFileSync(GOLDEN, "utf-8"));
39
39
  });
40
40
 
41
- test("its commands talk to Forgejo, on Forgejo's runner, with no permissions block", () => {
41
+ test("its commands talk to Forgejo, on Forgejo's runner, with the job permissions kept", () => {
42
42
  expect(result.yaml).toContain("--forge forgejo");
43
43
  expect(result.yaml).not.toContain("--forge github");
44
44
  expect(result.yaml).toContain("runs-on: docker");
45
- expect(result.yaml).not.toMatch(/^\s+permissions:/m);
45
+ expect(result.yaml).toMatch(/^\s+permissions:/m);
46
46
  });
47
47
 
48
48
  test.skipIf(!hasActionlint())("passes actionlint, less the runner label and actions by URL", () => {
@@ -31,13 +31,26 @@ function entityMap(...pairs: Array<[string, Declarable]>): Map<string, Declarabl
31
31
  }
32
32
 
33
33
  describe("applyForgejoDialect — dropped keys", () => {
34
- test("drops workflow-level permissions and warns", () => {
34
+ test("keeps workflow-level permissions without warning", () => {
35
35
  const wf = new MockWorkflow({ name: "CI", permissions: { contents: "read" } });
36
36
  const { entities, warnings } = applyForgejoDialect(entityMap(["workflow", wf]));
37
37
  const props = (entities.get("workflow") as MockWorkflow).props;
38
- expect(props.permissions).toBeUndefined();
38
+ expect(props.permissions).toEqual({ contents: "read" });
39
39
  expect(props.name).toBe("CI");
40
- expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(1);
40
+ expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0);
41
+ });
42
+
43
+ test("keeps job permissions, so a job can request an OIDC token", () => {
44
+ const job = new MockJob({
45
+ "runs-on": "ubuntu-latest",
46
+ permissions: { "id-token": "write", contents: "read" },
47
+ steps: [{ name: "a", run: "echo a" }],
48
+ });
49
+ const { entities, warnings } = applyForgejoDialect(entityMap(["job", job]));
50
+ const props = (entities.get("job") as MockJob).props;
51
+ expect(props.permissions).toEqual({ "id-token": "write", contents: "read" });
52
+ expect(props["runs-on"]).toBe("docker");
53
+ expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0);
41
54
  });
42
55
 
43
56
  test("drops job and step continue-on-error, one warning each", () => {
package/src/dialect.ts CHANGED
@@ -6,8 +6,11 @@
6
6
  * the right shape. The dialect differs in three small ways, all handled here as
7
7
  * a pre-pass over the resolved entity graph before the github serializer runs:
8
8
  *
9
- * 1. `permissions` and `continue-on-error` are silently ignored by the Forgejo
10
- * runner — we drop them from the output and warn per occurrence.
9
+ * 1. `continue-on-error` is silently ignored by the Forgejo runner — we drop
10
+ * it from the output and warn per occurrence. `permissions` is kept: a
11
+ * Forgejo version that honours it (15 and later may) needs `id-token: write`
12
+ * on a job to request an OIDC token, and an older runner ignores the key.
13
+ * There is no version setting in the dialect, so the key always passes.
11
14
  * 2. GitHub-hosted runner labels (`ubuntu-latest`, …) have no fixed meaning on
12
15
  * Forgejo — we map them to a default Forgejo label, overridable per project.
13
16
  * 3. Anything we can't place (an unmapped runner label) passes through with a
@@ -25,11 +28,11 @@ import { isDeclarable, isResourceDeclarable, type Declarable } from "@intentius/
25
28
  import { resolveActionRef } from "./actions";
26
29
 
27
30
  /**
28
- * Keys the Forgejo runner ignores. Emitting them is misleading (they look
31
+ * Keys the Forgejo runner ignores. `permissions` is not among them: see above. Emitting them is misleading (they look
29
32
  * enforced but aren't), so the dialect drops them. Compared in kebab-case so
30
33
  * both `continueOnError` and `continue-on-error` spellings are caught.
31
34
  */
32
- const DROPPED_KEYS = new Set(["permissions", "continue-on-error"]);
35
+ const DROPPED_KEYS = new Set(["continue-on-error"]);
33
36
 
34
37
  /** Property key whose value is a runner-label selector. */
35
38
  const RUNS_ON_KEY = "runs-on";
@@ -7,7 +7,7 @@ function analyze(yaml: string) {
7
7
  }
8
8
 
9
9
  describe("analyzeForgejoSecurity — fate classes", () => {
10
- test("workflow-level permissions is lost", () => {
10
+ test("workflow-level permissions needs review", () => {
11
11
  const records = analyze(`on: push
12
12
  permissions:
13
13
  contents: read
@@ -19,7 +19,7 @@ jobs:
19
19
  `);
20
20
  const perm = records.filter((r) => r.rule === "MIG-FJ-PERMISSIONS");
21
21
  expect(perm).toHaveLength(1);
22
- expect(perm[0].security.fate).toBe("lost");
22
+ expect(perm[0].security.fate).toBe("needs-review");
23
23
  });
24
24
 
25
25
  test("continue-on-error is lost (job and step)", () => {
@@ -78,12 +78,12 @@ export function analyzeForgejoSecurity(
78
78
  records.push({
79
79
  sourceKey: childPath,
80
80
  sourceFile: file,
81
- category: "skipped",
81
+ category: "needs-review",
82
82
  rule: "MIG-FJ-PERMISSIONS",
83
- note: `permissions: is ignored by the Forgejo runner — the least-privilege control is lost. Re-establish it through your Forgejo/runner and repository token settings.`,
84
- security: { property: "Least-privilege permissions", fate: "lost", severity: "warning", reestablish: "Forgejo runner/token settings" },
83
+ note: `permissions: is kept, but only a Forgejo version that honours it enforces it (15 and later may). An older runner ignores it, so confirm the token scope in your Forgejo/runner and repository settings.`,
84
+ security: { property: "Least-privilege permissions", fate: "needs-review", severity: "warning", reestablish: "Forgejo runner/token settings on a version that ignores permissions" },
85
85
  });
86
- continue; // don't descend into a dropped subtree
86
+ continue;
87
87
  }
88
88
 
89
89
  if (kebab === "continue-on-error") {
@@ -58,9 +58,7 @@ describe("forgejo MCP compare tool", () => {
58
58
 
59
59
  expect(result.found).toBe(true);
60
60
  const fates = result.properties.map((p) => p.fate);
61
- expect(fates).toContain("lost"); // permissions
62
- expect(fates).toContain("needs-review"); // unmapped action ref
63
- expect(result.summary.lost).toBeGreaterThanOrEqual(1);
61
+ expect(fates).toContain("needs-review"); // permissions, unmapped action ref
64
62
  expect(result.summary["needs-review"]).toBeGreaterThanOrEqual(1);
65
63
  });
66
64
 
@@ -23,7 +23,7 @@ describe("github → forgejo transform", () => {
23
23
  test("emits forgejo YAML with the dialect applied", async () => {
24
24
  const { output } = await transform(GHA_WORKFLOW, { sourceFile: "ci.yml" });
25
25
  expect(output).toContain("runs-on: docker");
26
- expect(output).not.toContain("permissions");
26
+ expect(output).toContain("permissions");
27
27
  expect(output).not.toContain("continue-on-error");
28
28
  expect(output).toContain("https://code.forgejo.org/actions/checkout@v4");
29
29
  // unmapped ref still present (passed through), surfaced in the compare
@@ -36,7 +36,7 @@ describe("github → forgejo transform", () => {
36
36
  expect(rules).toContain("MIG-FJ-PERMISSIONS");
37
37
  expect(rules).toContain("MIG-FJ-CONTINUE-ON-ERROR");
38
38
  expect(rules).toContain("MIG-FJ-ACTION-UNRESOLVED");
39
- expect(provenance.find((r) => r.rule === "MIG-FJ-PERMISSIONS")?.security.fate).toBe("lost");
39
+ expect(provenance.find((r) => r.rule === "MIG-FJ-PERMISSIONS")?.security.fate).toBe("needs-review");
40
40
  expect(securityPosture).toContain("## Security posture");
41
41
  expect(diagnostics.length).toBe(provenance.length);
42
42
  });
@@ -47,12 +47,12 @@ describe("forgejoSerializer — github-style source roundtrip", () => {
47
47
  expect(primary).toContain("npm run build");
48
48
  });
49
49
 
50
- test("drops permissions and continue-on-error, warning on each", () => {
50
+ test("keeps permissions, drops continue-on-error with a warning on each", () => {
51
51
  const result = asResult(forgejoSerializer.serialize(buildSource()));
52
- expect(result.primary).not.toContain("permissions");
52
+ expect(result.primary).toContain("permissions");
53
53
  expect(result.primary).not.toContain("continue-on-error");
54
- // workflow permissions + job continue-on-error + step continue-on-error
55
- expect((result.warnings ?? []).filter((w) => w.includes("permissions"))).toHaveLength(1);
54
+ // job continue-on-error + step continue-on-error
55
+ expect((result.warnings ?? []).filter((w) => w.includes("permissions"))).toHaveLength(0);
56
56
  expect((result.warnings ?? []).filter((w) => w.includes("continue-on-error"))).toHaveLength(2);
57
57
  });
58
58
 
@@ -162,9 +162,9 @@ describe("forgejoSerializer — inherits PrPlanReport from github (#1983)", () =
162
162
  const result = asResult(out);
163
163
  expect(result.primary).toContain("runs-on: docker");
164
164
  expect(result.primary).not.toContain("ubuntu-latest");
165
- // The job carries `permissions` — dropped by the dialect, warned once.
166
- expect(result.primary).not.toContain("permissions:");
167
- expect((result.warnings ?? []).filter((w) => w.includes("permissions"))).toHaveLength(1);
165
+ // The job carries `permissions`, which the dialect keeps.
166
+ expect(result.primary).toContain("permissions:");
167
+ expect((result.warnings ?? []).filter((w) => w.includes("permissions"))).toHaveLength(0);
168
168
  // The sticky-comment mechanism is a raw script, nothing to remap or drop.
169
169
  expect(result.primary).toContain("Post or update PR comment");
170
170
  expect(result.primary).toContain("gh api");