@intentius/chant-lexicon-forgejo 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,12 +28,20 @@
28
28
  * no mapping in ../actions.ts passes through verbatim and resolves only if
29
29
  * the runner can fetch it.
30
30
  *
31
- * One finding-mode does not cross over: `comment` (#2231) posts onto the
32
- * triggering pull request by shelling to `gh` against the GitHub API and
33
- * reading the GitHub Actions event payload. Forgejo's API is
34
- * GitHub-compatible in shape, but chant has no Forgejo client and no host
35
- * configuration to point `gh` at a Forgejo instance, so this refuses the mode
36
- * by name rather than generating a job whose finding step fails on every run.
31
+ * `comment` (#2231) crosses over too, since chant #2291: it posts onto the
32
+ * triggering pull request by shelling to `gh` against `${GITHUB_API_URL}`,
33
+ * and a Forgejo Actions job already sets that (and `github.token`) the same
34
+ * way a GitHub Actions job does. This generator used to refuse the mode by
35
+ * name here, on the premise that chant had no way to point `gh` at a Forgejo
36
+ * instance; that premise was checked against a real Forgejo
37
+ * 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
38
+ * false — the failure was `gh api` resolving a *relative* path against
39
+ * `/api/v3`, which Forgejo does not serve, not an unreachable forge. See
40
+ * `reconcilePr`'s `postOrUpdateComment` (`packages/core/src/op/activities/reconcile.ts`)
41
+ * for the fix. `issue` mode (`gh issue create`) was not part of that
42
+ * verification — only the comment endpoints were exercised on the real
43
+ * instance — so it remains un-refused-but-unverified here, exactly as it was
44
+ * on GitHub before this change.
37
45
  *
38
46
  * A spec's `environment` (#2257) is dropped on the same terms as
39
47
  * `permissions:`, and for a stronger reason: Forgejo Actions has no
@@ -56,6 +64,24 @@
56
64
  * Forgejo apply that stops at its gate is a green run rather than a red one,
57
65
  * and `chant run` still writes the gate and the approve command to
58
66
  * `GITHUB_STEP_SUMMARY`, which Forgejo Actions sets like GitHub does.
67
+ *
68
+ * The gate-notice job's own reason for being — `outputs: { gated, op, gate,
69
+ * approve }` on the Op's job, and the `node -e` step that writes them to
70
+ * `$GITHUB_OUTPUT` — does not survive it either (#2294). Both existed only for
71
+ * that job to read via `needs.<job>.outputs`; with the job gone, so is every
72
+ * reader, and `buildGithubOpPipelineDocs` is asked for neither
73
+ * (`emitGatedOutputs: false` below) rather than emitting them here and
74
+ * stripping them back out. Should the Forgejo posting work in #2291 grow a
75
+ * notice job of its own, this is the one flag that brings both back.
76
+ *
77
+ * A spec's own `variables` (#2290) crosses over unchanged: forgejo reuses the
78
+ * same job-level `env:` github's builder emits, and the dialect transform
79
+ * touches only `permissions:`/`environment:`/action refs, never a job's `env:`
80
+ * mapping. That is what makes a per-Op credential expressible here at all —
81
+ * `ComponentPipelineOptions.variables` is workflow-scoped and, on Forgejo,
82
+ * Actions mints no OIDC token to put in `setup` instead, so a job-level
83
+ * `variables` entry is the only way one Op's job can hold a credential no
84
+ * sibling Op's job receives.
59
85
  */
60
86
  import type { ComponentPipelineOptions, OpPipelineResult, ScheduledOpSpec } from "@intentius/chant/lexicon";
61
87
  import { type ForgejoDialectOptions } from "../dialect.js";
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAOH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAmCjF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,gBAAgB,CA+BlB"}
1
+ {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AAOH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAmCjF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,gBAAgB,CA2BlB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "56a899fd87d95111c56cb551f5a4961c91af788fb0a11c3ef4f9fb207f5abd3f",
4
+ "manifest.json": "3f69907e1588ef81d9bbf78e57ba60a06e4657ad54692aad9cc172ac21246ce6",
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": "a1a560429db736c187e0b34cf8dd9efc6ad771afefcbf6f802d160d3d5274257"
11
11
  },
12
- "composite": "390ec307dcfc42c1e376d49ee30e4f78429b316ded7bce20355efded9327d04e"
12
+ "composite": "c1b979e353a7bd9fbc449e1f98b765979d89d4e386ed94d8fce9ec20754ee514"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Forgejo",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-forgejo",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "description": "Forgejo / Codeberg / Gitea Actions lexicon for chant — a thin GitHub Actions dialect",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "zod": "^4.3.6",
57
- "@intentius/chant": "^0.60.0",
58
- "@intentius/chant-lexicon-github": "^0.60.0"
57
+ "@intentius/chant": "^0.62.0",
58
+ "@intentius/chant-lexicon-github": "^0.62.0"
59
59
  },
60
60
  "scripts": {
61
61
  "generate": "tsx src/codegen/generate-cli.ts",
@@ -14,9 +14,11 @@ import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
14
14
 
15
15
  interface ParsedJob {
16
16
  "runs-on"?: string;
17
+ container?: string;
17
18
  environment?: Record<string, string>;
18
19
  outputs?: Record<string, string>;
19
- steps: Array<{ id?: string; uses?: string; run?: string }>;
20
+ env?: Record<string, string>;
21
+ steps: Array<{ id?: string; uses?: string; run?: string; shell?: string }>;
20
22
  }
21
23
  interface ParsedDoc {
22
24
  on?: Record<string, unknown>;
@@ -105,27 +107,45 @@ describe("generateForgejoOpPipeline: non-cron trigger survives the dialect trans
105
107
  });
106
108
  });
107
109
 
108
- describe("generateForgejoOpPipeline: no comment finding mode (#2231)", () => {
109
- test("findingMode comment is refused by name, on the pull_request trigger it would otherwise fit", () => {
110
- // Forgejo Actions runs the same workflow shape and Forgejo's API is
111
- // GitHub-compatible, but the activity behind the mode shells to `gh`
112
- // against github.com and reads the GitHub Actions event payload. Nothing
113
- // in chant points either at a Forgejo instance, so the mode is refused
114
- // here rather than generating a job that fails at its Report step.
110
+ describe("generateForgejoOpPipeline: comment finding mode crosses over (#2291)", () => {
111
+ // Used to be refused by name here (#2231), on the premise that chant had no
112
+ // way to point `gh` at a Forgejo instance. Checked against a real Forgejo
113
+ // 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
114
+ // false: Forgejo's own `/api/v1` takes the same GET/POST/PATCH calls
115
+ // `reconcilePr`'s `postOrUpdateComment` makes, once it targets the right
116
+ // URL. See `packages/core/src/op/activities/reconcile.test.ts` for the
117
+ // mocked-transport proof of that fix; this file only proves the generator
118
+ // no longer refuses the mode and produces the same job shape github does.
119
+ test("findingMode comment generates cleanly on the pull_request trigger it needs", () => {
115
120
  const specs: ScheduledOpSpec[] = [
116
121
  { name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
117
122
  ];
118
- expect(() => generateForgejoOpPipeline(specs)).toThrow(
119
- /Scheduled Op "app-plan".*findingMode "comment".*no Forgejo API client/s,
120
- );
123
+ expect(() => generateForgejoOpPipeline(specs)).not.toThrow();
121
124
  });
122
125
 
123
- test("github generates the same spec, so the refusal is forgejo's and not the shared builder's", () => {
126
+ test("job/trigger/findingMode parity with the github generator (same input, same job list)", () => {
124
127
  const specs: ScheduledOpSpec[] = [
125
128
  { name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
126
129
  ];
127
- const gh = parseFile(generateGithubOpPipeline(specs).files[0].yaml);
128
- expect(gh.permissions).toEqual({ contents: "read", "pull-requests": "write" });
130
+ const fj = generateForgejoOpPipeline(specs);
131
+ const gh = generateGithubOpPipeline(specs);
132
+ expect(fj.jobs).toEqual(gh.jobs);
133
+ });
134
+
135
+ test("the run step carries GH_TOKEN/GITHUB_TOKEN, same as github's — permissions: still dropped by the dialect", () => {
136
+ const specs: ScheduledOpSpec[] = [
137
+ { name: "app-plan", trigger: { kind: "pull_request", branches: ["main"] }, findingMode: "comment" },
138
+ ];
139
+ const fjYaml = generateForgejoOpPipeline(specs).files[0].yaml;
140
+ const ghDoc = parseFile(generateGithubOpPipeline(specs).files[0].yaml);
141
+
142
+ expect(fjYaml).toContain("GH_TOKEN: '${{ github.token }}'");
143
+ expect(fjYaml).toContain("GITHUB_TOKEN: '${{ github.token }}'");
144
+ expect(fjYaml).not.toMatch(/^permissions:/m);
145
+ // github still declares the scope the mode needs — proving the omission
146
+ // above is the Forgejo dialect dropping `permissions:` wholesale, not a
147
+ // sign the mode generated with no token.
148
+ expect(ghDoc.permissions).toEqual({ contents: "read", "pull-requests": "write" });
129
149
  });
130
150
  });
131
151
 
@@ -198,6 +218,24 @@ describe("generateForgejoOpPipeline: the gated apply on push (#2243)", () => {
198
218
  expect(Object.keys(fj.jobs ?? {})).toEqual(["app-apply"]);
199
219
  expect(gh.jobs).toHaveProperty("app-apply-gate-notice");
200
220
  });
221
+
222
+ /**
223
+ * chant #2299 — Forgejo Actions runs the same act_runner semantics as
224
+ * GitHub Actions: a job with `container:` (which this generator's Op job
225
+ * always carries, unconditionally) defaults to `sh`, and `sh` rejects `set
226
+ * -o pipefail`. This generator reuses github's `buildGithubOpPipelineDocs`
227
+ * to build the job, then the dialect transform in ../dialect.ts to cross
228
+ * it, so the fix has to survive both: neither step drops or renames
229
+ * `shell:`.
230
+ */
231
+ test("the pipefail step keeps shell: bash across the Forgejo dialect — its job runs in a container, whose default shell is sh", () => {
232
+ const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
233
+ const job = doc.jobs!["app-apply"];
234
+ expect(job.container).toBe("node:22-slim");
235
+ const step = job.steps.find((s) => s.id === "chant-run");
236
+ expect(step?.run).toContain("set -o pipefail");
237
+ expect(step?.shell).toBe("bash");
238
+ });
201
239
  });
202
240
 
203
241
  /**
@@ -277,3 +315,129 @@ describe("generateForgejoOpPipeline: a dropped deployment environment (#2257)",
277
315
  ).toThrow(/neither an absolute http\(s\) URL nor a/);
278
316
  });
279
317
  });
318
+
319
+ /**
320
+ * chant #2290 — per-Op credentials, so a pull-request job need not hold the
321
+ * apply credential. The consuming project's own shape: `live-check` (no
322
+ * `variables`) carries no credential while `live-apply` (`variables` set)
323
+ * carries one, both generated from forge-wide `options.variables` that name
324
+ * no credential at all.
325
+ */
326
+ describe("generateForgejoOpPipeline: per-Op variables land on the job, not the workflow (#2290)", () => {
327
+ const FORGE_WIDE = { CHANT_FORGE: "forgejo", AWS_REGION: "${{ vars.AWS_REGION }}" };
328
+ const CREDENTIAL = { AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" };
329
+
330
+ test("a job whose spec declares no variables carries none, even when the forge-wide options do", () => {
331
+ const doc = parseFile(
332
+ generateForgejoOpPipeline([{ name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } }], {
333
+ variables: FORGE_WIDE,
334
+ }).files[0].yaml,
335
+ );
336
+ expect(doc.jobs!["live-check"].env).toBeUndefined();
337
+ });
338
+
339
+ test("a job whose spec declares variables carries them as the job's own env:, beside the workflow env:", () => {
340
+ const doc = parseFile(
341
+ generateForgejoOpPipeline(
342
+ [{ name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL }],
343
+ { variables: FORGE_WIDE },
344
+ ).files[0].yaml,
345
+ );
346
+ expect(doc.jobs!["live-apply"].env).toEqual(CREDENTIAL);
347
+ });
348
+
349
+ /**
350
+ * The proof the issue names: a generated workflow whose `live-check` job
351
+ * carries no credential while its `live-apply` job does, from one forge-wide
352
+ * options object that itself names no credential — the property the
353
+ * consuming project (`examples/ci-pipelines`) needs to be able to state.
354
+ */
355
+ test("live-check carries no credential while live-apply does, from the same forge-wide options", () => {
356
+ const files = generateForgejoOpPipeline(
357
+ [
358
+ { name: "live-check", trigger: { kind: "pull_request", branches: ["main"] } },
359
+ { name: "live-apply", trigger: { kind: "push", branches: ["main"] }, variables: CREDENTIAL },
360
+ ],
361
+ { variables: FORGE_WIDE },
362
+ ).files;
363
+ const check = parseFile(files.find((f) => f.name === "live-check.yml")!.yaml);
364
+ const apply = parseFile(files.find((f) => f.name === "live-apply.yml")!.yaml);
365
+
366
+ expect(JSON.stringify(check.jobs!["live-check"])).not.toContain("AWS_ACCESS_KEY_ID");
367
+ expect(apply.jobs!["live-apply"].env).toEqual(CREDENTIAL);
368
+ });
369
+
370
+ test("a per-Op key wins over a same-named forge-wide one — the job is more specific", () => {
371
+ const doc = parseFile(
372
+ generateForgejoOpPipeline(
373
+ [
374
+ {
375
+ name: "live-apply",
376
+ trigger: { kind: "push", branches: ["main"] },
377
+ variables: { CHANT_FORGE: "overridden" },
378
+ },
379
+ ],
380
+ { variables: FORGE_WIDE },
381
+ ).files[0].yaml,
382
+ );
383
+ expect(doc.jobs!["live-apply"].env).toEqual({ CHANT_FORGE: "overridden" });
384
+ });
385
+
386
+ test("a spec declaring variables changes exactly the job's env: block and nothing else", () => {
387
+ const spec: ScheduledOpSpec = { name: "live-apply", trigger: { kind: "push", branches: ["main"] } };
388
+ const before = generateForgejoOpPipeline([spec]).files[0].yaml;
389
+ const after = generateForgejoOpPipeline([{ ...spec, variables: CREDENTIAL }]).files[0].yaml;
390
+ expect(after).toContain(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n");
391
+ expect(
392
+ after.replace(" env:\n AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}'\n", ""),
393
+ ).toBe(before);
394
+ });
395
+ });
396
+
397
+ /**
398
+ * chant #2294 — the gated-apply outputs survive the notice job that consumed
399
+ * them. Asserted directly against forgejo's own output: no `outputs:` on the
400
+ * push job, no `node -e` line writing to `$GITHUB_OUTPUT`, while github's
401
+ * generator (same spec) still carries both because its notice job still reads
402
+ * them.
403
+ */
404
+ describe("generateForgejoOpPipeline: no unconsumed outputs (#2294)", () => {
405
+ const pushSpec: ScheduledOpSpec = { name: "live-apply", trigger: { kind: "push", branches: ["main"] } };
406
+
407
+ test("the push job carries no outputs: block", () => {
408
+ const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
409
+ expect(doc.jobs!["live-apply"].outputs).toBeUndefined();
410
+ });
411
+
412
+ test("the run step carries no node -e line writing to $GITHUB_OUTPUT", () => {
413
+ const doc = parseFile(generateForgejoOpPipeline([pushSpec]).files[0].yaml);
414
+ const step = doc.jobs!["live-apply"].steps.find((s) => s.id === "chant-run");
415
+ expect(step?.run).not.toContain("node -e");
416
+ expect(step?.run).not.toContain("GITHUB_OUTPUT");
417
+ // The tee'd invocation survives — it's what puts the run's own record in
418
+ // the log, gate-notice job or not.
419
+ expect(step?.run).toContain("| tee");
420
+ expect(step?.run).toContain("chant run live-apply --gated-exit 0 --json");
421
+ });
422
+
423
+ test("github, generated from the same spec, still carries both — the drop is forgejo's dialect, not the shared builder", () => {
424
+ const gh = parseFile(generateGithubOpPipeline([pushSpec]).files[0].yaml);
425
+ const ghStep = gh.jobs!["live-apply"].steps.find((s) => s.id === "chant-run");
426
+ expect(gh.jobs!["live-apply"].outputs).toEqual({
427
+ gated: "${{ steps.chant-run.outputs.gated }}",
428
+ op: "${{ steps.chant-run.outputs.op }}",
429
+ gate: "${{ steps.chant-run.outputs.gate }}",
430
+ approve: "${{ steps.chant-run.outputs.approve }}",
431
+ });
432
+ expect(ghStep?.run).toContain("node -e");
433
+ });
434
+
435
+ test("a non-push (cron) job carries no outputs and no gate script either way", () => {
436
+ const doc = parseFile(
437
+ generateForgejoOpPipeline([{ name: "live-discover", schedule: "0 6 * * *" }]).files[0].yaml,
438
+ );
439
+ expect(doc.jobs!["live-discover"].outputs).toBeUndefined();
440
+ const step = doc.jobs!["live-discover"].steps.find((s) => typeof s.run === "string");
441
+ expect(step?.run).not.toContain("node -e");
442
+ });
443
+ });
@@ -28,12 +28,20 @@
28
28
  * no mapping in ../actions.ts passes through verbatim and resolves only if
29
29
  * the runner can fetch it.
30
30
  *
31
- * One finding-mode does not cross over: `comment` (#2231) posts onto the
32
- * triggering pull request by shelling to `gh` against the GitHub API and
33
- * reading the GitHub Actions event payload. Forgejo's API is
34
- * GitHub-compatible in shape, but chant has no Forgejo client and no host
35
- * configuration to point `gh` at a Forgejo instance, so this refuses the mode
36
- * by name rather than generating a job whose finding step fails on every run.
31
+ * `comment` (#2231) crosses over too, since chant #2291: it posts onto the
32
+ * triggering pull request by shelling to `gh` against `${GITHUB_API_URL}`,
33
+ * and a Forgejo Actions job already sets that (and `github.token`) the same
34
+ * way a GitHub Actions job does. This generator used to refuse the mode by
35
+ * name here, on the premise that chant had no way to point `gh` at a Forgejo
36
+ * instance; that premise was checked against a real Forgejo
37
+ * 12.0.4+gitea-1.22.0 instance during INTENTIUS/choudoufu#1027 and found
38
+ * false — the failure was `gh api` resolving a *relative* path against
39
+ * `/api/v3`, which Forgejo does not serve, not an unreachable forge. See
40
+ * `reconcilePr`'s `postOrUpdateComment` (`packages/core/src/op/activities/reconcile.ts`)
41
+ * for the fix. `issue` mode (`gh issue create`) was not part of that
42
+ * verification — only the comment endpoints were exercised on the real
43
+ * instance — so it remains un-refused-but-unverified here, exactly as it was
44
+ * on GitHub before this change.
37
45
  *
38
46
  * A spec's `environment` (#2257) is dropped on the same terms as
39
47
  * `permissions:`, and for a stronger reason: Forgejo Actions has no
@@ -56,6 +64,24 @@
56
64
  * Forgejo apply that stops at its gate is a green run rather than a red one,
57
65
  * and `chant run` still writes the gate and the approve command to
58
66
  * `GITHUB_STEP_SUMMARY`, which Forgejo Actions sets like GitHub does.
67
+ *
68
+ * The gate-notice job's own reason for being — `outputs: { gated, op, gate,
69
+ * approve }` on the Op's job, and the `node -e` step that writes them to
70
+ * `$GITHUB_OUTPUT` — does not survive it either (#2294). Both existed only for
71
+ * that job to read via `needs.<job>.outputs`; with the job gone, so is every
72
+ * reader, and `buildGithubOpPipelineDocs` is asked for neither
73
+ * (`emitGatedOutputs: false` below) rather than emitting them here and
74
+ * stripping them back out. Should the Forgejo posting work in #2291 grow a
75
+ * notice job of its own, this is the one flag that brings both back.
76
+ *
77
+ * A spec's own `variables` (#2290) crosses over unchanged: forgejo reuses the
78
+ * same job-level `env:` github's builder emits, and the dialect transform
79
+ * touches only `permissions:`/`environment:`/action refs, never a job's `env:`
80
+ * mapping. That is what makes a per-Op credential expressible here at all —
81
+ * `ComponentPipelineOptions.variables` is workflow-scoped and, on Forgejo,
82
+ * Actions mints no OIDC token to put in `setup` instead, so a job-level
83
+ * `variables` entry is the only way one Op's job can hold a credential no
84
+ * sibling Op's job receives.
59
85
  */
60
86
 
61
87
  import {
@@ -117,18 +143,14 @@ export function generateForgejoOpPipeline(
117
143
  options: ComponentPipelineOptions = {},
118
144
  dialectOptions: ForgejoDialectOptions = {},
119
145
  ): OpPipelineResult {
120
- for (const spec of ops) {
121
- if (spec.findingMode === "comment") {
122
- throw new Error(
123
- `Scheduled Op "${spec.name}" has findingMode "comment", which posts its finding on the pull request ` +
124
- `that triggered the run. That activity shells to \`gh\` against the GitHub API and reads the ` +
125
- `GitHub Actions event payload; chant carries no Forgejo API client to post the equivalent comment ` +
126
- `(#2231). Use findingMode "issue" here, or generate this Op for github.`,
127
- );
128
- }
129
- }
146
+ // `findingMode: "comment"` used to be refused by name here (#2231); lifted
147
+ // in #2291 once a real Forgejo instance showed the forge itself was never
148
+ // the obstacle — see the module doc above.
130
149
 
