@intentius/chant-lexicon-forgejo 0.59.0 → 0.61.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.
@@ -35,6 +35,17 @@
35
35
  * configuration to point `gh` at a Forgejo instance, so this refuses the mode
36
36
  * by name rather than generating a job whose finding step fails on every run.
37
37
  *
38
+ * A spec's `environment` (#2257) is dropped on the same terms as
39
+ * `permissions:`, and for a stronger reason: Forgejo Actions has no
40
+ * environments at all — no protection rules, no required reviewers, no
41
+ * per-environment secrets — so the key names an object that does not exist on
42
+ * the instance. Dropping it silently would be the worst outcome available,
43
+ * because the whole point of the option is a human holding an apply, so this
44
+ * also writes a comment into the generated file's header saying which
45
+ * environment was asked for and that nothing on Forgejo enforces it. What
46
+ * still holds the apply here is chant's own gate (#2119), which is a fact on
47
+ * the `chant/lifecycle` branch and needs nothing from the forge.
48
+ *
38
49
  * The gated-apply notice job (#2243) does not cross over either, for the same
39
50
  * reason and by the same mechanism as `permissions:`: it shells to `gh`, which
40
51
  * a Forgejo `act_runner` neither ships nor can point at its own instance, and
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAOH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAOjF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,gBAAgB,CA2BlB"}
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,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "e73b98e495fadacf1d8eb846417c22f36d149b2f6a5fbaa822e3aa97cd0f987b",
4
+ "manifest.json": "3ad50028d0c5d782a8ab2eb2698b266fc87cc318ca165f3afb975673e50fbc12",
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": "0081cd5b78e54825816485bdff8d43a75f9dff6f63a59c8e32c7358c88dcdab2"
12
+ "composite": "6b8ee9eafa0ad60a4c7bff3c450faf3de13857d7b67766ec6a870f0695152fd6"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.59.0",
3
+ "version": "0.61.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.59.0",
3
+ "version": "0.61.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.59.0",
58
- "@intentius/chant-lexicon-github": "^0.59.0"
57
+ "@intentius/chant": "^0.61.0",
58
+ "@intentius/chant-lexicon-github": "^0.61.0"
59
59
  },
60
60
  "scripts": {
61
61
  "generate": "tsx src/codegen/generate-cli.ts",
@@ -14,6 +14,7 @@ import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
14
14
 
15
15
  interface ParsedJob {
16
16
  "runs-on"?: string;
17
+ environment?: Record<string, string>;
17
18
  outputs?: Record<string, string>;
18
19
  steps: Array<{ id?: string; uses?: string; run?: string }>;
19
20
  }
@@ -198,3 +199,81 @@ describe("generateForgejoOpPipeline: the gated apply on push (#2243)", () => {
198
199
  expect(gh.jobs).toHaveProperty("app-apply-gate-notice");
199
200
  });
200
201
  });
202
+
203
+ /**
204
+ * chant #2257 — the option Forgejo has no concept for at all. Unlike
205
+ * `permissions:`, which the runner reads and ignores, an environment is an
206
+ * object that does not exist on a Forgejo instance: no protection rules, no
207
+ * required reviewers, no wait timers. Emitting the key would read as a
208
+ * deployment gate and hold nothing back, so it is dropped — and, because the
209
+ * whole point of the option is a human holding an apply, the drop is said out
210
+ * loud in the file rather than only in a build warning.
211
+ */
212
+ describe("generateForgejoOpPipeline: a dropped deployment environment (#2257)", () => {
213
+ const APPLY: ScheduledOpSpec = { name: "app-apply", trigger: { kind: "push", branches: ["main"] } };
214
+ const GATED: ScheduledOpSpec = { ...APPLY, environment: { name: "production" } };
215
+
216
+ /** The document a spec with no `environment` emitted before the option existed. */
217
+ const AUDIT_YAML_BEFORE_2257 =
218
+ [
219
+ "on:",
220
+ " schedule:",
221
+ " - cron: '0 6 * * *'",
222
+ " workflow_dispatch: {}",
223
+ "",
224
+ "concurrency:",
225
+ " group: actions-audit",
226
+ " cancel-in-progress: false",
227
+ "",
228
+ "jobs:",
229
+ " actions-audit:",
230
+ " runs-on: docker",
231
+ " container: node:22-slim",
232
+ " steps:",
233
+ " - uses: https://code.forgejo.org/actions/checkout@v4",
234
+ " - run: chant run actions-audit",
235
+ " env:",
236
+ " GITHUB_TOKEN: '${{ github.token }}'",
237
+ " GH_TOKEN: '${{ github.token }}'",
238
+ ].join("\n") + "\n";
239
+
240
+ test("a spec with no environment emits the bytes it emitted before the option existed", () => {
241
+ const yaml = generateForgejoOpPipeline([
242
+ { name: "actions-audit", schedule: "0 6 * * *", findingMode: "issue" },
243
+ ]).files[0].yaml;
244
+ expect(yaml).toBe(AUDIT_YAML_BEFORE_2257);
245
+ });
246
+
247
+ test("drops the key, and github with the same spec keeps it", () => {
248
+ const fj = parseFile(generateForgejoOpPipeline([GATED]).files[0].yaml);
249
+ expect(fj.jobs!["app-apply"].environment).toBeUndefined();
250
+ // A dialect drop, not a builder that never computed it.
251
+ expect(generateGithubOpPipeline([GATED]).files[0].yaml).toContain("environment:");
252
+ });
253
+
254
+ test("says in the generated header which environment did not survive, and what still gates", () => {
255
+ const yaml = generateForgejoOpPipeline([GATED]).files[0].yaml;
256
+ expect(yaml.startsWith("# chant dropped `environment: production`")).toBe(true);
257
+ expect(yaml).toContain("Forgejo Actions has no");
258
+ expect(yaml).toContain("not held back by anything on the forge");
259
+ // The gate that does survive is chant's own, which needs nothing from the forge.
260
+ expect(yaml).toContain("chant/lifecycle");
261
+ expect(yaml).toContain("approve <op> <gate>");
262
+ });
263
+
264
+ test("the header only appears for the Op that asked for an environment", () => {
265
+ const files = generateForgejoOpPipeline([
266
+ { ...GATED, name: "app-apply" },
267
+ { name: "app-watch", schedule: "0 6 * * *" },
268
+ ]).files;
269
+ expect(files[0].yaml).toContain("# chant dropped `environment: production`");
270
+ expect(files[1].yaml).not.toContain("# chant dropped");
271
+ expect(files[1].yaml.startsWith("on:")).toBe(true);
272
+ });
273
+
274
+ test("refuses the same malformed environment github refuses, through the shared builder", () => {
275
+ expect(() =>
276
+ generateForgejoOpPipeline([{ ...APPLY, environment: { name: "production", url: "/deploys" } }]),
277
+ ).toThrow(/neither an absolute http\(s\) URL nor a/);
278
+ });
279
+ });
@@ -35,6 +35,17 @@
35
35
  * configuration to point `gh` at a Forgejo instance, so this refuses the mode
