@intentius/chant-lexicon-forgejo 0.124.31 → 0.124.33
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/dist/actions.js +96 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +99 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +31 -0
- package/dist/codegen/package-cli.js +12 -0
- package/dist/codegen/package.js +42 -0
- package/dist/components/ci-jobs.js +71 -0
- package/dist/components/generate-op-pipeline.js +116 -0
- package/dist/components/generate-op-waves-pipeline.js +66 -0
- package/dist/components/generate-pipeline.js +51 -0
- package/dist/config.js +36 -0
- package/dist/dialect.js +198 -0
- package/dist/index.js +24 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +9 -0
- package/dist/lint/post-synth/index.js +6 -0
- package/dist/lint/post-synth/wfj010.js +34 -0
- package/dist/lint/post-synth/wfj011.js +36 -0
- package/dist/lint/rules/delegate-to-github.js +30 -0
- package/dist/lint/rules/index.js +5 -0
- package/dist/lsp/completions.js +13 -0
- package/dist/lsp/hover.js +11 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +243 -0
- package/dist/migrate/from-github/index.js +63 -0
- package/dist/migrate/from-github/security.js +140 -0
- package/dist/plugin.js +206 -0
- package/dist/serializer.js +50 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.forgejo.golden.yml +6 -2
- package/src/components/__fixtures__/pr-loop-record.forgejo.golden.yml +64 -0
- package/src/components/__fixtures__/pr-loop-waves.forgejo.golden.yml +117 -0
- package/src/components/__fixtures__/pr-loop.forgejo.golden.yml +6 -2
- package/src/components/generate-pr-pipeline.test.ts +41 -7
- package/src/components/generate-pr-waves-pipeline.test.ts +42 -0
package/dist/actions.js
ADDED
|
@@ -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,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
|
+
});
|