@intentius/chant-lexicon-forgejo 0.4.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.
- package/README.md +116 -0
- package/package.json +46 -0
- package/src/actions.test.ts +82 -0
- package/src/actions.ts +112 -0
- package/src/dialect.test.ts +104 -0
- package/src/dialect.ts +189 -0
- package/src/index.ts +50 -0
- package/src/lint/post-synth/wfj.test.ts +80 -0
- package/src/lint/post-synth/wfj010.ts +38 -0
- package/src/lint/post-synth/wfj011.ts +41 -0
- package/src/mcp/context-tools.test.ts +99 -0
- package/src/mcp/context-tools.ts +263 -0
- package/src/migrate/from-github/index.ts +103 -0
- package/src/migrate/from-github/security.test.ts +120 -0
- package/src/migrate/from-github/security.ts +195 -0
- package/src/migrate.mcp.test.ts +75 -0
- package/src/migrate.test.ts +71 -0
- package/src/plugin.ts +173 -0
- package/src/serializer.test.ts +124 -0
- package/src/serializer.ts +62 -0
- package/src/skills/chant-forgejo.md +68 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { Workflow, Job, Step, Checkout, SetupNode } from "@intentius/chant-lexicon-github";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
5
|
+
import { forgejoSerializer } from "./serializer";
|
|
6
|
+
import { DEFAULT_ACTIONS_ROOT } from "./actions";
|
|
7
|
+
|
|
8
|
+
function asResult(out: string | SerializerResult): SerializerResult {
|
|
9
|
+
return typeof out === "string" ? { primary: out } : out;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("forgejoSerializer identity", () => {
|
|
13
|
+
test("claims the github partition with a distinct rule prefix", () => {
|
|
14
|
+
// name is "github" on purpose — it serializes github-lexicon entities.
|
|
15
|
+
expect(forgejoSerializer.name).toBe("github");
|
|
16
|
+
expect(forgejoSerializer.rulePrefix).toBe("WFJ");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("forgejoSerializer — github-style source roundtrip", () => {
|
|
21
|
+
function buildSource(): Map<string, Declarable> {
|
|
22
|
+
const workflow = new Workflow({
|
|
23
|
+
name: "CI",
|
|
24
|
+
on: { push: { branches: ["main"] } },
|
|
25
|
+
permissions: { contents: "read" },
|
|
26
|
+
}) as unknown as Declarable;
|
|
27
|
+
const build = new Job({
|
|
28
|
+
"runs-on": "ubuntu-latest",
|
|
29
|
+
"continue-on-error": true,
|
|
30
|
+
steps: [
|
|
31
|
+
new Step({ name: "Build", run: "npm run build", "continue-on-error": true }),
|
|
32
|
+
new Step({ name: "Test", run: "npm test" }),
|
|
33
|
+
],
|
|
34
|
+
}) as unknown as Declarable;
|
|
35
|
+
return new Map<string, Declarable>([
|
|
36
|
+
["workflow", workflow],
|
|
37
|
+
["build", build],
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("emits on/jobs/steps from reused github entities", () => {
|
|
42
|
+
const { primary } = asResult(forgejoSerializer.serialize(buildSource()));
|
|
43
|
+
expect(primary).toContain("on:");
|
|
44
|
+
expect(primary).toContain("jobs:");
|
|
45
|
+
expect(primary).toContain("push:");
|
|
46
|
+
expect(primary).toContain("Build");
|
|
47
|
+
expect(primary).toContain("npm run build");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("drops permissions and continue-on-error, warning on each", () => {
|
|
51
|
+
const result = asResult(forgejoSerializer.serialize(buildSource()));
|
|
52
|
+
expect(result.primary).not.toContain("permissions");
|
|
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);
|
|
56
|
+
expect((result.warnings ?? []).filter((w) => w.includes("continue-on-error"))).toHaveLength(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("maps ubuntu-latest to the default Forgejo runner label", () => {
|
|
60
|
+
const { primary } = asResult(forgejoSerializer.serialize(buildSource()));
|
|
61
|
+
expect(primary).toContain("runs-on: docker");
|
|
62
|
+
expect(primary).not.toContain("ubuntu-latest");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a chant.config forgejo.runnerLabels override changes the label", () => {
|
|
66
|
+
const out = forgejoSerializer.serialize(buildSource(), undefined, {
|
|
67
|
+
config: { forgejo: { runnerLabels: { "ubuntu-latest": "big-runner" } } },
|
|
68
|
+
});
|
|
69
|
+
expect(asResult(out).primary).toContain("runs-on: big-runner");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an unmapped label passes through (WFJ011 reports it post-synth, not the serializer)", () => {
|
|
73
|
+
const job = new Job({ "runs-on": "windows-latest", steps: [new Step({ run: "echo hi" })] }) as unknown as Declarable;
|
|
74
|
+
const out = forgejoSerializer.serialize(new Map([["build", job]]));
|
|
75
|
+
const result = asResult(out);
|
|
76
|
+
expect(result.primary).toContain("runs-on: windows-latest");
|
|
77
|
+
expect((result.warnings ?? []).filter((w) => w.includes("windows-latest"))).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("forgejoSerializer — uses: action resolution", () => {
|
|
82
|
+
function withSteps(...steps: unknown[]): Map<string, Declarable> {
|
|
83
|
+
const job = new Job({ "runs-on": "ubuntu-latest", steps }) as unknown as Declarable;
|
|
84
|
+
return new Map<string, Declarable>([["build", job]]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test("rewrites composite-emitted action refs under the actions root", () => {
|
|
88
|
+
const { primary } = asResult(
|
|
89
|
+
forgejoSerializer.serialize(withSteps(Checkout({}).step, SetupNode({ nodeVersion: "22" }).step)),
|
|
90
|
+
);
|
|
91
|
+
expect(primary).toContain(`uses: ${DEFAULT_ACTIONS_ROOT}/actions/checkout@v4`);
|
|
92
|
+
expect(primary).toContain(`uses: ${DEFAULT_ACTIONS_ROOT}/actions/setup-node@v4`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("forgejo.actionsRoot override changes the rewritten base", () => {
|
|
96
|
+
const out = forgejoSerializer.serialize(withSteps(Checkout({}).step), undefined, {
|
|
97
|
+
config: { forgejo: { actionsRoot: "https://codeberg.org" } },
|
|
98
|
+
});
|
|
99
|
+
expect(asResult(out).primary).toContain("uses: https://codeberg.org/actions/checkout@v4");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("an unmapped action ref is passed through (WFJ010 reports it post-synth)", () => {
|
|
103
|
+
const step = new Step({ name: "Custom", uses: "some-org/custom-action@v1" });
|
|
104
|
+
const result = asResult(forgejoSerializer.serialize(withSteps(step)));
|
|
105
|
+
expect(result.primary).toContain("uses: some-org/custom-action@v1");
|
|
106
|
+
expect((result.warnings ?? []).filter((w) => w.includes("some-org/custom-action@v1"))).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("forgejoSerializer — multi-workflow output", () => {
|
|
111
|
+
test("additional workflows become separate files", () => {
|
|
112
|
+
const a = new Workflow({ name: "A", on: { push: {} } }) as unknown as Declarable;
|
|
113
|
+
const b = new Workflow({ name: "B", on: { push: {} } }) as unknown as Declarable;
|
|
114
|
+
const out = forgejoSerializer.serialize(
|
|
115
|
+
new Map<string, Declarable>([
|
|
116
|
+
["ci", a],
|
|
117
|
+
["release", b],
|
|
118
|
+
]),
|
|
119
|
+
);
|
|
120
|
+
const result = asResult(out);
|
|
121
|
+
expect(result.primary).toContain("name: A");
|
|
122
|
+
expect(Object.keys(result.files ?? {})).toContain("release.yml");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo Actions YAML serializer.
|
|
3
|
+
*
|
|
4
|
+
* A thin dialect of the github serializer: it applies the Forgejo dialect
|
|
5
|
+
* (see ./dialect) to the entity graph, then delegates emission to the github
|
|
6
|
+
* serializer, which already produces GitHub-Actions-compatible YAML — exactly
|
|
7
|
+
* what Forgejo / Codeberg / Gitea runners execute.
|
|
8
|
+
*
|
|
9
|
+
* The serializer's `name` is "github" on purpose: github-lexicon entities are
|
|
10
|
+
* tagged `lexicon: "github"`, and the build pipeline partitions and looks up
|
|
11
|
+
* serializers by that tag. A forgejo project loads only this serializer, so it
|
|
12
|
+
* claims that partition. The distinct `rulePrefix` ("WFJ") keeps Forgejo lint
|
|
13
|
+
* diagnostics namespaced apart from github's "GHA".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
17
|
+
import type { Serializer, SerializerResult, SerializeContext } from "@intentius/chant/serializer";
|
|
18
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
19
|
+
import { githubSerializer } from "@intentius/chant-lexicon-github";
|
|
20
|
+
import { applyForgejoDialect, type ForgejoDialectOptions } from "./dialect";
|
|
21
|
+
|
|
22
|
+
/** Extract forgejo dialect options from the resolved project config, if any. */
|
|
23
|
+
function readForgejoOptions(config: Record<string, unknown> | undefined): ForgejoDialectOptions {
|
|
24
|
+
const forgejo = config?.forgejo;
|
|
25
|
+
if (!forgejo || typeof forgejo !== "object") return {};
|
|
26
|
+
const obj = forgejo as Record<string, unknown>;
|
|
27
|
+
const options: ForgejoDialectOptions = {};
|
|
28
|
+
if (obj.runnerLabels && typeof obj.runnerLabels === "object") {
|
|
29
|
+
options.runnerLabels = obj.runnerLabels as Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
if (typeof obj.actionsRoot === "string") {
|
|
32
|
+
options.actionsRoot = obj.actionsRoot;
|
|
33
|
+
}
|
|
34
|
+
return options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Forgejo Actions YAML serializer implementation.
|
|
39
|
+
*/
|
|
40
|
+
export const forgejoSerializer: Serializer = {
|
|
41
|
+
name: "github",
|
|
42
|
+
rulePrefix: "WFJ",
|
|
43
|
+
|
|
44
|
+
serialize(
|
|
45
|
+
entities: Map<string, Declarable>,
|
|
46
|
+
outputs?: LexiconOutput[],
|
|
47
|
+
context?: SerializeContext,
|
|
48
|
+
): string | SerializerResult {
|
|
49
|
+
const options = readForgejoOptions(context?.config);
|
|
50
|
+
const { entities: transformed, warnings } = applyForgejoDialect(entities, options);
|
|
51
|
+
|
|
52
|
+
const result = githubSerializer.serialize(transformed, outputs, context);
|
|
53
|
+
|
|
54
|
+
if (typeof result === "string") {
|
|
55
|
+
return warnings.length > 0 ? { primary: result, warnings } : result;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...result,
|
|
59
|
+
warnings: [...(result.warnings ?? []), ...warnings],
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# chant-forgejo
|
|
2
|
+
|
|
3
|
+
Forgejo / Codeberg / Gitea Actions with chant.
|
|
4
|
+
|
|
5
|
+
Forgejo runs **GitHub-Actions-compatible** workflows, so the forgejo lexicon is
|
|
6
|
+
a thin dialect of the github lexicon. You author exactly as you would for GitHub
|
|
7
|
+
Actions — same `Workflow`, `Job`, `Step`, and composites — but import from
|
|
8
|
+
`@intentius/chant-lexicon-forgejo`. The dialect applies on build.
|
|
9
|
+
|
|
10
|
+
## Authoring
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Workflow, Job, Step, Checkout, SetupNode } from "@intentius/chant-lexicon-forgejo";
|
|
14
|
+
|
|
15
|
+
export const workflow = new Workflow({
|
|
16
|
+
name: "CI",
|
|
17
|
+
on: { push: { branches: ["main"] } },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const build = new Job({
|
|
21
|
+
"runs-on": "ubuntu-latest",
|
|
22
|
+
steps: [
|
|
23
|
+
Checkout({}).step,
|
|
24
|
+
SetupNode({ nodeVersion: "22", cache: "npm" }).step,
|
|
25
|
+
new Step({ name: "Test", run: "npm test" }),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Build
|
|
31
|
+
|
|
32
|
+
Forgejo reads workflows from `.forgejo/workflows/` (or `.gitea/workflows/` for
|
|
33
|
+
Gitea):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
chant build src -o .forgejo/workflows/ci.yml
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What the dialect does on build
|
|
40
|
+
|
|
41
|
+
- **Drops keys the Forgejo runner ignores** — `permissions` and
|
|
42
|
+
`continue-on-error` are removed (each emits a build warning).
|
|
43
|
+
- **Maps runner labels** — `ubuntu-latest` → `docker` by default; override via
|
|
44
|
+
`forgejo.runnerLabels` in `chant.config.ts`. Unmapped labels warn.
|
|
45
|
+
- **Resolves `uses:` refs** — common `actions/*` rewrite under
|
|
46
|
+
`forgejo.actionsRoot` (default `https://code.forgejo.org`); `docker/*` pin to
|
|
47
|
+
their GitHub URL; unmapped refs warn.
|
|
48
|
+
|
|
49
|
+
## Migrating from GitHub Actions
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
chant migrate .github/workflows/ci.yml --to forgejo -o .forgejo/workflows/ci.yml --validate
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--validate` prints a **Security posture** report: what survives the move and
|
|
56
|
+
what Forgejo silently drops (`permissions`/`continue-on-error` → lost,
|
|
57
|
+
unresolved `uses:` / unmapped runner labels → needs-review). The same view is
|
|
58
|
+
the `forgejo:compare` MCP tool.
|
|
59
|
+
|
|
60
|
+
## Read-only context tools (MCP)
|
|
61
|
+
|
|
62
|
+
- `forgejo:workflow` — triggers and jobs as written
|
|
63
|
+
- `forgejo:references` — external actions/images and whether each is SHA-pinned
|
|
64
|
+
- `forgejo:affected` — jobs that re-run downstream of a given job
|
|
65
|
+
- `forgejo:checks` — Forgejo-specific findings (WFJ010/011)
|
|
66
|
+
- `forgejo:source` / `forgejo:owns` — where a job came from / whether it's declared here
|
|
67
|
+
- `forgejo:workflow-yaml` — the generated YAML
|
|
68
|
+
- `forgejo:compare` — the github→forgejo migration safety view
|