@intentius/chant-lexicon-forgejo 0.124.30 → 0.124.32

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.
Files changed (36) hide show
  1. package/dist/actions.js +96 -0
  2. package/dist/codegen/docs-cli.js +6 -0
  3. package/dist/codegen/docs.js +99 -0
  4. package/dist/codegen/generate-cli.js +7 -0
  5. package/dist/codegen/generate.js +31 -0
  6. package/dist/codegen/package-cli.js +12 -0
  7. package/dist/codegen/package.js +42 -0
  8. package/dist/components/ci-jobs.js +71 -0
  9. package/dist/components/generate-op-pipeline.js +116 -0
  10. package/dist/components/generate-op-waves-pipeline.js +66 -0
  11. package/dist/components/generate-pipeline.js +51 -0
  12. package/dist/config.js +36 -0
  13. package/dist/dialect.js +198 -0
  14. package/dist/index.js +24 -0
  15. package/dist/integrity.json +2 -2
  16. package/dist/lint/audit-catalog.js +9 -0
  17. package/dist/lint/post-synth/index.js +6 -0
  18. package/dist/lint/post-synth/wfj010.js +34 -0
  19. package/dist/lint/post-synth/wfj011.js +36 -0
  20. package/dist/lint/rules/delegate-to-github.js +30 -0
  21. package/dist/lint/rules/index.js +5 -0
  22. package/dist/lsp/completions.js +13 -0
  23. package/dist/lsp/hover.js +11 -0
  24. package/dist/manifest.json +1 -1
  25. package/dist/mcp/context-tools.js +243 -0
  26. package/dist/migrate/from-github/index.js +63 -0
  27. package/dist/migrate/from-github/security.js +140 -0
  28. package/dist/plugin.js +206 -0
  29. package/dist/serializer.js +50 -0
  30. package/package.json +7 -7
  31. package/src/codegen/package.ts +2 -2
  32. package/src/components/__fixtures__/pr-loop-build.forgejo.golden.yml +6 -2
  33. package/src/components/__fixtures__/pr-loop-waves.forgejo.golden.yml +117 -0
  34. package/src/components/__fixtures__/pr-loop.forgejo.golden.yml +6 -2
  35. package/src/components/generate-pr-pipeline.test.ts +18 -1
  36. package/src/components/generate-pr-waves-pipeline.test.ts +42 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Forgejo `uses:` action-reference resolver.