131
- const { files, jobs } = buildGithubOpPipelineDocs(ops, options);
150
+ // `emitGatedOutputs: false` (#2294): forgejo never carries a gate-notice job
151
+ // (`gatedNoticeDoc` never crosses the dialect, below), so the job outputs
152
+ // and the `node -e` step that populate them would have no reader.
153
+ const { files, jobs } = buildGithubOpPipelineDocs(ops, options, { emitGatedOutputs: false });
132
154
 
133
155
  return {
134
156
  // One file per spec, in spec order, which is what lets the header below
@@ -128,10 +128,27 @@ describe("forgejoSerializer — multi-workflow output", () => {
128
128
  // "@intentius/chant-lexicon-github"`); nothing forgejo-specific is written for
129
129
  // it. This is the functional check that inheritance actually holds, not just
130
130
  // that the type is importable: the dialect still drops the job's
131
- // `permissions` and remaps its runner label, while the sticky-comment step —
132
- // a plain `gh api` script with no `uses:` at all — passes through untouched,
133
- // since Forgejo's API accepts the same calls against its GitHub-compatible
134
- // surface.
131
+ // `permissions` and remaps its runner label, and the sticky-comment step — a
132
+ // plain `gh api` script with no `uses:` at all — passes through the dialect
133
+ // transform untouched, because the dialect only ever rewrites job-level
134
+ // `permissions:`/`environment:`/action refs and never looks at step content.
135
+ //
136
+ // That passing-through is a fact about serialization, not about whether the
137
+ // script would work if run: chant #2291 settled, against a real Forgejo
138
+ // 12.0.4+gitea-1.22.0 instance, that Forgejo's `/api/v1` does accept the same
139
+ // GET/POST/PATCH calls this script makes — but only when they target a full
140
+ // URL. Until chant #2305, this script's `gh api "repos/$REPO/issues/…"` took
141
+ // a bare relative path, which `gh` resolves against `/api/v3` on any host but
142
+ // github.com, and Forgejo does not serve `/api/v3`. `reconcilePr`'s
143
+ // `postOrUpdateComment` got the URL fix #2291 made
144
+ // (`packages/core/src/op/activities/reconcile.ts`), and `PrPlanReport`'s own
145
+ // script (`lexicons/github/src/composites/pr-plan-report.ts`) — out of
146
+ // #2291's scope, being the component composite rather than the Op generator —
147
+ // got the equivalent fix in #2305: every call now targets `$api_base`, built
148
+ // from `$GITHUB_API_URL` inline in the shell script. The dialect still leaves
149
+ // step content untouched, so that base-URL construction crosses over exactly
150
+ // as written, which is what makes the untouched script below a working
151
+ // Forgejo comment rather than just a passed-through one.
135
152
  describe("forgejoSerializer — inherits PrPlanReport from github (#1983)", () => {
136
153
  test("dialect still applies to a github-lexicon composite's job", () => {
137
154
  const workflow = new Workflow({
@@ -154,6 +171,10 @@ describe("forgejoSerializer — inherits PrPlanReport from github (#1983)", () =
154
171
  // Including the `-F` that reads the body from plan.md — forgejo renders
155
172
  // the same script, so the #2236 regression would show up here as well.
156
173
  expect(result.primary).toContain("-F body=@plan.md");
174
+ // And the #2305 fix: the resolved-base construction and every `gh api`
175
+ // call built from it cross the dialect untouched too.
176
+ expect(result.primary).toContain('api_base="${GITHUB_API_URL%/}"');
177
+ expect(result.primary).not.toMatch(/gh api[^\n]*"repos\//);
157
178
  expect(result.primary).not.toContain("-f body=@");
158
179
  });
159
180
  });