@intentius/chant-lexicon-forgejo 0.106.1 → 0.106.4
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 +6 -4
- package/dist/components/generate-op-pipeline.d.ts +2 -2
- package/dist/components/generate-pipeline.d.ts +2 -3
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/dialect.d.ts +7 -4
- package/dist/dialect.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/migrate/from-github/security.d.ts +2 -2
- package/dist/skills/chant-forgejo.md +4 -3
- package/package.json +3 -3
- package/src/components/__fixtures__/pr-loop.forgejo.golden.yml +0 -8
- package/src/components/generate-op-pipeline.ts +2 -2
- package/src/components/generate-pipeline.member.test.ts +1 -1
- package/src/components/generate-pipeline.ts +2 -3
- package/src/components/generate-pr-pipeline.test.ts +2 -2
- package/src/dialect.test.ts +27 -4
- package/src/dialect.ts +41 -5
- package/src/migrate/from-github/security.ts +4 -4
- package/src/migrate.test.ts +19 -1
- package/src/serializer.test.ts +5 -5
- package/src/skills/chant-forgejo.md +4 -3
package/README.md
CHANGED
|
@@ -33,9 +33,11 @@ On build, the Forgejo dialect:
|
|
|
33
33
|
|
|
34
34
|
- **Drops keys Forgejo ignores** — `continue-on-error` is silently ignored by
|
|
35
35
|
the Forgejo runner, so it is removed from the output and reported as a build
|
|
36
|
-
warning (emitting it is misleading). `permissions` is
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
warning (emitting it is misleading). `permissions` is dropped too, because
|
|
37
|
+
Forgejo reads none of its scopes and lists the key in the run page's
|
|
38
|
+
"Workflow warnings" box. The one exception is `id-token`: a job that assumes
|
|
39
|
+
a cloud role over OIDC keeps `id-token: write`, and nothing else from its
|
|
40
|
+
`permissions` block.
|
|
39
41
|
- **Maps runner labels** — GitHub-hosted labels like `ubuntu-latest` have no
|
|
40
42
|
fixed meaning on Forgejo. They are mapped to a default Forgejo label
|
|
41
43
|
(`docker`), overridable per project. Unmapped labels pass through with a
|
|
@@ -77,7 +79,7 @@ fate across the edge:
|
|
|
77
79
|
|---|---|
|
|
78
80
|
| `translated` | carried across as-is |
|
|
79
81
|
| `approximated` | carried with a close equivalent |
|
|
80
|
-
| `needs-review` | confirm/adjust on Forgejo (`permissions`, unresolved `uses:`, unmapped runner label) |
|
|
82
|
+
| `needs-review` | confirm/adjust on Forgejo (`permissions`, which the runner does not enforce, unresolved `uses:`, unmapped runner label) |
|
|
81
83
|
| `lost` | the Forgejo runner ignores it (`continue-on-error`) |
|
|
82
84
|
|
|
83
85
|
The same view is available to agents as the `forgejo:compare` MCP tool, which
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* before emitting.
|
|
11
11
|
*
|
|
12
12
|
* One difference from the component generator: Forgejo Actions ignores
|
|
13
|
-
* `permissions:`
|
|
14
|
-
* ../dialect.ts
|
|
13
|
+
* the workflow-level `permissions:` section (the dialect drops every scope
|
|
14
|
+
* but `id-token`, see ../dialect.ts), so this omits the section outright rather
|
|
15
15
|
* than emitting a control the runner can't act on. The token a finding-mode
|
|
16
16
|
* needs still rides the trigger step's `env:` (`GH_TOKEN`/`GITHUB_TOKEN`,
|
|
17
17
|
* built by github's generator); actual write access on Forgejo is a property
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
* (default `docker`, the label a fresh `act_runner` exposes);
|
|
17
17
|
* - `uses:` refs (`actions/checkout@v4`, `actions/upload-artifact@v4`, …)
|
|
18
18
|
* rewritten to a Forgejo-resolvable form;
|
|
19
|
-
* - keys the Forgejo runner ignores (`
|
|
20
|
-
* dropped
|
|
21
|
-
* the two dialects in lock-step if that changes.
|
|
19
|
+
* - keys the Forgejo runner ignores (`continue-on-error`, and `permissions` bar `id-token`)
|
|
20
|
+
* dropped (`permissions` keeps only `id-token`, which OIDC needs).
|
|
22
21
|
*
|
|
23
22
|
* The output path convention is `.forgejo/workflows/` (vs github's
|
|
24
23
|
* `.github/workflows/`), set by the caller's `-o` flag — this module emits the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAOH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAOjF;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,uBAAuB,CAsBzB"}
|
package/dist/dialect.d.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* a pre-pass over the resolved entity graph before the github serializer runs:
|
|
8
8
|
*
|
|
9
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* it from the output and warn per occurrence. `permissions` is reduced to
|
|
11
|
+
* the one scope a Forgejo job can still use: `id-token`, which a job that
|
|
12
|
+
* assumes a cloud role over OIDC needs (#3500). Every other scope
|
|
13
|
+
* (`contents`, `pull-requests`, ...) is dropped without a warning, since
|
|
14
|
+
* Forgejo reads none of them and flags the key in the run page's
|
|
15
|
+
* "Workflow warnings" box. A `permissions` block with no `id-token` goes
|
|
16
|
+
* away whole; one with it keeps only that line.
|
|
14
17
|
* 2. GitHub-hosted runner labels (`ubuntu-latest`, …) have no fixed meaning on
|
|
15
18
|
* Forgejo — we map them to a default Forgejo label, overridable per project.
|
|
16
19
|
* 3. Anything we can't place (an unmapped runner label) passes through with a
|
package/dist/dialect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AA4ClG;;;;;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;AAwFD,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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "aed6c54f28b05a2c78f4fa77f94bfa00b3712d2d8624d802f355ef0a6f8cd440",
|
|
5
5
|
"meta.json": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
|
6
6
|
"types/index.d.ts": "66204549b2a864ab5489e3f02c1878e73ab4b3c9d938cfb480b66f71543a43de",
|
|
7
7
|
"rules/delegate-to-github.ts": "1060cda40f4b73d6cca3ba3fa13b80b4886147d4034c88584c2d80c5510dbe5e",
|
|
8
8
|
"rules/wfj010.ts": "f32834f8b8d0a8dd2a8e19f456df7e44aa63084cce3022ccb13f58f4a9f4f75c",
|
|
9
9
|
"rules/wfj011.ts": "fcb8bf6685d744af20ad804ce929488257995c74b9a3353b2eacae3768d1c83e",
|
|
10
|
-
"skills/chant-forgejo.md": "
|
|
10
|
+
"skills/chant-forgejo.md": "f28d7fb69fb1cfd992496ef41fda0ea5ccd54a29b84ea1c501a44f6a3992c4d5"
|
|
11
11
|
},
|
|
12
|
-
"composite": "
|
|
12
|
+
"composite": "4052e26d9660a9906469efb30bbb101c445d6f39bcbe45fee0c5ef73a6012dae"
|
|
13
13
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* github → forgejo YAML is near-identical, so the migration itself is thin. The
|
|
5
5
|
* differentiated value is the **compare**: classifying what survives the move
|
|
6
6
|
* and what Forgejo silently drops. Most properties translate verbatim; the
|
|
7
|
-
* useful findings are the keys the Forgejo runner ignores (`permissions
|
|
8
|
-
* `continue-on-error` → `lost`) plus refs/labels that need attention
|
|
7
|
+
* useful findings are the keys the Forgejo runner ignores (`permissions` →
|
|
8
|
+
* `needs-review`, `continue-on-error` → `lost`) plus refs/labels that need attention
|
|
9
9
|
* (unresolved `uses:`, unmapped runner labels → `needs-review`).
|
|
10
10
|
*
|
|
11
11
|
* Mirrors the gitlab lexicon's fate model (translated / approximated /
|
|
@@ -38,8 +38,9 @@ chant build src -o .forgejo/workflows/ci.yml
|
|
|
38
38
|
|
|
39
39
|
## What the dialect does on build
|
|
40
40
|
|
|
41
|
-
- **Drops keys the Forgejo runner ignores** — `
|
|
42
|
-
`
|
|
41
|
+
- **Drops keys the Forgejo runner ignores** — `continue-on-error` is removed
|
|
42
|
+
with a build warning. `permissions` is removed silently, except `id-token`,
|
|
43
|
+
which stays for OIDC.
|
|
43
44
|
- **Maps runner labels** — `ubuntu-latest` → `docker` by default; override via
|
|
44
45
|
`forgejo.runnerLabels` in `chant.config.ts`. Unmapped labels warn.
|
|
45
46
|
- **Resolves `uses:` refs** — common `actions/*` rewrite under
|
|
@@ -53,7 +54,7 @@ chant migrate .github/workflows/ci.yml --to forgejo -o .forgejo/workflows/ci.yml
|
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
`--validate` prints a **Security posture** report: what survives the move and
|
|
56
|
-
what Forgejo silently drops (`
|
|
57
|
+
what Forgejo silently drops (`continue-on-error` → lost, `permissions` → needs-review,
|
|
57
58
|
unresolved `uses:` / unmapped runner labels → needs-review). The same view is
|
|
58
59
|
the `forgejo:compare` MCP tool.
|
|
59
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-forgejo",
|
|
3
|
-
"version": "0.106.
|
|
3
|
+
"version": "0.106.4",
|
|
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.106.
|
|
58
|
-
"@intentius/chant-lexicon-github": "^0.106.
|
|
57
|
+
"@intentius/chant": "^0.106.4",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.106.4"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"generate": "tsx src/codegen/generate-cli.ts",
|
|
@@ -16,10 +16,6 @@ jobs:
|
|
|
16
16
|
if: github.event_name == 'pull_request'
|
|
17
17
|
runs-on: docker
|
|
18
18
|
container: node:22
|
|
19
|
-
permissions:
|
|
20
|
-
contents: read
|
|
21
|
-
pull-requests: write
|
|
22
|
-
statuses: write
|
|
23
19
|
steps:
|
|
24
20
|
- uses: https://code.forgejo.org/actions/checkout@v7
|
|
25
21
|
with:
|
|
@@ -46,10 +42,6 @@ jobs:
|
|
|
46
42
|
concurrency:
|
|
47
43
|
group: chant-apply-prod
|
|
48
44
|
cancel-in-progress: false
|
|
49
|
-
permissions:
|
|
50
|
-
contents: write
|
|
51
|
-
pull-requests: write
|
|
52
|
-
statuses: write
|
|
53
45
|
steps:
|
|
54
46
|
- uses: https://code.forgejo.org/actions/checkout@v7
|
|
55
47
|
with:
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* before emitting.
|
|
11
11
|
*
|
|
12
12
|
* One difference from the component generator: Forgejo Actions ignores
|
|
13
|
-
* `permissions:`
|
|
14
|
-
* ../dialect.ts
|
|
13
|
+
* the workflow-level `permissions:` section (the dialect drops every scope
|
|
14
|
+
* but `id-token`, see ../dialect.ts), so this omits the section outright rather
|
|
15
15
|
* than emitting a control the runner can't act on. The token a finding-mode
|
|
16
16
|
* needs still rides the trigger step's `env:` (`GH_TOKEN`/`GITHUB_TOKEN`,
|
|
17
17
|
* built by github's generator); actual write access on Forgejo is a property
|
|
@@ -15,7 +15,7 @@ describe("generateForgejoPipeline for a workspace member", () => {
|
|
|
15
15
|
test("working directory and name survive the dialect, and the triggers stay the plain pipeline's", () => {
|
|
16
16
|
const member = { name: "api", dir: "services/api", file: ".forgejo/workflows/chant-api-staging.yml" };
|
|
17
17
|
const doc = parseYAML(generateForgejoPipeline(components, { env: "staging", member }).yaml) as Record<string, unknown>;
|
|
18
|
-
expect(doc.name).toBe("chant-components
|
|
18
|
+
expect(doc.name).toBe("chant-components.api.staging");
|
|
19
19
|
expect(Object.keys(doc.on as Record<string, unknown>)).toEqual(["workflow_dispatch"]);
|
|
20
20
|
expect(doc.defaults).toEqual({ run: { "working-directory": "services/api" } });
|
|
21
21
|
});
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
* (default `docker`, the label a fresh `act_runner` exposes);
|
|
17
17
|
* - `uses:` refs (`actions/checkout@v4`, `actions/upload-artifact@v4`, …)
|
|
18
18
|
* rewritten to a Forgejo-resolvable form;
|
|
19
|
-
* - keys the Forgejo runner ignores (`
|
|
20
|
-
* dropped
|
|
21
|
-
* the two dialects in lock-step if that changes.
|
|
19
|
+
* - keys the Forgejo runner ignores (`continue-on-error`, and `permissions` bar `id-token`)
|
|
20
|
+
* dropped (`permissions` keeps only `id-token`, which OIDC needs).
|
|
22
21
|
*
|
|
23
22
|
* The output path convention is `.forgejo/workflows/` (vs github's
|
|
24
23
|
* `.github/workflows/`), set by the caller's `-o` flag — this module emits the
|
|
@@ -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 the job permissions
|
|
41
|
+
test("its commands talk to Forgejo, on Forgejo's runner, with the job permissions dropped", () => {
|
|
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).toMatch(/^\s
|
|
45
|
+
expect(result.yaml).not.toMatch(/^\s*permissions:/m);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test("the apply keeps its resume record in the runner's cache, through the Forgejo mirror of actions/cache (#3543)", () => {
|
package/src/dialect.test.ts
CHANGED
|
@@ -31,16 +31,16 @@ function entityMap(...pairs: Array<[string, Declarable]>): Map<string, Declarabl
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
describe("applyForgejoDialect — dropped keys", () => {
|
|
34
|
-
test("
|
|
34
|
+
test("drops 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
|
|
38
|
+
expect("permissions" in props).toBe(false);
|
|
39
39
|
expect(props.name).toBe("CI");
|
|
40
40
|
expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
test("keeps job permissions, so a job can request an OIDC token", () => {
|
|
43
|
+
test("keeps only id-token from job permissions, so a job can request an OIDC token", () => {
|
|
44
44
|
const job = new MockJob({
|
|
45
45
|
"runs-on": "ubuntu-latest",
|
|
46
46
|
permissions: { "id-token": "write", contents: "read" },
|
|
@@ -48,11 +48,34 @@ describe("applyForgejoDialect — dropped keys", () => {
|
|
|
48
48
|
});
|
|
49
49
|
const { entities, warnings } = applyForgejoDialect(entityMap(["job", job]));
|
|
50
50
|
const props = (entities.get("job") as MockJob).props;
|
|
51
|
-
expect(props.permissions).toEqual({ "id-token": "write"
|
|
51
|
+
expect(props.permissions).toEqual({ "id-token": "write" });
|
|
52
52
|
expect(props["runs-on"]).toBe("docker");
|
|
53
53
|
expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("drops job permissions that carry no id-token, and the read-all shorthand", () => {
|
|
57
|
+
const job = new MockJob({
|
|
58
|
+
"runs-on": "ubuntu-latest",
|
|
59
|
+
permissions: { contents: "read", "pull-requests": "write" },
|
|
60
|
+
steps: [{ name: "a", run: "echo a" }],
|
|
61
|
+
});
|
|
62
|
+
const other = new MockJob({ "runs-on": "ubuntu-latest", permissions: "read-all" });
|
|
63
|
+
const { entities, warnings } = applyForgejoDialect(entityMap(["job", job], ["other", other]));
|
|
64
|
+
expect("permissions" in (entities.get("job") as MockJob).props).toBe(false);
|
|
65
|
+
expect("permissions" in (entities.get("other") as MockJob).props).toBe(false);
|
|
66
|
+
expect(warnings).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("leaves a step input named permissions alone", () => {
|
|
70
|
+
const job = new MockJob({
|
|
71
|
+
"runs-on": "ubuntu-latest",
|
|
72
|
+
steps: [{ uses: "some-org/custom-action@v1", with: { permissions: "everyone" } }],
|
|
73
|
+
});
|
|
74
|
+
const { entities } = applyForgejoDialect(entityMap(["job", job]));
|
|
75
|
+
const steps = (entities.get("job") as MockJob).props.steps as Array<{ with: Record<string, unknown> }>;
|
|
76
|
+
expect(steps[0].with.permissions).toBe("everyone");
|
|
77
|
+
});
|
|
78
|
+
|
|
56
79
|
test("drops job and step continue-on-error, one warning each", () => {
|
|
57
80
|
const job = new MockJob({
|
|
58
81
|
"runs-on": "ubuntu-latest",
|
package/src/dialect.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* a pre-pass over the resolved entity graph before the github serializer runs:
|
|
8
8
|
*
|
|
9
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* it from the output and warn per occurrence. `permissions` is reduced to
|
|
11
|
+
* the one scope a Forgejo job can still use: `id-token`, which a job that
|
|
12
|
+
* assumes a cloud role over OIDC needs (#3500). Every other scope
|
|
13
|
+
* (`contents`, `pull-requests`, ...) is dropped without a warning, since
|
|
14
|
+
* Forgejo reads none of them and flags the key in the run page's
|
|
15
|
+
* "Workflow warnings" box. A `permissions` block with no `id-token` goes
|
|
16
|
+
* away whole; one with it keeps only that line.
|
|
14
17
|
* 2. GitHub-hosted runner labels (`ubuntu-latest`, …) have no fixed meaning on
|
|
15
18
|
* Forgejo — we map them to a default Forgejo label, overridable per project.
|
|
16
19
|
* 3. Anything we can't place (an unmapped runner label) passes through with a
|
|
@@ -28,12 +31,40 @@ import { isDeclarable, isResourceDeclarable, type Declarable } from "@intentius/
|
|
|
28
31
|
import { resolveActionRef } from "./actions";
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* Keys the Forgejo runner ignores.
|
|
34
|
+
* Keys the Forgejo runner ignores. Emitting them is misleading (they look
|
|
32
35
|
* enforced but aren't), so the dialect drops them. Compared in kebab-case so
|
|
33
36
|
* both `continueOnError` and `continue-on-error` spellings are caught.
|
|
37
|
+
* `permissions` is handled apart, by {@link reducePermissions}.
|
|
34
38
|
*/
|
|
35
39
|
const DROPPED_KEYS = new Set(["continue-on-error"]);
|
|
36
40
|
|
|
41
|
+
/** The one `permissions` scope kept: a job needs it to request an OIDC token. */
|
|
42
|
+
const KEPT_PERMISSION = "id-token";
|
|
43
|
+
|
|
44
|
+
const PERMISSION_LEVELS = new Set(["read", "write", "none"]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* True for a GitHub `permissions` value: `read-all`/`write-all`, or a map of
|
|
48
|
+
* scope to `read`/`write`/`none`. Anything else (an action's `with: {
|
|
49
|
+
* permissions: ... }` input, say) is not one and passes through untouched.
|
|
50
|
+
*/
|
|
51
|
+
function isPermissionsValue(value: unknown): boolean {
|
|
52
|
+
if (typeof value === "string") return value === "read-all" || value === "write-all";
|
|
53
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
54
|
+
const entries = Object.values(value as Record<string, unknown>);
|
|
55
|
+
return entries.every((v) => typeof v === "string" && PERMISSION_LEVELS.has(v));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reduce a `permissions` value to what Forgejo can use: the `id-token` scope,
|
|
60
|
+
* or `undefined` when there is none (the caller then omits the key).
|
|
61
|
+
*/
|
|
62
|
+
function reducePermissions(value: unknown): Record<string, unknown> | undefined {
|
|
63
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
64
|
+
const scope = (value as Record<string, unknown>)[KEPT_PERMISSION];
|
|
65
|
+
return scope === undefined ? undefined : { [KEPT_PERMISSION]: scope };
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
/** Property key whose value is a runner-label selector. */
|
|
38
69
|
const RUNS_ON_KEY = "runs-on";
|
|
39
70
|
|
|
@@ -117,6 +148,11 @@ function transformValue(value: unknown, ctx: TransformCtx): unknown {
|
|
|
117
148
|
);
|
|
118
149
|
continue;
|
|
119
150
|
}
|
|
151
|
+
if (kebab === "permissions" && isPermissionsValue(v)) {
|
|
152
|
+
const kept = reducePermissions(v);
|
|
153
|
+
if (kept) result[key] = kept;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
120
156
|
if (kebab === RUNS_ON_KEY) {
|
|
121
157
|
result[key] = remapRunsOn(v, ctx);
|
|
122
158
|
continue;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* github → forgejo YAML is near-identical, so the migration itself is thin. The
|
|
5
5
|
* differentiated value is the **compare**: classifying what survives the move
|
|
6
6
|
* and what Forgejo silently drops. Most properties translate verbatim; the
|
|
7
|
-
* useful findings are the keys the Forgejo runner ignores (`permissions
|
|
8
|
-
* `continue-on-error` → `lost`) plus refs/labels that need attention
|
|
7
|
+
* useful findings are the keys the Forgejo runner ignores (`permissions` →
|
|
8
|
+
* `needs-review`, `continue-on-error` → `lost`) plus refs/labels that need attention
|
|
9
9
|
* (unresolved `uses:`, unmapped runner labels → `needs-review`).
|
|
10
10
|
*
|
|
11
11
|
* Mirrors the gitlab lexicon's fate model (translated / approximated /
|
|
@@ -80,8 +80,8 @@ export function analyzeForgejoSecurity(
|
|
|
80
80
|
sourceFile: file,
|
|
81
81
|
category: "needs-review",
|
|
82
82
|
rule: "MIG-FJ-PERMISSIONS",
|
|
83
|
-
note: `permissions: is
|
|
84
|
-
security: { property: "Least-privilege permissions", fate: "needs-review", severity: "warning", reestablish: "Forgejo runner/token settings
|
|
83
|
+
note: `permissions: is dropped, except id-token, which a job needs to request an OIDC token. The Forgejo runner does not enforce the scopes, 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" },
|
|
85
85
|
});
|
|
86
86
|
continue;
|
|
87
87
|
}
|
package/src/migrate.test.ts
CHANGED
|
@@ -23,13 +23,31 @@ 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).toContain("permissions");
|
|
26
|
+
expect(output).not.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
|
|
30
30
|
expect(output).toContain("some-org/custom-action@v1");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("keeps id-token from a job's permissions, for OIDC", async () => {
|
|
34
|
+
const { output } = await transform(
|
|
35
|
+
`on: push
|
|
36
|
+
jobs:
|
|
37
|
+
deploy:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write
|
|
41
|
+
contents: read
|
|
42
|
+
steps:
|
|
43
|
+
- run: echo hi
|
|
44
|
+
`,
|
|
45
|
+
{ sourceFile: "deploy.yml" },
|
|
46
|
+
);
|
|
47
|
+
expect(output).toContain("id-token: write");
|
|
48
|
+
expect(output).not.toContain("contents: read");
|
|
49
|
+
});
|
|
50
|
+
|
|
33
51
|
test("classifies property fates (provenance + posture)", async () => {
|
|
34
52
|
const { provenance, securityPosture, diagnostics } = await transform(GHA_WORKFLOW, { sourceFile: "ci.yml" });
|
|
35
53
|
const rules = provenance.map((r) => r.rule);
|
package/src/serializer.test.ts
CHANGED
|
@@ -47,9 +47,9 @@ describe("forgejoSerializer — github-style source roundtrip", () => {
|
|
|
47
47
|
expect(primary).toContain("npm run build");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
test("
|
|
50
|
+
test("drops permissions silently, drops continue-on-error with a warning on each", () => {
|
|
51
51
|
const result = asResult(forgejoSerializer.serialize(buildSource()));
|
|
52
|
-
expect(result.primary).toContain("permissions");
|
|
52
|
+
expect(result.primary).not.toContain("permissions");
|
|
53
53
|
expect(result.primary).not.toContain("continue-on-error");
|
|
54
54
|
// job continue-on-error + step continue-on-error
|
|
55
55
|
expect((result.warnings ?? []).filter((w) => w.includes("permissions"))).toHaveLength(0);
|
|
@@ -128,7 +128,7 @@ describe("forgejoSerializer — multi-workflow output", () => {
|
|
|
128
128
|
// "@intentius/chant-lexicon-github"`); nothing forgejo-specific is written for
|
|
129
129
|
// it. This is the functional check that inheritance actually holds, not just
|
|
130
130
|
// that the type is importable: the dialect still drops the job's
|
|
131
|
-
// `permissions` and remaps its runner label, and the sticky-comment step — a
|
|
131
|
+
// `permissions` (but for `id-token`) and remaps its runner label, and the sticky-comment step — a
|
|
132
132
|
// plain `gh api` script with no `uses:` at all — passes through the dialect
|
|
133
133
|
// transform untouched, because the dialect only ever rewrites job-level
|
|
134
134
|
// `permissions:`/`environment:`/action refs and never looks at step content.
|
|
@@ -162,8 +162,8 @@ 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`, which the dialect
|
|
166
|
-
expect(result.primary).toContain("permissions:");
|
|
165
|
+
// The job carries `permissions` with no `id-token`, which the dialect drops.
|
|
166
|
+
expect(result.primary).not.toContain("permissions:");
|
|
167
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");
|
|
@@ -38,8 +38,9 @@ chant build src -o .forgejo/workflows/ci.yml
|
|
|
38
38
|
|
|
39
39
|
## What the dialect does on build
|
|
40
40
|
|
|
41
|
-
- **Drops keys the Forgejo runner ignores** — `
|
|
42
|
-
`
|
|
41
|
+
- **Drops keys the Forgejo runner ignores** — `continue-on-error` is removed
|
|
42
|
+
with a build warning. `permissions` is removed silently, except `id-token`,
|
|
43
|
+
which stays for OIDC.
|
|
43
44
|
- **Maps runner labels** — `ubuntu-latest` → `docker` by default; override via
|
|
44
45
|
`forgejo.runnerLabels` in `chant.config.ts`. Unmapped labels warn.
|
|
45
46
|
- **Resolves `uses:` refs** — common `actions/*` rewrite under
|
|
@@ -53,7 +54,7 @@ chant migrate .github/workflows/ci.yml --to forgejo -o .forgejo/workflows/ci.yml
|
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
`--validate` prints a **Security posture** report: what survives the move and
|
|
56
|
-
what Forgejo silently drops (`
|
|
57
|
+
what Forgejo silently drops (`continue-on-error` → lost, `permissions` → needs-review,
|
|
57
58
|
unresolved `uses:` / unmapped runner labels → needs-review). The same view is
|
|
58
59
|
the `forgejo:compare` MCP tool.
|
|
59
60
|
|