@intentius/chant-lexicon-forgejo 0.124.23 → 0.124.25

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.
@@ -14,6 +14,13 @@
14
14
  *
15
15
  * A `pr-review` wave (#3684) gets `<name>-plans.yml` on pull requests, as on
16
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.
17
24
  */
18
25
  import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
19
26
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
@@ -1 +1 @@
1
- {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,6EAA6E;AAC7E,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,EAC/B,cAAc,GAAE,qBAA0B,GACzC,qBAAqB,CAgBvB"}
1
+ {"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAEhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,6EAA6E;AAC7E,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,EAC/B,cAAc,GAAE,qBAA0B,GACzC,qBAAqB,CAiBvB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4d91b353db6715db9fb58dced5a640ef409910826130a76a79b52da80243c71a",
4
+ "manifest.json": "45ae4a976a5a4a68eeb3981159b993c4a4934fb73b2bbf29ea47b890a7a09e9f",
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": "54c97202efac9c91c77df2b119d0cbe8ce16511c61032c076af5cf823f1ddbc0"
12
+ "composite": "e38b66ca8684b2afc0921b4f9c9a8e6c823c67d0759e6ae417cff1a5e6c238ab"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.124.23",
3
+ "version": "0.124.25",
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.124.23",
3
+ "version": "0.124.25",
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.124.23",
58
- "@intentius/chant-lexicon-github": "^0.124.23"
57
+ "@intentius/chant": "^0.124.25",
58
+ "@intentius/chant-lexicon-github": "^0.124.25"
59
59
  },
60
60
  "scripts": {
61
61
  "generate": "tsx src/codegen/generate-cli.ts",
@@ -0,0 +1,81 @@
1
+ name: migrations
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch: {}
8
+
9
+ concurrency:
10
+ group: migrations
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ wave-1-dev:
15
+ runs-on: plan
16
+ container: node:22
17
+ steps:
18
+ - uses: https://code.forgejo.org/actions/checkout@v7
19
+ with:
20
+ fetch-depth: 0
21
+ - name: Plan, gate and apply wave 1 (dev)
22
+ run: chant run wave --spec waves.json --wave 1
23
+ wave-2-prod-decide:
24
+ runs-on: plan
25
+ needs:
26
+ - wave-1-dev
27
+ container: node:22
28
+ steps:
29
+ - uses: https://code.forgejo.org/actions/checkout@v7
30
+ with:
31
+ fetch-depth: 0
32
+ - name: Plan wave 2 (prod) and decide its gate
33
+ run: chant run wave --spec waves.json --wave 2 --decide
34
+ - name: Keep wave 2's decision
35
+ if: always()
36
+ uses: https://code.forgejo.org/actions/upload-artifact@v4
37
+ with:
38
+ name: migrations-wave-2-decision
39
+ path: .chant/op-waves/migrations/wave-2.json
40
+ if-no-files-found: ignore
41
+ include-hidden-files: true
42
+ overwrite: true
43
+ wave-2-prod-us:
44
+ runs-on:
45
+ - self-hosted
46
+ - us
47
+ needs:
48
+ - wave-2-prod-decide
49
+ container: node:22
50
+ permissions:
51
+ id-token: write
52
+ env:
53
+ REGION: us
54
+ CLOUD_TOKEN_AUDIENCE: us.example
55
+ steps:
56
+ - uses: https://code.forgejo.org/actions/checkout@v7
57
+ with:
58
+ fetch-depth: 0
59
+ - name: Download wave 2's decision
60
+ uses: https://code.forgejo.org/actions/download-artifact@v4
61
+ with:
62
+ name: migrations-wave-2-decision
63
+ path: .chant/op-waves/migrations
64
+ - name: Apply job group us of wave 2 (prod)
65
+ run: chant run wave --spec waves.json --wave 2 --group us
66
+ wave-2-prod-eu:
67
+ runs-on: eu-apply
68
+ needs:
69
+ - wave-2-prod-decide
70
+ container: node:22
71
+ steps:
72
+ - uses: https://code.forgejo.org/actions/checkout@v7
73
+ with:
74
+ fetch-depth: 0
75
+ - name: Download wave 2's decision
76
+ uses: https://code.forgejo.org/actions/download-artifact@v4
77
+ with:
78
+ name: migrations-wave-2-decision
79
+ path: .chant/op-waves/migrations
80
+ - name: Apply job group eu of wave 2 (prod)
81
+ run: chant run wave --spec waves.json --wave 2 --group eu
@@ -63,3 +63,43 @@ describe("generateForgejoOpWavesPipeline", () => {
63
63
  expect(() => generateForgejoOpWavesPipeline({ ...waveLevel, hooks: { after: { apply: [{ command: ["./bin/smoke"], onFailure: "approve" }] } } }, { specFile: "waves.json" })).toThrow(/nothing left to hold/);
64
64
  });
65
65
  });
66
+
67
+ describe("a runner per wave and job groups per wave (#3914)", () => {
68
+ const grouped: OpWavesSpec = {
69
+ ...spec,
70
+ runner: "plan",
71
+ waves: [
72
+ { name: "dev", runs: [{ target: "dev" }], gate: "never" },
73
+ {
74
+ name: "prod",
75
+ runs: [{ target: "us/a" }, { target: "eu/a" }],
76
+ jobs: [
77
+ { name: "us", runs: ["us/*"], runner: ["self-hosted", "us"], variables: { REGION: "us" }, idTokens: { CLOUD_TOKEN: "us.example" } },
78
+ { name: "eu", runs: ["eu/*"], runner: "eu-apply" },
79
+ ],
80
+ },
81
+ ],
82
+ };
83
+
84
+ test("the rendered pipeline matches its golden", async () => {
85
+ await expect(generateForgejoOpWavesPipeline(grouped, { specFile: "waves.json" }).files[0]!.yaml).toMatchFileSnapshot("./__golden__/op-waves-jobs.yml");
86
+ });
87
+
88
+ test("groups render as their own jobs with their runner, variables and id-token: write", () => {
89
+ const { files, jobs } = generateForgejoOpWavesPipeline(grouped, { specFile: "waves.json" });
90
+ expect(jobs.map((j) => j.jobName)).toEqual(["wave-1-dev", "wave-2-prod-decide", "wave-2-prod-us", "wave-2-prod-eu"]);
91
+ const yaml = files[0]!.yaml;
92
+ expect(yaml).toMatch(/wave-1-dev:\n\s+runs-on: plan\n/);
93
+ expect(yaml).toMatch(/wave-2-prod-us:\n\s+runs-on:\n\s+- self-hosted\n\s+- us\n[\s\S]*?permissions:\n\s+id-token: write\n\s+env:\n\s+REGION: us\n\s+CLOUD_TOKEN_AUDIENCE: us.example\n/);
94
+ expect(yaml).toMatch(/wave-2-prod-eu:\n\s+runs-on: eu-apply\n\s+needs:\n\s+- wave-2-prod-decide\n\s+container: node:22\n\s+steps:/);
95
+ expect(yaml).not.toContain("contents: write");
96
+ });
97
+
98
+ test("a runner group, or an environment on a job group, is a spec error naming the wave and the field", () => {
99
+ const prod = grouped.waves[1]!;
100
+ const withGroup = (g: Record<string, unknown>): OpWavesSpec => ({ ...grouped, waves: [grouped.waves[0]!, { ...prod, jobs: [{ ...prod.jobs![0]!, ...g }, prod.jobs![1]!] }] });
101
+ expect(() => generateForgejoOpWavesPipeline(withGroup({ environment: { name: "prod-us" } }), { specFile: "waves.json" })).toThrow(/wave "prod" job group "us" names the environment "prod-us", but Forgejo Actions has no environments/);
102
+ expect(() => generateForgejoOpWavesPipeline(withGroup({ runner: { group: "apply" } }), { specFile: "waves.json" })).toThrow(/wave "prod" job group "us" runner is the runner group "apply", which only GitHub has/);
103
+ expect(() => generateForgejoOpWavesPipeline({ ...grouped, waves: [{ ...grouped.waves[0]!, runner: { group: "x" } }] }, { specFile: "waves.json" })).toThrow(/wave "dev" runner is the runner group/);
104
+ });
105
+ });
@@ -14,11 +14,19 @@
14
14
  *
15
15
  * A `pr-review` wave (#3684) gets `<name>-plans.yml` on pull requests, as on
16
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.
17
24
  */
18
25
 
19
26
  import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
20
27
  import { emitOpPipelineYAML } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
21
28
  import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
29
+ import { assertRunnerLabels } from "@intentius/chant/ci-jobs";
22
30
  import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
23
31
  import type { ForgejoDialectOptions } from "../dialect";
24
32
  import { droppedWaveEnvironmentsHeader, FORGE_TOKEN_ENV, forgejoWorkflowDoc } from "./ci-jobs";
@@ -29,6 +37,7 @@ export function generateForgejoOpWavesPipeline(
29
37
  options: OpWavesPipelineOptions,
30
38
  dialectOptions: ForgejoDialectOptions = {},
31
39
  ): OpWavesPipelineResult {
40
+ assertForgejoWaves(spec);
32
41
  const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
33
42
  const dropped = spec.waves.filter((w) => w.environment).map((w) => `${w.name} -> ${w.environment!.name}`);
34
43
  const header = dropped.length > 0 ? droppedWaveEnvironmentsHeader(dropped) : undefined;
@@ -45,3 +54,22 @@ export function generateForgejoOpWavesPipeline(
45
54
  if (plansDoc) files.push({ name: `${spec.name}-plans.yml`, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(plansDoc, dialectOptions)) });
46
55
  return { files, jobs };
47
56
  }
57
+
58
+ /** Refuse what Forgejo cannot carry (#3914): a runner group, and a job group's environment. */
59
+ function assertForgejoWaves(spec: OpWavesSpec): void {
60
+ const subject = `Op waves "${spec.name}"`;
61
+ assertRunnerLabels(subject, "runner", spec.runner, "Forgejo");
62
+ for (const wave of spec.waves) {
63
+ assertRunnerLabels(subject, `wave "${wave.name}" runner`, wave.runner, "Forgejo");
64
+ for (const group of wave.jobs ?? []) {
65
+ const field = `wave "${wave.name}" job group "${group.name}"`;
66
+ assertRunnerLabels(subject, `${field} runner`, group.runner, "Forgejo");
67
+ if (group.environment) {
68
+ throw new Error(
69
+ `${subject}: ${field} names the environment "${group.environment.name}", but Forgejo Actions has no environments, ` +
70
+ "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.",
71
+ );
72
+ }
73
+ }
74
+ }
75
+ }