@intentius/chant-lexicon-forgejo 0.124.31 → 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.
- 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-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 +18 -1
- package/src/components/generate-pr-waves-pipeline.test.ts +42 -0
package/dist/dialect.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo dialect transform.
|
|
3
|
+
*
|
|
4
|
+
* Forgejo Actions (the engine behind Codeberg, self-hosted Forgejo, and Gitea)
|
|
5
|
+
* runs GitHub-Actions-compatible YAML, so the github lexicon's serializer emits
|
|
6
|
+
* the right shape. The dialect differs in three small ways, all handled here as
|
|
7
|
+
* a pre-pass over the resolved entity graph before the github serializer runs:
|
|
8
|
+
*
|
|
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 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.
|
|
17
|
+
* 2. GitHub-hosted runner labels (`ubuntu-latest`, …) have no fixed meaning on
|
|
18
|
+
* Forgejo — we map them to a default Forgejo label, overridable per project.
|
|
19
|
+
* 3. Anything we can't place (an unmapped runner label) passes through with a
|
|
20
|
+
* warning rather than being dropped.
|
|
21
|
+
*
|
|
22
|
+
* Operating on the entity graph (rather than string-munging YAML) keeps the
|
|
23
|
+
* transform faithful: the github serializer still does the actual emission.
|
|
24
|
+
*/
|
|
25
|
+
// chant#2444 — core's `isDeclarable`, not a local copy. This file carried its
|
|
26
|
+
// own, testing only that the marker was PRESENT where core also requires it to
|
|
27
|
+
// be `true`, so the two disagreed about a value carrying `false`. Core's also
|
|
28
|
+
// accepts an entity a host built, which a local identity check cannot.
|
|
29
|
+
import { isDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
|
|
30
|
+
import { resolveActionRef } from "./actions.js";
|
|
31
|
+
/**
|
|
32
|
+
* Keys the Forgejo runner ignores. Emitting them is misleading (they look
|
|
33
|
+
* enforced but aren't), so the dialect drops them. Compared in kebab-case so
|
|
34
|
+
* both `continueOnError` and `continue-on-error` spellings are caught.
|
|
35
|
+
* `permissions` is handled apart, by {@link reducePermissions}.
|
|
36
|
+
*/
|
|
37
|
+
const DROPPED_KEYS = new Set(["continue-on-error"]);
|
|
38
|
+
/** The one `permissions` scope kept: a job needs it to request an OIDC token. */
|
|
39
|
+
const KEPT_PERMISSION = "id-token";
|
|
40
|
+
const PERMISSION_LEVELS = new Set(["read", "write", "none"]);
|
|
41
|
+
/**
|
|
42
|
+
* True for a GitHub `permissions` value: `read-all`/`write-all`, or a map of
|
|
43
|
+
* scope to `read`/`write`/`none`. Anything else (an action's `with: {
|
|
44
|
+
* permissions: ... }` input, say) is not one and passes through untouched.
|
|
45
|
+
*/
|
|
46
|
+
function isPermissionsValue(value) {
|
|
47
|
+
if (typeof value === "string")
|
|
48
|
+
return value === "read-all" || value === "write-all";
|
|
49
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
50
|
+
return false;
|
|
51
|
+
const entries = Object.values(value);
|
|
52
|
+
return entries.every((v) => typeof v === "string" && PERMISSION_LEVELS.has(v));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reduce a `permissions` value to what Forgejo can use: the `id-token` scope,
|
|
56
|
+
* or `undefined` when there is none (the caller then omits the key).
|
|
57
|
+
*/
|
|
58
|
+
function reducePermissions(value) {
|
|
59
|
+
if (value === null || typeof value !== "object")
|
|
60
|
+
return undefined;
|
|
61
|
+
const scope = value[KEPT_PERMISSION];
|
|
62
|
+
return scope === undefined ? undefined : { [KEPT_PERMISSION]: scope };
|
|
63
|
+
}
|
|
64
|
+
/** Property key whose value is a runner-label selector. */
|
|
65
|
+
const RUNS_ON_KEY = "runs-on";
|
|
66
|
+
/** Property key whose value is an action/workflow reference. */
|
|
67
|
+
const USES_KEY = "uses";
|
|
68
|
+
/**
|
|
69
|
+
* Default GitHub-hosted runner label → Forgejo label mapping. `docker` is the
|
|
70
|
+
* label a freshly-registered Forgejo `act_runner` exposes, and the one used
|
|
71
|
+
* throughout the Forgejo Actions docs, so it is the safest default target.
|
|
72
|
+
* Override per project via `forgejo.runnerLabels` in `chant.config.ts`.
|
|
73
|
+
*/
|
|
74
|
+
export const DEFAULT_RUNNER_LABELS = {
|
|
75
|
+
"ubuntu-latest": "docker",
|
|
76
|
+
"ubuntu-24.04": "docker",
|
|
77
|
+
"ubuntu-22.04": "docker",
|
|
78
|
+
"ubuntu-20.04": "docker",
|
|
79
|
+
};
|
|
80
|
+
/** Convert a camelCase or kebab-case key to a canonical kebab-case form. */
|
|
81
|
+
function toKebabCase(name) {
|
|
82
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Map a single runner label, passing it through unchanged when unmapped. An
|
|
86
|
+
* unmapped label that survives into the output is reported by the WFJ011
|
|
87
|
+
* post-synth check (which surfaces in both `chant build` and `chant lint`),
|
|
88
|
+
* so the dialect doesn't also warn here — that would double-report.
|
|
89
|
+
*/
|
|
90
|
+
function mapLabel(label, ctx) {
|
|
91
|
+
return ctx.labels[label] ?? label;
|
|
92
|
+
}
|
|
93
|
+
/** Remap a `runs-on` value (string or string[]); leave other shapes untouched. */
|
|
94
|
+
function remapRunsOn(value, ctx) {
|
|
95
|
+
if (typeof value === "string")
|
|
96
|
+
return mapLabel(value, ctx);
|
|
97
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
98
|
+
return value.map((v) => mapLabel(v, ctx));
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function transformValue(value, ctx) {
|
|
103
|
+
if (value === null || typeof value !== "object")
|
|
104
|
+
return value;
|
|
105
|
+
if (isDeclarable(value))
|
|
106
|
+
return cloneDeclarable(value, ctx);
|
|
107
|
+
if (Array.isArray(value))
|
|
108
|
+
return value.map((v) => transformValue(v, ctx));
|
|
109
|
+
// Plain object: drop ignored keys, remap runs-on, recurse into the rest.
|
|
110
|
+
const result = {};
|
|
111
|
+
for (const [key, v] of Object.entries(value)) {
|
|
112
|
+
const kebab = toKebabCase(key);
|
|
113
|
+
if (DROPPED_KEYS.has(kebab)) {
|
|
114
|
+
ctx.warnings.push(`forgejo: dropped '${kebab}' in ${ctx.where} — the Forgejo runner ignores it. ` +
|
|
115
|
+
`Re-establish this control through your Forgejo/runner configuration.`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (kebab === "permissions" && isPermissionsValue(v)) {
|
|
119
|
+
const kept = reducePermissions(v);
|
|
120
|
+
if (kept)
|
|
121
|
+
result[key] = kept;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (kebab === RUNS_ON_KEY) {
|
|
125
|
+
result[key] = remapRunsOn(v, ctx);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (kebab === USES_KEY && typeof v === "string") {
|
|
129
|
+
// Rewrite to a Forgejo-resolvable form. An unresolved ref that survives
|
|
130
|
+
// is reported by the WFJ010 post-synth check (build + lint), so the
|
|
131
|
+
// dialect doesn't also warn here — that would double-report.
|
|
132
|
+
result[key] = resolveActionRef(v, { actionsRoot: ctx.actionsRoot }).rewritten;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
result[key] = transformValue(v, ctx);
|
|
136
|
+
}
|
|
137
|
+
dropV4OnlyArtifactInputs(result);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The inputs only `upload-artifact` v4 has, which a step moved to v3
|
|
142
|
+
* (`FORGEJO_ARTIFACT_VERSION`, #4008) loses. v3 always uploads hidden files,
|
|
143
|
+
* so dropping `include-hidden-files` keeps what the step uploads; and a v3
|
|
144
|
+
* artifact is not immutable, so a second upload under one name needs no
|
|
145
|
+
* `overwrite`.
|
|
146
|
+
*/
|
|
147
|
+
const V4_ONLY_UPLOAD_INPUTS = new Set(["include-hidden-files", "overwrite"]);
|
|
148
|
+
/** Drop {@link V4_ONLY_UPLOAD_INPUTS} from a step that uses `upload-artifact@v3`. */
|
|
149
|
+
function dropV4OnlyArtifactInputs(step) {
|
|
150
|
+
const uses = step.uses;
|
|
151
|
+
const withs = step.with;
|
|
152
|
+
if (typeof uses !== "string" || !/\/actions\/upload-artifact@v3$/.test(uses))
|
|
153
|
+
return;
|
|
154
|
+
if (withs === null || typeof withs !== "object" || Array.isArray(withs))
|
|
155
|
+
return;
|
|
156
|
+
const kept = Object.fromEntries(Object.entries(withs).filter(([k]) => !V4_ONLY_UPLOAD_INPUTS.has(toKebabCase(k))));
|
|
157
|
+
step.with = kept;
|
|
158
|
+
}
|
|
159
|
+
/** Shallow-clone a Declarable, replacing its `props` with a transformed copy. */
|
|
160
|
+
function cloneDeclarable(entity, ctx) {
|
|
161
|
+
const descriptors = Object.getOwnPropertyDescriptors(entity);
|
|
162
|
+
const clone = Object.create(Object.getPrototypeOf(entity), descriptors);
|
|
163
|
+
const rawProps = isResourceDeclarable(entity) ? entity.props : undefined;
|
|
164
|
+
const newProps = transformValue(rawProps, ctx);
|
|
165
|
+
Object.defineProperty(clone, "props", {
|
|
166
|
+
value: newProps,
|
|
167
|
+
enumerable: descriptors.props?.enumerable ?? false,
|
|
168
|
+
configurable: true,
|
|
169
|
+
writable: descriptors.props?.writable ?? true,
|
|
170
|
+
});
|
|
171
|
+
return clone;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Apply the Forgejo dialect to a plain object — e.g. a parsed GitHub Actions
|
|
175
|
+
* workflow during migration, rather than the resolved entity graph. Same
|
|
176
|
+
* rules as {@link applyForgejoDialect}: drop ignored keys, remap runner
|
|
177
|
+
* labels, resolve `uses:` refs.
|
|
178
|
+
*/
|
|
179
|
+
export function transformWorkflowObject(obj, options = {}) {
|
|
180
|
+
const labels = { ...DEFAULT_RUNNER_LABELS, ...(options.runnerLabels ?? {}) };
|
|
181
|
+
const warnings = [];
|
|
182
|
+
const ctx = { labels, actionsRoot: options.actionsRoot, warnings, where: "workflow" };
|
|
183
|
+
return { value: transformValue(obj, ctx), warnings };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Apply the Forgejo dialect to a resolved entity map. Returns transformed
|
|
187
|
+
* clones plus the diagnostics produced (dropped keys, unmapped labels).
|
|
188
|
+
*/
|
|
189
|
+
export function applyForgejoDialect(entities, options = {}) {
|
|
190
|
+
const labels = { ...DEFAULT_RUNNER_LABELS, ...(options.runnerLabels ?? {}) };
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const out = new Map();
|
|
193
|
+
for (const [name, entity] of entities) {
|
|
194
|
+
const ctx = { labels, actionsRoot: options.actionsRoot, warnings, where: name };
|
|
195
|
+
out.set(name, cloneDeclarable(entity, ctx));
|
|
196
|
+
}
|
|
197
|
+
return { entities: out, warnings };
|
|
198
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Forgejo Actions lexicon — a thin dialect of the github lexicon.
|
|
2
|
+
//
|
|
3
|
+
// Serializer + plugin override only the dialect; everything a user writes
|
|
4
|
+
// (entities, expression helpers, context variables, composites) is reused
|
|
5
|
+
// directly from the github lexicon and re-exported here so a forgejo project
|
|
6
|
+
// imports solely from "@intentius/chant-lexicon-forgejo".
|
|
7
|
+
// Serializer
|
|
8
|
+
export { forgejoSerializer } from "./serializer.js";
|
|
9
|
+
// Plugin
|
|
10
|
+
export { forgejoPlugin } from "./plugin.js";
|
|
11
|
+
// Dialect (exposed for tooling/tests)
|
|
12
|
+
export { applyForgejoDialect, transformWorkflowObject, DEFAULT_RUNNER_LABELS, } from "./dialect.js";
|
|
13
|
+
// github → forgejo migration
|
|
14
|
+
export { transform, detectGitHubWorkflow, } from "./migrate/from-github/index.js";
|
|
15
|
+
export { analyzeForgejoSecurity, renderSecurityPosture, } from "./migrate/from-github/security.js";
|
|
16
|
+
// Action-reference resolver (exposed for tooling/tests)
|
|
17
|
+
export { resolveActionRef, DEFAULT_ACTIONS_ROOT, KNOWN_ACTIONS, } from "./actions.js";
|
|
18
|
+
// Reuse the entire github authoring surface: generated entities, expression
|
|
19
|
+
// system, context variables, and composites.
|
|
20
|
+
export * from "@intentius/chant-lexicon-github";
|
|
21
|
+
// The `forgejo` chant.config namespace (#1344). Exported as a type and, by
|
|
22
|
+
// virtue of this module being part of the program, as the `ChantConfig`
|
|
23
|
+
// augmentation that makes `forgejo: { … }` typecheck in a project config.
|
|
24
|
+
export { forgejoConfigSchema } from "./config.js";
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "7885f3eea1327f2431dd65676726257cf9bd60253edeaa96bc9c1f9e18b2335f",
|
|
5
5
|
"meta.json": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
|
6
6
|
"types/index.d.ts": "66204549b2a864ab5489e3f02c1878e73ab4b3c9d938cfb480b66f71543a43de",
|
|
7
7
|
"rules/delegate-to-github.ts": "1060cda40f4b73d6cca3ba3fa13b80b4886147d4034c88584c2d80c5510dbe5e",
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
"rules/wfj011.ts": "fcb8bf6685d744af20ad804ce929488257995c74b9a3353b2eacae3768d1c83e",
|
|
10
10
|
"skills/chant-forgejo.md": "f28d7fb69fb1cfd992496ef41fda0ea5ccd54a29b84ea1c501a44f6a3992c4d5"
|
|
11
11
|
},
|
|
12
|
-
"composite": "
|
|
12
|
+
"composite": "dcc44816420a6008af0661e553d724c0a9242ba2cea6720b9c63bf44bf7ce962"
|
|
13
13
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forgejo lexicon's chant audit catalog — metadata for its post-synth rules
|
|
3
|
+
* (WFJ Forgejo rules). Contributed via forgejoPlugin.auditCatalog() (#687).
|
|
4
|
+
*/
|
|
5
|
+
import { auditRule } from "@intentius/chant/audit/catalog";
|
|
6
|
+
export const forgejoAuditCatalog = {
|
|
7
|
+
WFJ010: auditRule("WFJ010", "merge-worthy", "guidance", "Unresolved action reference on Forgejo", "Use an action reference Forgejo can resolve (full URL or a mirrored action).", { category: "correctness" }),
|
|
8
|
+
WFJ011: auditRule("WFJ011", "merge-worthy", "guidance", "GitHub-hosted runner label with no Forgejo equivalent", "Use a runner label your Forgejo instance provides.", { category: "correctness" }),
|
|
9
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WFJ010: Unresolved action reference.
|
|
3
|
+
*
|
|
4
|
+
* After the forgejo dialect runs, every `uses:` should be a Forgejo-resolvable
|
|
5
|
+
* form (a full URL, a local `./` action, or `docker://`). A bare `owner/repo@ref`
|
|
6
|
+
* that survives into the output won't resolve — Forgejo has no GitHub
|
|
7
|
+
* Marketplace. Flags it so it's caught in `chant lint` / `forgejo:checks`, not
|
|
8
|
+
* only as a build-time warning.
|
|
9
|
+
*/
|
|
10
|
+
import { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { extractActionRefs } from "@intentius/chant-lexicon-github/lint/post-synth/yaml-helpers";
|
|
12
|
+
import { resolveActionRef } from "../../actions.js";
|
|
13
|
+
export const wfj010 = {
|
|
14
|
+
id: "WFJ010",
|
|
15
|
+
description: "Unresolved action reference (won't resolve on Forgejo)",
|
|
16
|
+
check(ctx) {
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const yaml = getPrimaryOutput(output);
|
|
20
|
+
for (const ref of extractActionRefs(yaml)) {
|
|
21
|
+
if (resolveActionRef(ref.ref).warning) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "WFJ010",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `Job "${ref.job}" references '${ref.ref}', which has no Forgejo-resolvable form. Use a full repository URL or map it under forgejo.actionsRoot.`,
|
|
26
|
+
entity: ref.job,
|
|
27
|
+
lexicon: "forgejo",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return diagnostics;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WFJ011: GitHub-hosted runner label.
|
|
3
|
+
*
|
|
4
|
+
* `runs-on` labels like `macos-latest` / `windows-latest` name GitHub-hosted
|
|
5
|
+
* runners that don't exist on Forgejo, and `ubuntu-*` labels that the dialect
|
|
6
|
+
* didn't map (e.g. when mapping is overridden away) won't be advertised by a
|
|
7
|
+
* default Forgejo runner. Flags any such label that survives into the output.
|
|
8
|
+
*/
|
|
9
|
+
import { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { extractRunsOnByJob } from "@intentius/chant-lexicon-github/lint/post-synth/yaml-helpers";
|
|
11
|
+
/** Labels that name a GitHub-hosted runner image (no fixed Forgejo equivalent). */
|
|
12
|
+
const GITHUB_HOSTED = /^(ubuntu|macos|windows)-/;
|
|
13
|
+
export const wfj011 = {
|
|
14
|
+
id: "WFJ011",
|
|
15
|
+
description: "GitHub-hosted runner label with no Forgejo equivalent",
|
|
16
|
+
check(ctx) {
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const yaml = getPrimaryOutput(output);
|
|
20
|
+
for (const [job, labels] of extractRunsOnByJob(yaml)) {
|
|
21
|
+
for (const label of labels) {
|
|
22
|
+
if (GITHUB_HOSTED.test(label)) {
|
|
23
|
+
diagnostics.push({
|
|
24
|
+
checkId: "WFJ011",
|
|
25
|
+
severity: "warning",
|
|
26
|
+
message: `Job "${job}" runs on '${label}', a GitHub-hosted runner label with no Forgejo equivalent. Map it via forgejo.runnerLabels or target a label your Forgejo runners advertise.`,
|
|
27
|
+
entity: job,
|
|
28
|
+
lexicon: "forgejo",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return diagnostics;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo lint rules — wrapped from github, not duplicated.
|
|
3
|
+
*
|
|
4
|
+
* Forgejo has no lint rules of its own: entities, composites, and every
|
|
5
|
+
* expression helper a forgejo project imports are the exact same github
|
|
6
|
+
* classes (see src/index.ts's `export * from "@intentius/chant-lexicon-github"`).
|
|
7
|
+
* A forgejo workflow is TypeScript indistinguishable in shape from an
|
|
8
|
+
* equivalent github workflow, so github's lint rules (GHA001-GHA058 — typed
|
|
9
|
+
* composites, secret detection, supply-chain checks, etc.) apply to it
|
|
10
|
+
* unmodified. Re-implementing or forking any of them here would drift from
|
|
11
|
+
* github's the first time either side changed.
|
|
12
|
+
*
|
|
13
|
+
* Each rule is wrapped rather than re-exported verbatim under github's own
|
|
14
|
+
* id: `chant lint`/`chant audit` can load the github and forgejo plugins in
|
|
15
|
+
* the same process (e.g. `chant audit` scans a repo without knowing its CI
|
|
16
|
+
* provider up front — see packages/core/src/audit/core.ts's
|
|
17
|
+
* `defaultChecksProvider`, which already loads forgejo+github together for
|
|
18
|
+
* post-synth checks), and `loadPlugins`' cross-lexicon conflict check
|
|
19
|
+
* (packages/core/src/cli/conflict-check.ts) hard-fails on two plugins
|
|
20
|
+
* claiming the same rule id. Prefixing with `WFJ-` — forgejo's existing
|
|
21
|
+
* post-synth namespace — keeps every id globally unique while leaving the
|
|
22
|
+
* check (and fix, where one exists) exactly as github wrote it; only the
|
|
23
|
+
* `id` field changes.
|
|
24
|
+
*/
|
|
25
|
+
import { githubPlugin } from "@intentius/chant-lexicon-github";
|
|
26
|
+
/** Wrap a github lint rule under forgejo's `WFJ-` namespace, unmodified otherwise. */
|
|
27
|
+
export function wrapGithubRule(rule) {
|
|
28
|
+
return { ...rule, id: `WFJ-${rule.id}` };
|
|
29
|
+
}
|
|
30
|
+
export const forgejoLintRules = (githubPlugin.lintRules?.() ?? []).map(wrapGithubRule);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo LSP completions — delegated to github, not duplicated.
|
|
3
|
+
*
|
|
4
|
+
* Forgejo reuses github's entire authoring surface (entities, composites) —
|
|
5
|
+
* there is no forgejo-specific resource catalog to index. This forwards
|
|
6
|
+
* straight to `githubPlugin`'s own completion provider, which already reads
|
|
7
|
+
* github's generated entity catalog; forking a second copy of it here would
|
|
8
|
+
* only drift the moment github's catalog regenerates.
|
|
9
|
+
*/
|
|
10
|
+
import { githubPlugin } from "@intentius/chant-lexicon-github";
|
|
11
|
+
export function forgejoCompletions(ctx) {
|
|
12
|
+
return githubPlugin.completionProvider?.(ctx) ?? [];
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo LSP hover — delegated to github, not duplicated.
|
|
3
|
+
*
|
|
4
|
+
* Same reasoning as ./completions.ts: forgejo authors against github's
|
|
5
|
+
* exact classes, so hover information for them is github's hover
|
|
6
|
+
* information, unmodified.
|
|
7
|
+
*/
|
|
8
|
+
import { githubPlugin } from "@intentius/chant-lexicon-github";
|
|
9
|
+
export function forgejoHover(ctx) {
|
|
10
|
+
return githubPlugin.hoverProvider?.(ctx);
|
|
11
|
+
}
|
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only MCP tools for the forgejo lexicon.
|
|
3
|
+
*
|
|
4
|
+
* The forgejo counterpart to the github/gitlab context tools (#327): expose
|
|
5
|
+
* what `chant build` already computes about a Forgejo workflow — its triggers
|
|
6
|
+
* and jobs, what it pulls in from outside, its findings, where each job came
|
|
7
|
+
* from in source — plus `forgejo:compare`, the migration safety view. Because
|
|
8
|
+
* Forgejo emits GitHub-style YAML, the parsing reuses the github lexicon's
|
|
9
|
+
* yaml-helpers.
|
|
10
|
+
*
|
|
11
|
+
* Every tool builds from source and returns data. None touch a live forge,
|
|
12
|
+
* read run history, or write anything.
|
|
13
|
+
*/
|
|
14
|
+
import { build } from "@intentius/chant/build";
|
|
15
|
+
import { runPostSynthChecks, getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { postSynthChecks } from "../lint/post-synth/index.js";
|
|
17
|
+
import { getProvenance } from "@intentius/chant/provenance";
|
|
18
|
+
import { relative, isAbsolute, resolve } from "path";
|
|
19
|
+
import { readFile } from "fs/promises";
|
|
20
|
+
import { extractJobs, extractActionRefs, extractImageRefs, extractTriggers, extractWorkflowName, } from "@intentius/chant-lexicon-github/lint/post-synth/yaml-helpers";
|
|
21
|
+
import { forgejoSerializer } from "../serializer.js";
|
|
22
|
+
import { transform, detectGitHubWorkflow } from "../migrate/from-github/index.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build the project with the forgejo serializer and return the emitted YAML.
|
|
25
|
+
* The serializer is registered under the "github" lexicon (it serializes the
|
|
26
|
+
* reused github entities), so the output is keyed "github".
|
|
27
|
+
*/
|
|
28
|
+
async function buildForgejo(path) {
|
|
29
|
+
const result = await build(path, [forgejoSerializer]);
|
|
30
|
+
const out = result.outputs.get("github");
|
|
31
|
+
return { yaml: out ? getPrimaryOutput(out) : "", result };
|
|
32
|
+
}
|
|
33
|
+
/** Discover the forgejo post-synth checks without depending on the plugin. */
|
|
34
|
+
function forgejoPostSynthChecks() {
|
|
35
|
+
return postSynthChecks;
|
|
36
|
+
}
|
|
37
|
+
/** Is a `uses:` ref pinned to a full commit SHA (the only immutable form)? */
|
|
38
|
+
export function actionPinned(ref) {
|
|
39
|
+
const at = ref.lastIndexOf("@");
|
|
40
|
+
return at !== -1 && /^[0-9a-f]{40}$/.test(ref.slice(at + 1));
|
|
41
|
+
}
|
|
42
|
+
/** The YAML job name a build entity serializes to (camelCase → kebab-case). */
|
|
43
|
+
export function jobNameOf(entityName) {
|
|
44
|
+
return entityName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
/** Find the build entity whose serialized job name matches `job`. */
|
|
47
|
+
function entityForJob(result, job) {
|
|
48
|
+
for (const [name, entity] of result.entities) {
|
|
49
|
+
if (name === job || jobNameOf(name) === job)
|
|
50
|
+
return { name, entity };
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Render a source-file path relative to the project root, when possible. */
|
|
55
|
+
function relSource(root, file) {
|
|
56
|
+
if (!file)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!isAbsolute(file))
|
|
59
|
+
return file;
|
|
60
|
+
const rel = relative(root, file);
|
|
61
|
+
return rel.startsWith("..") ? file : rel;
|
|
62
|
+
}
|
|
63
|
+
/** Jobs that would re-run because they depend (transitively) on `job`. */
|
|
64
|
+
export function downstreamJobs(yaml, job) {
|
|
65
|
+
const jobs = extractJobs(yaml);
|
|
66
|
+
const downstreamOf = new Map();
|
|
67
|
+
for (const [name, j] of jobs) {
|
|
68
|
+
for (const dep of j.needs ?? []) {
|
|
69
|
+
const arr = downstreamOf.get(dep) ?? [];
|
|
70
|
+
arr.push(name);
|
|
71
|
+
downstreamOf.set(dep, arr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const out = new Set();
|
|
75
|
+
const queue = [job];
|
|
76
|
+
while (queue.length) {
|
|
77
|
+
const cur = queue.shift();
|
|
78
|
+
for (const next of downstreamOf.get(cur) ?? []) {
|
|
79
|
+
if (!out.has(next)) {
|
|
80
|
+
out.add(next);
|
|
81
|
+
queue.push(next);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...out];
|
|
86
|
+
}
|
|
87
|
+
const PATH_INPUT = {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const JOB_INPUT = {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
97
|
+
job: { type: "string", description: "Job name (as it appears in the generated YAML)" },
|
|
98
|
+
},
|
|
99
|
+
required: ["job"],
|
|
100
|
+
};
|
|
101
|
+
/** The read-only context tools for the forgejo lexicon. */
|
|
102
|
+
export function forgejoContextTools() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
name: "forgejo:checks",
|
|
106
|
+
description: "Build the workflow and return its Forgejo-specific findings (the WFJ checks: unresolved action refs, GitHub-hosted runner labels) as JSON. Read-only.",
|
|
107
|
+
inputSchema: PATH_INPUT,
|
|
108
|
+
async handler(params) {
|
|
109
|
+
const { yaml, result } = await buildForgejo(params.path ?? ".");
|
|
110
|
+
if (!yaml)
|
|
111
|
+
return { findings: [], note: "no Forgejo workflow produced from this project" };
|
|
112
|
+
const scoped = { ...result, outputs: new Map([["github", result.outputs.get("github")]]) };
|
|
113
|
+
const diags = runPostSynthChecks(forgejoPostSynthChecks(), scoped);
|
|
114
|
+
return {
|
|
115
|
+
findings: diags.map((d) => ({ id: d.checkId, severity: d.severity, job: d.entity ?? null, message: d.message })),
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "forgejo:workflow",
|
|
121
|
+
description: "Build the workflow and return its triggers and jobs as written (name, what each job runs after, step count) — before anything runs. Read-only.",
|
|
122
|
+
inputSchema: PATH_INPUT,
|
|
123
|
+
async handler(params) {
|
|
124
|
+
const { yaml } = await buildForgejo(params.path ?? ".");
|
|
125
|
+
const jobs = [...extractJobs(yaml).values()].map((j) => ({
|
|
126
|
+
name: j.name,
|
|
127
|
+
runsAfter: j.needs ?? [],
|
|
128
|
+
steps: j.steps?.length ?? 0,
|
|
129
|
+
}));
|
|
130
|
+
return { workflow: extractWorkflowName(yaml) ?? null, triggers: Object.keys(extractTriggers(yaml)), jobs };
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "forgejo:references",
|
|
135
|
+
description: "Build the workflow and list everything it pulls in from outside (actions via uses:, container/service images) and whether each is pinned to an immutable commit SHA. Read-only.",
|
|
136
|
+
inputSchema: PATH_INPUT,
|
|
137
|
+
async handler(params) {
|
|
138
|
+
const { yaml } = await buildForgejo(params.path ?? ".");
|
|
139
|
+
const actions = extractActionRefs(yaml).map((a) => ({
|
|
140
|
+
kind: a.level === "job" ? "reusable-workflow" : "action",
|
|
141
|
+
job: a.job,
|
|
142
|
+
source: a.ref,
|
|
143
|
+
pinned: actionPinned(a.ref),
|
|
144
|
+
}));
|
|
145
|
+
const images = extractImageRefs(yaml).map((i) => ({
|
|
146
|
+
kind: "image",
|
|
147
|
+
job: i.job,
|
|
148
|
+
source: i.image,
|
|
149
|
+
pinned: i.image.includes("@sha256:"),
|
|
150
|
+
}));
|
|
151
|
+
return [...actions, ...images];
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "forgejo:affected",
|
|
156
|
+
description: "Build the workflow and, given a job name, list the jobs that would re-run because they depend on it (the needs chain). Read-only.",
|
|
157
|
+
inputSchema: JOB_INPUT,
|
|
158
|
+
async handler(params) {
|
|
159
|
+
const { yaml } = await buildForgejo(params.path ?? ".");
|
|
160
|
+
const job = params.job;
|
|
161
|
+
return { job, wouldRerun: downstreamJobs(yaml, job) };
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "forgejo:workflow-yaml",
|
|
166
|
+
description: "Build the project and return the generated Forgejo Actions workflow YAML as a string. Read-only.",
|
|
167
|
+
inputSchema: PATH_INPUT,
|
|
168
|
+
async handler(params) {
|
|
169
|
+
const { yaml } = await buildForgejo(params.path ?? ".");
|
|
170
|
+
return { yaml };
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "forgejo:source",
|
|
175
|
+
description: "Build the project and, given a job name, say where it came from in the TypeScript source — the file that declared it and the composite that expanded it, if any. Entity-level provenance, not a YAML-line source map. Read-only.",
|
|
176
|
+
inputSchema: JOB_INPUT,
|
|
177
|
+
async handler(params) {
|
|
178
|
+
const root = params.path ?? ".";
|
|
179
|
+
const { result } = await buildForgejo(root);
|
|
180
|
+
const job = params.job;
|
|
181
|
+
const found = entityForJob(result, job);
|
|
182
|
+
if (!found)
|
|
183
|
+
return { job, found: false, note: "no build entity serializes to that job name" };
|
|
184
|
+
const prov = getProvenance(found.entity);
|
|
185
|
+
return { job, found: true, entity: found.name, from: relSource(root, prov?.sourceFile) ?? null, via: prov?.composite ?? null };
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "forgejo:owns",
|
|
190
|
+
description: "Build the project and report whether a job is declared (owned) by chant in this project's source. For workflow config, ownership means \"declared here\" — Forgejo Actions jobs are not taggable cloud resources, so there is no live ownership marker as there is for cloud lexicons. Read-only.",
|
|
191
|
+
inputSchema: JOB_INPUT,
|
|
192
|
+
async handler(params) {
|
|
193
|
+
const root = params.path ?? ".";
|
|
194
|
+
const { result } = await buildForgejo(root);
|
|
195
|
+
const job = params.job;
|
|
196
|
+
const found = entityForJob(result, job);
|
|
197
|
+
return {
|
|
198
|
+
job,
|
|
199
|
+
owned: Boolean(found),
|
|
200
|
+
basis: "declared-in-source",
|
|
201
|
+
note: "Forgejo Actions jobs are not taggable cloud resources; ownership here means the job is declared by chant in this project. Live ownership markers apply to cloud lexicons (aws/azure/k8s).",
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "forgejo:compare",
|
|
207
|
+
description: "Given a GitHub Actions workflow file, migrate it to Forgejo and report which properties survive the move and which weaken or are lost (the migration safety view). Returns a per-property fate (translated/approximated/needs-review/lost) plus summary counts. Read-only — analyzes, never writes.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
file: { type: "string", description: "Path to a .github/workflows/*.yml workflow file to migrate and compare" },
|
|
212
|
+
},
|
|
213
|
+
required: ["file"],
|
|
214
|
+
},
|
|
215
|
+
async handler(params) {
|
|
216
|
+
const file = resolve(params.file ?? "");
|
|
217
|
+
let content;
|
|
218
|
+
try {
|
|
219
|
+
content = await readFile(file, "utf8");
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { file: params.file ?? null, found: false, note: "could not read the workflow file" };
|
|
223
|
+
}
|
|
224
|
+
if (!detectGitHubWorkflow(content)) {
|
|
225
|
+
return { file: params.file ?? null, found: false, note: "file does not look like a GitHub Actions workflow" };
|
|
226
|
+
}
|
|
227
|
+
const migration = await transform(content, { security: true, sourceFile: file });
|
|
228
|
+
const properties = migration.provenance.map((r) => ({
|
|
229
|
+
property: r.security.property,
|
|
230
|
+
fate: r.security.fate,
|
|
231
|
+
severity: r.security.severity,
|
|
232
|
+
sourceKey: r.sourceKey,
|
|
233
|
+
reestablish: r.security.reestablish ?? null,
|
|
234
|
+
note: r.note ?? null,
|
|
235
|
+
}));
|
|
236
|
+
const summary = { translated: 0, approximated: 0, "needs-review": 0, lost: 0 };
|
|
237
|
+
for (const p of properties)
|
|
238
|
+
summary[p.fate] = (summary[p.fate] ?? 0) + 1;
|
|
239
|
+
return { found: true, properties, summary };
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|