@intentius/chant-lexicon-forgejo 0.124.23 → 0.124.24
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/components/generate-op-waves-pipeline.d.ts +7 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/__golden__/op-waves-jobs.yml +81 -0
- package/src/components/generate-op-waves-pipeline.test.ts +40 -0
- package/src/components/generate-op-waves-pipeline.ts +28 -0
|
@@ -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
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "8d1a9b3e747b19467ac4ea8c9664869ec8ad666b37847596d2102f47d69c3558",
|
|
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": "81febbfee8a63ebe6a7d7ab137ed222da1fd562b7a11a4fd7465e13c21cdec2d"
|
|
13
13
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-forgejo",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.24",
|
|
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.
|
|
58
|
-
"@intentius/chant-lexicon-github": "^0.124.
|
|
57
|
+
"@intentius/chant": "^0.124.24",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.124.24"
|
|
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
|
+
}
|