3
+ *
4
+ * GitHub Actions resolves a bare `uses: actions/checkout@v4` against the GitHub
5
+ * Marketplace. Forgejo / Codeberg / Gitea runners have no Marketplace, so such a
6
+ * ref must be rewritten to a form their runner can fetch — a full repository URL,
7
+ * or a path under a configured actions root that mirrors the action.
8
+ *
9
+ * The resolver rewrites a built-in set of common actions and reports anything it
10
+ * can't place as a diagnostic (never dropping or silently passing it through).
11
+ */
12
+ /**
13
+ * Default actions root. `code.forgejo.org` hosts the Forgejo `actions/*` mirror
14
+ * (checkout, setup-*, cache, upload/download-artifact, …). Override per project
15
+ * via `forgejo.actionsRoot` in `chant.config.ts`.
16
+ */
17
+ export const DEFAULT_ACTIONS_ROOT = "https://code.forgejo.org";
18
+ /**
19
+ * `upload-artifact` and `download-artifact` v4 and later use `@actions/artifact`
20
+ * v2, which refuses to run on GitHub Enterprise Server, and a Forgejo runner
21
+ * presents itself to an action as one: the step fails with "@actions/artifact
22
+ * v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently
23
+ * supported on GHES" (#4008). v3 talks to the artifact service Forgejo serves.
24
+ */
25
+ export const FORGEJO_ARTIFACT_VERSION = "v3";
26
+ /** Built-in mapping of GitHub action `owner/repo` → Forgejo-resolvable target. */
27
+ export const KNOWN_ACTIONS = {
28
+ // actions/* — mirrored on the Forgejo actions root.
29
+ "actions/checkout": { kind: "mirror", repo: "actions/checkout" },
30
+ "actions/setup-node": { kind: "mirror", repo: "actions/setup-node" },
31
+ "actions/setup-go": { kind: "mirror", repo: "actions/setup-go" },
32
+ "actions/setup-python": { kind: "mirror", repo: "actions/setup-python" },
33
+ "actions/setup-java": { kind: "mirror", repo: "actions/setup-java" },
34
+ "actions/setup-dotnet": { kind: "mirror", repo: "actions/setup-dotnet" },
35
+ "actions/cache": { kind: "mirror", repo: "actions/cache" },
36
+ "actions/upload-artifact": { kind: "mirror", repo: "actions/upload-artifact", newest: FORGEJO_ARTIFACT_VERSION },
37
+ "actions/download-artifact": { kind: "mirror", repo: "actions/download-artifact", newest: FORGEJO_ARTIFACT_VERSION },
38
+ // docker/* — not on the Forgejo mirror; pin to the full GitHub URL, which
39
+ // Forgejo runners can fetch directly.
40
+ "docker/build-push-action": { kind: "url", url: "https://github.com/docker/build-push-action" },
41
+ "docker/login-action": { kind: "url", url: "https://github.com/docker/login-action" },
42
+ "docker/setup-buildx-action": { kind: "url", url: "https://github.com/docker/setup-buildx-action" },
43
+ "docker/setup-qemu-action": { kind: "url", url: "https://github.com/docker/setup-qemu-action" },
44
+ "docker/metadata-action": { kind: "url", url: "https://github.com/docker/metadata-action" },
45
+ };
46
+ /** A ref that already resolves on Forgejo as-is needs no rewrite and no warning. */
47
+ function isAlreadyResolvable(ref) {
48
+ return (ref.startsWith("./") ||
49
+ ref.startsWith("../") ||
50
+ ref.startsWith(".\\") ||
51
+ ref.startsWith("docker://") ||
52
+ /^https?:\/\//.test(ref));
53
+ }
54
+ /**
55
+ * Resolve a single `uses:` action ref to a Forgejo-resolvable form.
56
+ */
57
+ export function resolveActionRef(ref, options = {}) {
58
+ const trimmed = ref.trim();
59
+ if (trimmed === "" || isAlreadyResolvable(trimmed))
60
+ return { rewritten: ref };
61
+ const root = (options.actionsRoot ?? DEFAULT_ACTIONS_ROOT).replace(/\/+$/, "");
62
+ const known = options.known ?? KNOWN_ACTIONS;
63
+ const atIndex = trimmed.lastIndexOf("@");
64
+ const path = atIndex >= 0 ? trimmed.slice(0, atIndex) : trimmed;
65
+ const version = atIndex >= 0 ? trimmed.slice(atIndex + 1) : "";
66
+ const segments = path.split("/");
67
+ if (segments.length < 2) {
68
+ return { rewritten: ref, warning: unresolvedWarning(trimmed) };
69
+ }
70
+ const actionName = `${segments[0]}/${segments[1]}`;
71
+ const subpath = segments.slice(2).join("/");
72
+ const target = known[actionName];
73
+ if (!target) {
74
+ return { rewritten: ref, warning: unresolvedWarning(trimmed) };
75
+ }
76
+ const base = target.kind === "mirror" ? `${root}/${target.repo}` : target.url;
77
+ const withSubpath = subpath ? `${base}/${subpath}` : base;
78
+ const pinned = target.kind === "mirror" && target.newest && laterMajor(version, target.newest) ? target.newest : version;
79
+ const rewritten = pinned ? `${withSubpath}@${pinned}` : withSubpath;
80
+ return { rewritten };
81
+ }
82
+ /** Whether `version` (`v4`, `v4.1.0`) names a later major than `newest` (`v3`). A SHA or branch never does. */
83
+ function laterMajor(version, newest) {
84
+ const major = (v) => {
85
+ const m = /^v?(\d+)(?:\.\d+)*$/.exec(v);
86
+ return m ? Number(m[1]) : undefined;
87
+ };
88
+ const a = major(version);
89
+ const b = major(newest);
90
+ return a !== undefined && b !== undefined && a > b;
91
+ }
92
+ function unresolvedWarning(ref) {
93
+ return (`forgejo: unresolved action ref '${ref}' — it has no built-in mapping and won't ` +
94
+ `resolve from a GitHub Marketplace on Forgejo. Use a full repository URL, or mirror ` +
95
+ `it under forgejo.actionsRoot in chant.config.ts.`);
96
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run docs` in lexicon-forgejo.
4
+ */
5
+ import { generateDocs } from "./docs.js";
6
+ await generateDocs({ verbose: true });
@@ -0,0 +1,99 @@
1
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
2
+ const OVERVIEW_CONTENT = `\`@intentius/chant-lexicon-forgejo\` targets **Forgejo Actions** — the CI behind
3
+ [Codeberg](https://codeberg.org), self-hosted [Forgejo](https://forgejo.org),
4
+ and [Gitea](https://about.gitea.com). Forgejo runs **GitHub-Actions-compatible**
5
+ workflows, so the lexicon is a **thin dialect of the [github lexicon](/chant/lexicons/github/)**
6
+ (chant #338): every entity, composite, and expression helper is the same class
7
+ you'd import from the github package. This package supplies only what is
8
+ genuinely Forgejo-specific — the serializer dialect and a github → forgejo
9
+ migration path. One package covers Codeberg, self-hosted Forgejo, and Gitea.
10
+
11
+ Author exactly as you would for GitHub Actions, importing from the forgejo
12
+ package; see [The Forgejo Dialect](/chant/lexicons/forgejo/dialect/) for what
13
+ changes on the way out.
14
+
15
+ ## Build
16
+
17
+ Forgejo reads workflows from \`.forgejo/workflows/\` (or \`.gitea/workflows/\` for
18
+ Gitea), so point the output there — the same way the github lexicon targets
19
+ \`.github/workflows/\`:
20
+
21
+ \`\`\`sh
22
+ chant build src -o .forgejo/workflows/ci.yml
23
+ \`\`\`
24
+
25
+ ## Configuration
26
+
27
+ Override the runner-label map and the actions root in \`chant.config.ts\`:
28
+
29
+ \`\`\`ts
30
+ import type { ChantConfig } from "@intentius/chant/config";
31
+ // Brings the \`forgejo\` key into ChantConfig (chant #1344).
32
+ import "@intentius/chant-lexicon-forgejo";
33
+
34
+ export default {
35
+ lexicons: ["forgejo"],
36
+ forgejo: {
37
+ runnerLabels: {
38
+ "ubuntu-latest": "docker",
39
+ "ubuntu-22.04": "ubuntu-lts",
40
+ },
41
+ actionsRoot: "https://code.forgejo.org",
42
+ },
43
+ } satisfies ChantConfig;
44
+ \`\`\`
45
+
46
+ ## Read-only context tools
47
+
48
+ The forgejo lexicon ships the full \`forgejo:*\` [read-only context tools](/chant/guide/agent-integration/#read-only-context-tools)
49
+ (\`checks\`, \`workflow\`, \`references\`, \`affected\`, \`workflow-yaml\`, \`source\`,
50
+ \`owns\`, \`compare\`). Each builds from source and returns what chant already
51
+ computes — without touching any live forge.
52
+
53
+ ## Runtime observation
54
+
55
+ N/A — workflow definitions are git-tracked, so drift is a \`git diff\`, the same
56
+ rationale as the github and gitlab lexicons. The lexicon implements neither
57
+ \`describeResources()\` nor \`listArtifacts()\` and is warn-skipped on \`--live\`.
58
+
59
+ ## Validating execution
60
+
61
+ A runtime E2E (\`just forgejo-runtime-e2e\`) builds a workflow and **runs** the
62
+ generated \`.forgejo/workflows/ci.yml\` in Docker via a Forgejo runner
63
+ (\`forgejo-runner\` / \`act_runner\` / \`act\` \`exec\`), proving Forgejo Actions
64
+ accepts and executes chant's output. On-demand — it needs Docker and a runner
65
+ tool, and is not part of the gating CI.
66
+
67
+ A second harness (\`just forgejo-preview-e2e\`, chant #1223) drives a
68
+ chant-declared per-PR preview workflow through stubbed \`pull_request\`
69
+ opened/closed events: the on-open job deploys an isolated environment to a
70
+ live emulator, the on-close job sweeps it by its ownership marker, and the
71
+ harness asserts the environment's resources exist between the two runs and
72
+ are gone after. Same on-demand terms.
73
+
74
+ ## Caveats
75
+
76
+ Flow-style YAML (\`branches: [main]\`) is parsed by chant's lightweight core
77
+ parser, which keeps it as a scalar — prefer block style in sources you intend to
78
+ migrate. This is shared with the github import path.
79
+ `;
80
+ /**
81
+ * Generate documentation site for the Forgejo lexicon.
82
+ */
83
+ export async function generateDocs(options) {
84
+ const config = {
85
+ name: "forgejo",
86
+ displayName: "Forgejo",
87
+ description: "Forgejo / Codeberg / Gitea Actions lexicon documentation — a thin GitHub Actions dialect",
88
+ distDir: "./dist",
89
+ outDir: "./docs",
90
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/forgejo/",
91
+ overview: OVERVIEW_CONTENT,
92
+ outputFormat: "The Forgejo lexicon serializes entities into the same YAML shape as GitHub Actions, then applies the Forgejo dialect (see [The Forgejo Dialect](/chant/lexicons/forgejo/dialect/)) before writing to `.forgejo/workflows/*.yml` (or `.gitea/workflows/*.yml` for Gitea).",
93
+ };
94
+ const result = docsPipeline(config);
95
+ writeDocsSite(config, result);
96
+ if (options?.verbose) {
97
+ console.error("Documentation generated");
98
+ }
99
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run generate` in lexicon-forgejo.
4
+ */
5
+ import { generate } from "./generate.js";
6
+ const result = await generate({ verbose: true });
7
+ console.error(`forgejo: ${result.resources} resources (reuses github's wholesale)`);
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Forgejo lexicon "generation" step.
3
+ *
4
+ * Forgejo has no spec of its own — every entity, property type, and enum a
5
+ * forgejo project uses comes from `@intentius/chant-lexicon-github` (see
6
+ * src/index.ts). There is nothing to fetch or parse here; this module exists
7
+ * only so `packagePipeline` (./package.ts) has a `GenerateResult` to build
8
+ * dist/manifest.json from. Refresh github's entities by running `chant
9
+ * generate` in the github lexicon, not here.
10
+ */
11
+ // No entities of forgejo's own — the catalog is intentionally empty.
12
+ const LEXICON_JSON = "{}";
13
+ const TYPES_DTS = `// forgejo has no resource types of its own. Every entity a forgejo
14
+ // project imports is declared in @intentius/chant-lexicon-github and
15
+ // re-exported from this package's main entry point.
16
+ export {};
17
+ `;
18
+ export async function generate(opts) {
19
+ if (opts?.verbose) {
20
+ console.error("forgejo: no spec of its own — reuses the github lexicon's entities wholesale (0 own resources)");
21
+ }
22
+ return {
23
+ lexiconJSON: LEXICON_JSON,
24
+ typesDTS: TYPES_DTS,
25
+ indexTS: "",
26
+ resources: 0,
27
+ properties: 0,
28
+ enums: 0,
29
+ warnings: [],
30
+ };
31
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run bundle` in lexicon-forgejo.
4
+ */
5
+ import { packageLexicon } from "./package.js";
6
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
7
+ import { join, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
10
+ const { spec, stats } = await packageLexicon({ verbose: true });
11
+ writeBundleSpec(spec, join(pkgDir, "dist"));
12
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Forgejo lexicon packaging — delegates to core packagePipeline.
3
+ *
4
+ * Bundles what forgejo actually ships (its post-synth checks, its skill,
5
+ * an empty resource catalog) into dist/manifest.json + dist/meta.json so
6
+ * the lexicon has a real, versioned package manifest like every other
7
+ * lexicon — see ./generate.ts for why the catalog itself is empty.
8
+ */
9
+ import { readFileSync } from "fs";
10
+ import { forgejoPlugin } from "../plugin.js";
11
+ import { join, dirname } from "path";
12
+ import { moduleSourceDir } from "@intentius/chant/module-source";
13
+ import { packagePipeline, collectSkills, } from "@intentius/chant/codegen/package";
14
+ import { generate } from "./generate.js";
15
+ // package.ts is at src/codegen/package.ts — 2 dirname calls reach src/
16
+ // then join(pkgDir, "..") is the package root where package.json lives
17
+ const pkgDir = dirname(moduleSourceDir(import.meta.url));
18
+ /**
19
+ * Package the Forgejo lexicon into a distributable BundleSpec.
20
+ */
21
+ export async function packageLexicon(opts = {}) {
22
+ const pkgJson = JSON.parse(readFileSync(join(pkgDir, "..", "package.json"), "utf-8"));
23
+ return packagePipeline({
24
+ generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
25
+ buildManifest: (_genResult) => {
26
+ return {
27
+ name: "forgejo",
28
+ version: pkgJson.version ?? "0.0.1",
29
+ chantVersion: ">=0.1.0",
30
+ namespace: "Forgejo",
31
+ intrinsics: [],
32
+ pseudoParameters: {},
33
+ };
34
+ },
35
+ srcDir: pkgDir,
36
+ collectSkills: () => {
37
+ const skillDefs = forgejoPlugin.skills?.() ?? [];
38
+ return collectSkills(skillDefs);
39
+ },
40
+ version: pkgJson.version ?? "0.0.1",
41
+ }, opts);
42
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The Forgejo dialect of the three generated pipelines (#3765). Each forgejo
3
+ * generator builds github's document and hands it here: action refs and
4
+ * runner labels rewritten ({@link transformWorkflowObject}), `permissions:`
5
+ * dropped (the runner reads none), and a job's `environment:` dropped with a
6
+ * header note naming it, since Forgejo Actions has no environments.
7
+ */
8
+ import { githubSecret } from "@intentius/chant/ci-jobs";
9
+ import { transformWorkflowObject } from "../dialect.js";
10
+ /** Apply the Forgejo dialect to one section of a pipeline doc. */
11
+ export function forgejoize(value, dialect) {
12
+ return transformWorkflowObject(value, dialect).value;
13
+ }
14
+ /**
15
+ * Strip each job's `environment:` (#2257). Forgejo Actions has no
16
+ * environments, so the key would read as a deployment gate and hold nothing
17
+ * back. Dropped here rather than in the dialect's own `DROPPED_KEYS`, so it
18
+ * touches the generated pipelines alone and leaves every other document the
19
+ * dialect transforms as it is.
20
+ */
21
+ export function withoutEnvironment(jobsDoc) {
22
+ return Object.fromEntries(Object.entries(jobsDoc).map(([jobName, job]) => {
23
+ if (typeof job !== "object" || job === null || !("environment" in job))
24
+ return [jobName, job];
25
+ const { environment: _dropped, ...rest } = job;
26
+ return [jobName, rest];
27
+ }));
28
+ }
29
+ /** The header of an Op's workflow whose environment gate did not survive the crossing. */
30
+ export function droppedEnvironmentHeader(environmentName) {
31
+ return [
32
+ `# chant dropped \`environment: ${environmentName}\` from this workflow: Forgejo Actions has no`,
33
+ "# environments, so there is no protection rule, required reviewer or wait timer for the key to",
34
+ "# name. This job is not held back by anything on the forge. What still stops the apply is chant's",
35
+ "# own gate: the run records a pending fact on the chant/lifecycle branch and ends, and `chant",
36
+ "# approve <op> <gate>` is what lets the next run through.",
37
+ ];
38
+ }
39
+ /** The header of an Op waves workflow, naming each wave whose environment was dropped as `wave -> environment`. */
40
+ export function droppedWaveEnvironmentsHeader(dropped) {
41
+ return [
42
+ `# chant dropped the environment of each of these waves: ${dropped.join(", ")}.`,
43
+ "# Forgejo Actions has no environments, so nothing on the forge holds these jobs.",
44
+ "# Each wave's own chant gate does: it records a pending fact on chant/lifecycle",
45
+ "# and the wave exits 3 until `chant approve` approves its set digest.",
46
+ ];
47
+ }
48
+ /**
49
+ * The token a Forgejo job uses for what the job token cannot do, such as
50
+ * dispatching a workflow: the `CHANT_FORGE_TOKEN` secret, a token with the
51
+ * `write:repository` scope.
52
+ */
53
+ export const FORGE_TOKEN_ENV = { CHANT_FORGE_TOKEN: githubSecret("CHANT_FORGE_TOKEN") };
54
+ /**
55
+ * A github workflow document under the Forgejo dialect: every section
56
+ * transformed, each job's `environment:` dropped, `permissions:` emptied, and
57
+ * `header` above it when given.
58
+ */
59
+ export function forgejoWorkflowDoc(doc, dialect, header) {
60
+ return {
61
+ ...(header ? { header } : {}),
62
+ // Forgejo Actions reads `name:` as GitHub does (#2601).
63
+ ...(doc.name ? { name: doc.name } : {}),
64
+ on: forgejoize(doc.on, dialect),
65
+ ...(doc.env ? { env: forgejoize(doc.env, dialect) } : {}),
66
+ concurrency: forgejoize(doc.concurrency, dialect),
67
+ permissions: {},
68
+ jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialect),
69
+ ...(doc.gatedNoticeDoc ? { gatedNoticeDoc: forgejoize(doc.gatedNoticeDoc, dialect) } : {}),
70
+ };
71
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Generate mode — scheduled Op → Forgejo Actions workflow YAML (#927).
3
+ *
4
+ * The forgejo counterpart to the github Op generator
5
+ * (`@intentius/chant-lexicon-github/components/generate-op-pipeline`, #927),
6
+ * exactly like `./generate-pipeline.ts` is the forgejo counterpart to
7
+ * github's component generator (#969). Reuses github's
8
+ * `buildGithubOpPipelineDocs` to build the same cron-trigger/concurrency/job
9
+ * structure, then applies the Forgejo dialect ({@link transformWorkflowObject})
10
+ * before emitting.
11
+ *
12
+ * One difference from the component generator: Forgejo Actions ignores
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
+ * than emitting a control the runner can't act on. The token a finding-mode
16
+ * needs still rides the trigger step's `env:` (`GH_TOKEN`/`GITHUB_TOKEN`,
17
+ * built by github's generator); actual write access on Forgejo is a property
18
+ * of the runner/token configuration, not the workflow YAML.
19
+ *
20
+ * That covers a spec's additive `permissions` too (#2242): an `id-token:
21
+ * write` added for OIDC is dropped here along with the mode's own scopes,
22
+ * because the Forgejo runner reads none of them and issues no OIDC token off
23
+ * a workflow permission. A Forgejo job authenticates to a cloud provider
24
+ * through whatever the runner already holds. A spec's `setup` steps do cross
25
+ * over unchanged: Forgejo runs `uses:` steps, so they are emitted in the
26
+ * position github puts them (after the checkout, ahead of the `beforeScript`
27
+ * lines) with the dialect's own action-ref rewrite applied — an action with
28
+ * no mapping in ../actions.ts passes through verbatim and resolves only if
29
+ * the runner can fetch it.
30
+ *
31
+ * `comment` (#2231) crosses over, since chant #2291: it posts onto the
32
+ * triggering pull request against `${GITHUB_API_URL}`, and a Forgejo Actions
33
+ * job already sets that (and `github.token`) the same way a GitHub Actions
34
+ * job does. This generator used to refuse the mode by name here, on the
35
+ * premise that chant had no way to reach a Forgejo instance; that premise
36
+ * was checked against a real Forgejo 12.0.4+gitea-1.22.0 instance during
37
+ * INTENTIUS/choudoufu#1027 and found false. Since chant #3757 the post goes
38
+ * through the Forgejo client in `packages/core/src/pr-forge.ts` over `fetch`
39
+ * rather than through `gh`, so the job's image needs no `gh` either.
40
+ *
41
+ * `issue` crosses over too, since chant #2351. It was refused here from
42
+ * #2332 on, after chant #2315 drove the mode against a real Forgejo
43
+ * 12.0.4+gitea-1.22.0 instance: the paginated read and the `.pull_request ==
44
+ * null` filter behaved as on GitHub, but every POST and PATCH answered
45
+ * `{"message":"token is required"}` (HTTP 401), because `gh` sends
46
+ * `GH_TOKEN` only to github.com and `ghe.com` hosts. #2333 fixed that
47
+ * credential, and #3757 moved the mode off `gh` onto the Forgejo client in
48
+ * `packages/core/src/pr-forge.ts`, which sends `Authorization: token …`
49
+ * itself. The job this emits for a cron `issue` Op is github's, unchanged:
50
+ * the same trigger, and the same `GH_TOKEN`/`GITHUB_TOKEN` from
51
+ * `github.token` on the run step, which is the credential the mode reads.
52
+ * Only `permissions:` is dropped, as for every mode, so what that token may
53
+ * write on a scheduled run is the runner's configuration.
54
+ *
55
+ * A spec's `environment` (#2257) is dropped on the same terms as
56
+ * `permissions:`, and for a stronger reason: Forgejo Actions has no
57
+ * environments at all — no protection rules, no required reviewers, no
58
+ * per-environment secrets — so the key names an object that does not exist on
59
+ * the instance. Dropping it silently would be the worst outcome available,
60
+ * because the whole point of the option is a human holding an apply, so this
61
+ * also writes a comment into the generated file's header saying which
62
+ * environment was asked for and that nothing on Forgejo enforces it. What
63
+ * still holds the apply here is chant's own gate (#2119), which is a fact on
64
+ * the `chant/lifecycle` branch and needs nothing from the forge.
65
+ *
66
+ * The gated-apply notice job (#2243) crosses over since #3773. It used to
67
+ * shell to `gh` on GitHub's hosted runner image, which a Forgejo runner
68
+ * neither ships nor can point at its own instance, so it was dropped here
69
+ * along with the apply job's `gated`/`op`/`gate`/`approve` outputs that
70
+ * only it read (#2294). It now runs in the Op's own image and posts through
71
+ * `@intentius/chant/pr-forge`, whose Forgejo client finds the pull request
72
+ * a commit merged with `commits/{sha}/pull` and sends `Authorization: token`
73
+ * itself. So the doc rebuilt below carries `gatedNoticeDoc` through the
74
+ * same dialect transform as the jobs, which drops its `permissions:` the
75
+ * way it drops every scope but `id-token`. A `push` job also still runs
76
+ * with `--gated-exit 0`, and `chant run` still writes the gate and the
77
+ * approve command to `GITHUB_STEP_SUMMARY`.
78
+ *
79
+ * A spec's own `variables` (#2290) crosses over unchanged: forgejo reuses the
80
+ * same job-level `env:` github's builder emits, and the dialect transform
81
+ * touches only `permissions:`/`environment:`/action refs, never a job's `env:`
82
+ * mapping. That is what makes a per-Op credential expressible here at all —
83
+ * `ComponentPipelineOptions.variables` is workflow-scoped and, on Forgejo,
84
+ * Actions mints no OIDC token to put in `setup` instead, so a job-level
85
+ * `variables` entry is the only way one Op's job can hold a credential no
86
+ * sibling Op's job receives.
87
+ */
88
+ import { buildGithubOpPipelineDocs, emitOpPipelineYAML, } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
89
+ import { droppedEnvironmentHeader, forgejoWorkflowDoc } from "./ci-jobs.js";
90
+ /**
91
+ * Synthesize one `.forgejo/workflows/*.yml` per scheduled Op. Reuses github's
92
+ * trigger/job structure ({@link buildGithubOpPipelineDocs}) — its `setup`-step
93
+ * and additive-permission validation included, so an unpinned action ref is
94
+ * refused here on the same terms — then applies the Forgejo dialect and drops
95
+ * `permissions:` (ignored by the Forgejo runner).
96
+ * Wired into core's Op generate mode via the forgejo lexicon plugin's
97
+ * `generateOpPipeline` (../plugin.ts).
98
+ */
99
+ export function generateForgejoOpPipeline(ops, options = {}, dialectOptions = {}) {
100
+ // `findingMode: "comment"` used to be refused by name here (#2231), and
101
+ // `findingMode: "issue"` from #2332 to #2351. Neither is now: both post
102
+ // through the Forgejo client — see the module doc above.
103
+ const { files, jobs } = buildGithubOpPipelineDocs(ops, options);
104
+ return {
105
+ // One file per spec, in spec order, which is what lets the header below
106
+ // read its Op's own `environment` off the input by position.
107
+ files: files.map(({ name, doc }, index) => {
108
+ const environment = ops[index].environment;
109
+ // The Op's name carries over, and the gate-notice job (#3773) takes the
110
+ // dialect like the Op's own job.
111
+ const header = environment ? droppedEnvironmentHeader(environment.name) : undefined;
112
+ return { name, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(doc, dialectOptions, header)) };
113
+ }),
114
+ jobs,
115
+ };
116
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Op waves → Forgejo Actions workflow (#3679).
3
+ *
4
+ * The github workflow ({@link buildGithubOpWavesDoc}) under the Forgejo
5
+ * dialect: action refs rewritten, `permissions:` dropped (the runner reads
6
+ * none), and each wave's `environment:` dropped with a header line saying so,
7
+ * since Forgejo Actions has no environments. The wave's chant gate is what
8
+ * holds it there.
9
+ *
10
+ * With `resume` on the spec (#3683), `<name>-resume.yml` runs `chant run
11
+ * resume --op <name>` on that schedule. Forgejo has no API to re-run a run,
12
+ * so it dispatches the waves workflow again on its branch, with the
13
+ * `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
14
+ *
15
+ * A `pr-review` wave (#3684) gets `<name>-plans.yml` on pull requests, as on
16
+ * GitHub. Forgejo's job token reads reviews with no permission to ask for.
17
+ *
18
+ * `runner` and a wave's `jobs` (#3914) render as on GitHub, with two limits:
19
+ * a runner is a label or a list of labels (a GitHub runner group is a spec
20
+ * error), and a job group cannot name an `environment`, since Forgejo has
21
+ * none to put in front of it. A job with id tokens keeps `permissions:
22
+ * id-token: write`, the one scope the dialect leaves, and reads each audience
23
+ * from `<NAME>_AUDIENCE` when it requests its token.
24
+ */
25
+ import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
26
+ import { emitOpPipelineYAML } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
27
+ import { assertRunnerLabels } from "@intentius/chant/ci-jobs";
28
+ import { droppedWaveEnvironmentsHeader, FORGE_TOKEN_ENV, forgejoWorkflowDoc } from "./ci-jobs.js";
29
+ /** Render an Op waves spec as one Forgejo Actions workflow, `<name>.yml`. */
30
+ export function generateForgejoOpWavesPipeline(spec, options, dialectOptions = {}) {
31
+ assertForgejoWaves(spec);
32
+ const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
33
+ const dropped = spec.waves.filter((w) => w.environment).map((w) => `${w.name} -> ${w.environment.name}`);
34
+ const header = dropped.length > 0 ? droppedWaveEnvironmentsHeader(dropped) : undefined;
35
+ const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(doc, dialectOptions, header)) }];
36
+ if (resumeDoc) {
37
+ // Forgejo has no API to re-run a run, so resume dispatches the workflow
38
+ // again, with a token that may.
39
+ const resume = forgejoWorkflowDoc(resumeDoc, dialectOptions);
40
+ for (const job of Object.values(resume.jobsDoc)) {
41
+ for (const step of job.steps)
42
+ if (step.env)
43
+ step.env = { ...FORGE_TOKEN_ENV };
44
+ }
45
+ files.push({ name: `${spec.name}-resume.yml`, yaml: emitOpPipelineYAML(resume) });
46
+ }
47
+ if (plansDoc)
48
+ files.push({ name: `${spec.name}-plans.yml`, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(plansDoc, dialectOptions)) });
49
+ return { files, jobs };
50
+ }
51
+ /** Refuse what Forgejo cannot carry (#3914): a runner group, and a job group's environment. */
52
+ function assertForgejoWaves(spec) {
53
+ const subject = `Op waves "${spec.name}"`;
54
+ assertRunnerLabels(subject, "runner", spec.runner, "Forgejo");
55
+ for (const wave of spec.waves) {
56
+ assertRunnerLabels(subject, `wave "${wave.name}" runner`, wave.runner, "Forgejo");
57
+ for (const group of wave.jobs ?? []) {
58
+ const field = `wave "${wave.name}" job group "${group.name}"`;
59
+ assertRunnerLabels(subject, `${field} runner`, group.runner, "Forgejo");
60
+ if (group.environment) {
61
+ throw new Error(`${subject}: ${field} names the environment "${group.environment.name}", but Forgejo Actions has no environments, ` +
62
+ "so nothing on the forge would hold the job or scope its secrets. Drop it; the group still renders as its own job with its own runner and variables.");
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Generate mode — component → Forgejo Actions workflow YAML (#969, epic #885).
3
+ *
4
+ * The forgejo counterpart to the github generator
5
+ * (`@intentius/chant-lexicon-github/components/generate-pipeline`, #891). Forgejo
6
+ * Actions is a GitHub-Actions dialect, and this lexicon is already a thin
7
+ * github-dialect serializer, so the pipeline SHAPE is identical: one job per
8
+ * component, `needs:` mirroring `dependsOn`, cross-stack outputs threaded as
9
+ * workflow artifacts, thin trigger jobs. Rather than re-derive the component
10
+ * graph, this reuses github's `buildGithubPipelineDoc` to build the exact same
11
+ * `on`/`env`/`jobs` structure, then applies the Forgejo dialect
12
+ * ({@link transformWorkflowObject}) before emitting — the same transform the
13
+ * forgejo serializer applies to authored workflows:
14
+ *
15
+ * - runner labels: `runs-on: ubuntu-latest` → the project's Forgejo label
16
+ * (default `docker`, the label a fresh `act_runner` exposes);
17
+ * - `uses:` refs (`actions/checkout@v4`, `actions/upload-artifact@v4`, …)
18
+ * rewritten to a Forgejo-resolvable form;
19
+ * - keys the Forgejo runner ignores (`continue-on-error`, and `permissions` bar `id-token`)
20
+ * dropped (`permissions` keeps only `id-token`, which OIDC needs).
21
+ *
22
+ * The output path convention is `.forgejo/workflows/` (vs github's
23
+ * `.github/workflows/`), set by the caller's `-o` flag — this module emits the
24
+ * workflow content, not its location, exactly like the github generator.
25
+ */
26
+ import { buildGithubPipelineDoc, emitPipelineYAML, } from "@intentius/chant-lexicon-github/components/generate-pipeline";
27
+ import { forgejoize } from "./ci-jobs.js";
28
+ /**
29
+ * Synthesize a `.forgejo/workflows/*.yml` pipeline from a set of components.
30
+ * Reuses github's job/`needs:`/artifact structure ({@link buildGithubPipelineDoc}),
31
+ * then applies the Forgejo dialect. Wired into core's generate mode via the
32
+ * forgejo lexicon plugin's `generateComponentPipeline` (../plugin.ts).
33
+ */
34
+ export function generateForgejoPipeline(components, options = {}, dialectOptions = {}) {
35
+ // The pull-request loop (#3183) talks to the forge, so its commands name Forgejo.
36
+ const doc = buildGithubPipelineDoc(components, options.prLoop ? { ...options, prLoop: { ...options.prLoop, forge: "forgejo" } } : options);
37
+ const forgejoDoc = {
38
+ // The environment identity (#2046) is dialect-neutral: name, environment,
39
+ // and the env-map's CHANT_ENV entry pass through untransformed.
40
+ name: doc.name,
41
+ environment: doc.environment,
42
+ on: forgejoize(doc.on, dialectOptions),
43
+ ...(doc.env ? { env: forgejoize(doc.env, dialectOptions) } : {}),
44
+ // A workspace member's working directory (#2542) reads the same on Forgejo.
45
+ ...(doc.defaults ? { defaults: doc.defaults } : {}),
46
+ jobsDoc: forgejoize(doc.jobsDoc, dialectOptions),
47
+ stages: doc.stages,
48
+ jobs: doc.jobs,
49
+ };
50
+ return { yaml: emitPipelineYAML(forgejoDoc), stages: doc.stages, jobs: doc.jobs, env: doc.environment };
51
+ }
package/dist/config.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The `forgejo` namespace in `chant.config.ts` (#1344).
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-forgejo"; // brings the `forgejo` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["forgejo"],
10
+ * forgejo: {
11
+ * runnerLabels: { "ubuntu-latest": "docker" },
12
+ * actionsRoot: "https://code.forgejo.org",
13
+ * },
14
+ * } satisfies ChantConfig;
15
+ * ```
16
+ *
17
+ * One schema, three consumers: core validates the namespace against it at load,
18
+ * the type below is derived from it, and that derived type is what augments
19
+ * `ChantConfig` — so an unknown key fails at build time and at compile time for
20
+ * the same reason, rather than being silently ignored at both.
21
+ */
22
+ import { z } from "zod";
23
+ /**
24
+ * `strictObject`, not `object`. Core applies `.strict()` to the top level of a
25
+ * declared namespace itself, but nested objects are the lexicon's own to make
26
+ * strict — and `runnerLabels` is exactly where a typo would otherwise land.
27
+ */
28
+ export const forgejoConfigSchema = z.strictObject({
29
+ /**
30
+ * GitHub-hosted runner label → Forgejo label. A label with no mapping falls
31
+ * back to the default Forgejo label and is flagged by WFJ011.
32
+ */
33
+ runnerLabels: z.record(z.string(), z.string()).optional(),
34
+ /** Root the common `actions/*` refs are rewritten under. */
35
+ actionsRoot: z.string().optional(),
36
+ });