36
36
  * by name rather than generating a job whose finding step fails on every run.
37
37
  *
38
+ * A spec's `environment` (#2257) is dropped on the same terms as
39
+ * `permissions:`, and for a stronger reason: Forgejo Actions has no
40
+ * environments at all — no protection rules, no required reviewers, no
41
+ * per-environment secrets — so the key names an object that does not exist on
42
+ * the instance. Dropping it silently would be the worst outcome available,
43
+ * because the whole point of the option is a human holding an apply, so this
44
+ * also writes a comment into the generated file's header saying which
45
+ * environment was asked for and that nothing on Forgejo enforces it. What
46
+ * still holds the apply here is chant's own gate (#2119), which is a fact on
47
+ * the `chant/lifecycle` branch and needs nothing from the forge.
48
+ *
38
49
  * The gated-apply notice job (#2243) does not cross over either, for the same
39
50
  * reason and by the same mechanism as `permissions:`: it shells to `gh`, which
40
51
  * a Forgejo `act_runner` neither ships nor can point at its own instance, and
@@ -64,6 +75,34 @@ function forgejoize(value: Record<string, unknown>, dialect: ForgejoDialectOptio
64
75
  return transformWorkflowObject(value, dialect).value as Record<string, unknown>;
65
76
  }
66
77
 
78
+ /**
79
+ * Strip the job-level `environment:` github emitted (#2257). Forgejo Actions
80
+ * has no environments, so the key would read as a deployment gate and hold
81
+ * nothing back. Dropped from the rebuilt doc rather than from the dialect's
82
+ * own `DROPPED_KEYS`, so this touches the Op generator alone and leaves every
83
+ * other document the dialect transforms exactly as it emits today.
84
+ */
85
+ function withoutEnvironment(jobsDoc: Record<string, unknown>): Record<string, unknown> {
86
+ return Object.fromEntries(
87
+ Object.entries(jobsDoc).map(([jobName, job]) => {
88
+ if (typeof job !== "object" || job === null || !("environment" in job)) return [jobName, job];
89
+ const { environment: _dropped, ...rest } = job as Record<string, unknown>;
90
+ return [jobName, rest];
91
+ }),
92
+ );
93
+ }
94
+
95
+ /** Say in the generated file which environment gate did not survive the crossing. */
96
+ function droppedEnvironmentHeader(environmentName: string): string[] {
97
+ return [
98
+ `# chant dropped \`environment: ${environmentName}\` from this workflow: Forgejo Actions has no`,
99
+ "# environments, so there is no protection rule, required reviewer or wait timer for the key to",
100
+ "# name. This job is not held back by anything on the forge. What still stops the apply is chant's",
101
+ "# own gate: the run records a pending fact on the chant/lifecycle branch and ends, and `chant",
102
+ "# approve <op> <gate>` is what lets the next run through.",
103
+ ];
104
+ }
105
+
67
106
  /**
68
107
  * Synthesize one `.forgejo/workflows/*.yml` per scheduled Op. Reuses github's
69
108
  * trigger/job structure ({@link buildGithubOpPipelineDocs}) — its `setup`-step
@@ -92,13 +131,17 @@ export function generateForgejoOpPipeline(
92
131
  const { files, jobs } = buildGithubOpPipelineDocs(ops, options);
93
132
 
94
133
  return {
95
- files: files.map(({ name, doc }) => {
134
+ // One file per spec, in spec order, which is what lets the header below
135
+ // read its Op's own `environment` off the input by position.
136
+ files: files.map(({ name, doc }, index) => {
137
+ const environment = ops[index].environment;
96
138
  const forgejoDoc: GithubOpPipelineDoc = {
139
+ ...(environment ? { header: droppedEnvironmentHeader(environment.name) } : {}),
97
140
  on: forgejoize(doc.on, dialectOptions),
98
141
  ...(doc.env ? { env: forgejoize(doc.env, dialectOptions) } : {}),
99
142
  concurrency: forgejoize(doc.concurrency, dialectOptions),
100
143
  permissions: {},
101
- jobsDoc: forgejoize(doc.jobsDoc, dialectOptions),
144
+ jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialectOptions),
102
145
  };
103
146
  return { name, yaml: emitOpPipelineYAML(forgejoDoc) };
104
147
  }),