@intentius/chant-lexicon-forgejo 0.124.54 → 0.124.56
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/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +1 -0
- package/dist/components/control-pipeline.d.ts +12 -0
- package/dist/components/control-pipeline.d.ts.map +1 -0
- package/dist/components/control-pipeline.js +20 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.js +5 -1
- package/dist/components/generate-op-waves-pipeline.d.ts +6 -2
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +7 -3
- 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 +16 -0
- package/src/components/ci-jobs.ts +1 -0
- package/src/components/control-pipeline.ts +27 -0
- package/src/components/generate-op-pipeline.agent.test.ts +46 -0
- package/src/components/generate-op-pipeline.cloud.test.ts +25 -0
- package/src/components/generate-op-pipeline.ts +4 -1
- package/src/components/generate-op-waves-pipeline.approval.test.ts +26 -0
- package/src/components/generate-op-waves-pipeline.control.test.ts +38 -0
- package/src/components/generate-op-waves-pipeline.guard.test.ts +31 -0
- package/src/components/generate-op-waves-pipeline.test.ts +20 -2
- package/src/components/generate-op-waves-pipeline.ts +7 -3
- package/src/components/generate-pr-pipeline.comment-locks.test.ts +25 -0
- package/src/components/generate-pr-pipeline.comment-preview.test.ts +31 -0
- package/src/components/generate-pr-pipeline.comment-targets.test.ts +18 -0
- package/src/components/generate-pr-pipeline.control.test.ts +30 -0
- package/src/components/generate-pr-pipeline.runners.test.ts +24 -0
- package/src/components/generate-pr-pipeline.stale-notes.test.ts +12 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ci-jobs.d.ts","sourceRoot":"","sources":["../../src/components/ci-jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iEAAiE,CAAC;AAC3G,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEjF,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAElH;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ5F;AAED,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1E;AAED,mHAAmH;AACnH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOlF;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe;IAAK,iBAAiB;CAAqC,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,mBAAmB,
|
|
1
|
+
{"version":3,"file":"ci-jobs.d.ts","sourceRoot":"","sources":["../../src/components/ci-jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iEAAiE,CAAC;AAC3G,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEjF,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAElH;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ5F;AAED,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1E;AAED,mHAAmH;AACnH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOlF;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe;IAAK,iBAAiB;CAAqC,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAcnI"}
|
|
@@ -68,5 +68,6 @@ export function forgejoWorkflowDoc(doc, dialect, header) {
|
|
|
68
68
|
jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialect),
|
|
69
69
|
...(doc.gatedNoticeDoc ? { gatedNoticeDoc: forgejoize(doc.gatedNoticeDoc, dialect) } : {}),
|
|
70
70
|
...(doc.reviewNoteDoc ? { reviewNoteDoc: forgejoize(doc.reviewNoteDoc, dialect) } : {}),
|
|
71
|
+
...(doc.agentDoc ? { agentDoc: forgejoize(doc.agentDoc, dialect) } : {}),
|
|
71
72
|
};
|
|
72
73
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control repo's own workflow (#4568) on Forgejo: github's document
|
|
3
|
+
* (`controlGithubDoc`) with the Forgejo dialect applied, the way
|
|
4
|
+
* ./generate-pipeline.ts reuses github's pipeline. Forgejo has no
|
|
5
|
+
* `repository_dispatch`, so a member's dispatch is a workflow dispatch of
|
|
6
|
+
* this file by name.
|
|
7
|
+
*/
|
|
8
|
+
import { type ControlPipelineFile, type ControlPipelineOptions } from "@intentius/chant-lexicon-github/components/control-pipeline";
|
|
9
|
+
import type { ForgejoDialectOptions } from "../dialect.js";
|
|
10
|
+
/** The control repo's workflow on Forgejo: `.forgejo/workflows/<product>-dependents.yml`. */
|
|
11
|
+
export declare function generateControlPipeline(options?: ControlPipelineOptions, dialect?: ForgejoDialectOptions): ControlPipelineFile;
|
|
12
|
+
//# sourceMappingURL=control-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/control-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC5B,MAAM,6DAA6D,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,6FAA6F;AAC7F,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,sBAA2B,EAAE,OAAO,GAAE,qBAA0B,GAAG,mBAAmB,CAQtI"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control repo's own workflow (#4568) on Forgejo: github's document
|
|
3
|
+
* (`controlGithubDoc`) with the Forgejo dialect applied, the way
|
|
4
|
+
* ./generate-pipeline.ts reuses github's pipeline. Forgejo has no
|
|
5
|
+
* `repository_dispatch`, so a member's dispatch is a workflow dispatch of
|
|
6
|
+
* this file by name.
|
|
7
|
+
*/
|
|
8
|
+
import { controlGithubDoc, } from "@intentius/chant-lexicon-github/components/control-pipeline";
|
|
9
|
+
import { emitWorkflowYAML } from "@intentius/chant-lexicon-github/components/ci-jobs";
|
|
10
|
+
import { forgejoize } from "./ci-jobs.js";
|
|
11
|
+
/** The control repo's workflow on Forgejo: `.forgejo/workflows/<product>-dependents.yml`. */
|
|
12
|
+
export function generateControlPipeline(options = {}, dialect = {}) {
|
|
13
|
+
const { doc, path, notes } = controlGithubDoc("forgejo", options);
|
|
14
|
+
const content = emitWorkflowYAML({
|
|
15
|
+
...doc,
|
|
16
|
+
on: forgejoize(doc.on, dialect),
|
|
17
|
+
jobsDoc: forgejoize(doc.jobsDoc, dialect),
|
|
18
|
+
});
|
|
19
|
+
return { path, content, notes };
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAMH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-op-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAMH,OAAO,KAAK,EACV,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,eAAe,EAAE,EACtB,OAAO,GAAE,wBAA6B,EACtC,cAAc,GAAE,qBAA0B,GACzC,gBAAgB,CAqBlB"}
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
* sibling Op's job receives.
|
|
87
87
|
*/
|
|
88
88
|
import { buildGithubOpPipelineDocs, emitOpPipelineYAML, } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
|
|
89
|
+
import { assertRunnerLabels } from "@intentius/chant/ci-jobs";
|
|
89
90
|
import { droppedEnvironmentHeader, forgejoWorkflowDoc } from "./ci-jobs.js";
|
|
90
91
|
/**
|
|
91
92
|
* Synthesize one `.forgejo/workflows/*.yml` per scheduled Op. Reuses github's
|
|
@@ -100,7 +101,10 @@ export function generateForgejoOpPipeline(ops, options = {}, dialectOptions = {}
|
|
|
100
101
|
// `findingMode: "comment"` used to be refused by name here (#2231), and
|
|
101
102
|
// `findingMode: "issue"` from #2332 to #2351. Neither is now: both post
|
|
102
103
|
// through the Forgejo client — see the module doc above.
|
|
103
|
-
|
|
104
|
+
// #4616: a spec's runner is a label or a list of labels here.
|
|
105
|
+
for (const spec of ops)
|
|
106
|
+
assertRunnerLabels(`Scheduled Op "${spec.name}"`, "runner", spec.runner, "Forgejo");
|
|
107
|
+
const { files, jobs } = buildGithubOpPipelineDocs(ops, options, "forgejo");
|
|
104
108
|
return {
|
|
105
109
|
// One file per spec, in spec order, which is what lets the header below
|
|
106
110
|
// read its Op's own `environment` off the input by position.
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* so it dispatches the waves workflow again on its branch, with the
|
|
13
13
|
* `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
|
|
14
14
|
*
|
|
15
|
-
* A `pr-review` wave (#3684)
|
|
16
|
-
* GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
15
|
+
* A `pr-review` wave (#3684), or a `pull-request` wave (#4630), gets
|
|
16
|
+
* `<name>-plans.yml` on pull requests, as on GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
17
17
|
*
|
|
18
18
|
* `runner` and a wave's `jobs` (#3914) render as on GitHub, with two limits:
|
|
19
19
|
* a runner is a label or a list of labels (a GitHub runner group is a spec
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* none to put in front of it. A job with id tokens keeps `permissions:
|
|
22
22
|
* id-token: write`, the one scope the dialect leaves, and reads each audience
|
|
23
23
|
* from `<NAME>_AUDIENCE` when it requests its token.
|
|
24
|
+
*
|
|
25
|
+
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
|
+
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
|
+
* the runner, and no forge environment, since Forgejo has none.
|
|
24
28
|
*/
|
|
25
29
|
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
26
30
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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"}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* so it dispatches the waves workflow again on its branch, with the
|
|
13
13
|
* `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
|
|
14
14
|
*
|
|
15
|
-
* A `pr-review` wave (#3684)
|
|
16
|
-
* GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
15
|
+
* A `pr-review` wave (#3684), or a `pull-request` wave (#4630), gets
|
|
16
|
+
* `<name>-plans.yml` on pull requests, as on GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
17
17
|
*
|
|
18
18
|
* `runner` and a wave's `jobs` (#3914) render as on GitHub, with two limits:
|
|
19
19
|
* a runner is a label or a list of labels (a GitHub runner group is a spec
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* none to put in front of it. A job with id tokens keeps `permissions:
|
|
22
22
|
* id-token: write`, the one scope the dialect leaves, and reads each audience
|
|
23
23
|
* from `<NAME>_AUDIENCE` when it requests its token.
|
|
24
|
+
*
|
|
25
|
+
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
|
+
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
|
+
* the runner, and no forge environment, since Forgejo has none.
|
|
24
28
|
*/
|
|
25
29
|
import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
|
|
26
30
|
import { emitOpPipelineYAML } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
|
|
@@ -29,7 +33,7 @@ import { droppedWaveEnvironmentsHeader, FORGE_TOKEN_ENV, forgejoWorkflowDoc } fr
|
|
|
29
33
|
/** Render an Op waves spec as one Forgejo Actions workflow, `<name>.yml`. */
|
|
30
34
|
export function generateForgejoOpWavesPipeline(spec, options, dialectOptions = {}) {
|
|
31
35
|
assertForgejoWaves(spec);
|
|
32
|
-
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
|
|
36
|
+
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options, { forge: "forgejo" });
|
|
33
37
|
const dropped = spec.waves.filter((w) => w.environment).map((w) => `${w.name} -> ${w.environment.name}`);
|
|
34
38
|
const header = dropped.length > 0 ? droppedWaveEnvironmentsHeader(dropped) : undefined;
|
|
35
39
|
const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(doc, dialectOptions, header)) }];
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "d8927356783df4ae21c431e0a016299273a031fecc05ead9df07f900b57ed4c7",
|
|
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": "7b0449ca0d3675a7e8c15a30fc85f3de807f6eb7eb538536867e77edd16ee133"
|
|
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.56",
|
|
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.56",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.124.56"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"generate": "tsx src/codegen/generate-cli.ts",
|
|
@@ -15,6 +15,10 @@ jobs:
|
|
|
15
15
|
runs-on: plan
|
|
16
16
|
container: node:22
|
|
17
17
|
steps:
|
|
18
|
+
- name: Check the job runs where the spec set it to run
|
|
19
|
+
env:
|
|
20
|
+
CHANT_JOB_GUARD: '{"forge":"forgejo","job":"wave 1 (dev) of migrations","branches":["main"]}'
|
|
21
|
+
run: '(cd / && node -e ''"use strict";const E=process.env,A="chant-check-job";const shown=(v)=>v===undefined||v===null||v===""?"none":v;const fail=(m)=>{console.error("check-job: "+m);process.exit(9)};let x;try{x=JSON.parse(E.CHANT_JOB_GUARD||"")}catch(e){fail("CHANT_JOB_GUARD is not JSON")}if(!x||!["github","gitlab","forgejo"].includes(x.forge)||!Array.isArray(x.branches))fail("CHANT_JOB_GUARD is not a guard spec");const tags=(r)=>{if(r===undefined)return undefined;const t=r.trim();if(t.startsWith("[")){try{const v=JSON.parse(t);if(Array.isArray(v))return v.map(String)}catch(e){}}return t===""?[]:t.split(",").map((s)=>s.trim()).filter((s)=>s!=="")};const claimsOf=(tok)=>{const p=tok.split(".")[1];if(!p)throw new Error("the forge\u0027s OIDC token is not a JWT");const c=JSON.parse(Buffer.from(p.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"));if(!c||typeof c!=="object")throw new Error("the forge\u0027s OIDC token has no claims");return c};async function seenOf(){if(x.forge==="gitlab")return{forgeEnvironment:E.CI_ENVIRONMENT_NAME||null,ref:E.CI_COMMIT_REF_NAME,refProtected:E.CI_COMMIT_REF_PROTECTED,runnerTags:tags(E.CI_RUNNER_TAGS)};const s={ref:E.GITHUB_REF};let c;if(x.forge==="github"&&x.forgeEnvironment!==undefined){try{if(!E.ACTIONS_ID_TOKEN_REQUEST_URL||!E.ACTIONS_ID_TOKEN_REQUEST_TOKEN)throw new Error("the job may request no OIDC token (no ACTIONS_ID_TOKEN_REQUEST_URL); it needs the id-token: write permission");const r=await fetch(E.ACTIONS_ID_TOKEN_REQUEST_URL+"&audience="+encodeURIComponent(A),{headers:{authorization:"bearer "+E.ACTIONS_ID_TOKEN_REQUEST_TOKEN}});if(!r.ok)throw new Error("the OIDC token request answered "+r.status);const v=(await r.json()).value;if(typeof v!=="string")throw new Error("the OIDC token request answered no token");c=claimsOf(v);s.forgeEnvironment=typeof c.environment==="string"&&c.environment!==""?c.environment:null}catch(e){s.forgeEnvironmentError=e&&e.message?e.message:String(e)}}const run=c&&typeof c.runner_environment==="string"?c.runner_environment:E.RUNNER_ENVIRONMENT;if(run)s.runnerEnvironment=run;return s}seenOf().then((s)=>{const P=[],C=[],S=[];const fe=x.forgeEnvironment;if(x.forge==="forgejo")S.push("forge environment (Forgejo has none)");else if(fe!==undefined){if(s.forgeEnvironmentError!==undefined)P.push("forge environment: expected "+fe+", and it could not be read: "+s.forgeEnvironmentError);else if(s.forgeEnvironment!==fe)P.push("forge environment: expected "+fe+", found "+shown(s.forgeEnvironment));else C.push("forge environment "+fe)}const b=x.branches.filter((v)=>v!=="");if(b.length===0)P.push("ref: the job names no branch to run from");else if(x.forge==="gitlab"){if(!s.ref||!b.includes(s.ref))P.push("ref: expected "+b.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref);if(s.refProtected!=="true")P.push("ref: expected a protected ref (CI_COMMIT_REF_PROTECTED true), found "+shown(s.refProtected));else C.push("protected ref")}else{const refs=b.map((v)=>"refs/heads/"+v);if(!s.ref||!refs.includes(s.ref))P.push("ref: expected "+refs.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref)}if(x.forge==="gitlab"){const w=x.runnerTags||[];if(w.length>0){const h=s.runnerTags;const miss=w.filter((t)=>!(h||[]).includes(t));if(h===undefined)P.push("runner: expected tags "+w.join(", ")+", and the job has no CI_RUNNER_TAGS");else if(miss.length>0)P.push("runner: expected tags "+w.join(", ")+", found "+(h.length?h.join(", "):"none"));else C.push("runner tags "+w.join(", "))}}else if(x.selfHosted){if(s.runnerEnvironment===undefined)S.push("runner (the runner says nothing about itself: no RUNNER_ENVIRONMENT)");else if(s.runnerEnvironment!=="self-hosted")P.push("runner: expected self-hosted, found "+s.runnerEnvironment);else C.push("self-hosted runner")}const at=(x.product||"chant")+" check-job: "+x.job;if(P.length>0){for(const p of P)console.error(at+": "+p);console.error(at+": this job does not run where the spec set it to run, so it stops before any credential is taken or code checked out");process.exit(9)}console.log(at+": "+(C.length?C.join(", "):"nothing to check")+(S.length?"; not checked: "+S.join(", "):""))}).catch((e)=>fail(e&&e.message?e.message:String(e)));'')'
|
|
18
22
|
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
19
23
|
with:
|
|
20
24
|
fetch-depth: 0
|
|
@@ -34,6 +38,10 @@ jobs:
|
|
|
34
38
|
if: '!cancelled()'
|
|
35
39
|
container: node:22
|
|
36
40
|
steps:
|
|
41
|
+
- name: Check the job runs where the spec set it to run
|
|
42
|
+
env:
|
|
43
|
+
CHANT_JOB_GUARD: '{"forge":"forgejo","job":"the deciding job of wave 2 (prod) of migrations","branches":["main"]}'
|
|
44
|
+
run: '(cd / && node -e ''"use strict";const E=process.env,A="chant-check-job";const shown=(v)=>v===undefined||v===null||v===""?"none":v;const fail=(m)=>{console.error("check-job: "+m);process.exit(9)};let x;try{x=JSON.parse(E.CHANT_JOB_GUARD||"")}catch(e){fail("CHANT_JOB_GUARD is not JSON")}if(!x||!["github","gitlab","forgejo"].includes(x.forge)||!Array.isArray(x.branches))fail("CHANT_JOB_GUARD is not a guard spec");const tags=(r)=>{if(r===undefined)return undefined;const t=r.trim();if(t.startsWith("[")){try{const v=JSON.parse(t);if(Array.isArray(v))return v.map(String)}catch(e){}}return t===""?[]:t.split(",").map((s)=>s.trim()).filter((s)=>s!=="")};const claimsOf=(tok)=>{const p=tok.split(".")[1];if(!p)throw new Error("the forge\u0027s OIDC token is not a JWT");const c=JSON.parse(Buffer.from(p.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"));if(!c||typeof c!=="object")throw new Error("the forge\u0027s OIDC token has no claims");return c};async function seenOf(){if(x.forge==="gitlab")return{forgeEnvironment:E.CI_ENVIRONMENT_NAME||null,ref:E.CI_COMMIT_REF_NAME,refProtected:E.CI_COMMIT_REF_PROTECTED,runnerTags:tags(E.CI_RUNNER_TAGS)};const s={ref:E.GITHUB_REF};let c;if(x.forge==="github"&&x.forgeEnvironment!==undefined){try{if(!E.ACTIONS_ID_TOKEN_REQUEST_URL||!E.ACTIONS_ID_TOKEN_REQUEST_TOKEN)throw new Error("the job may request no OIDC token (no ACTIONS_ID_TOKEN_REQUEST_URL); it needs the id-token: write permission");const r=await fetch(E.ACTIONS_ID_TOKEN_REQUEST_URL+"&audience="+encodeURIComponent(A),{headers:{authorization:"bearer "+E.ACTIONS_ID_TOKEN_REQUEST_TOKEN}});if(!r.ok)throw new Error("the OIDC token request answered "+r.status);const v=(await r.json()).value;if(typeof v!=="string")throw new Error("the OIDC token request answered no token");c=claimsOf(v);s.forgeEnvironment=typeof c.environment==="string"&&c.environment!==""?c.environment:null}catch(e){s.forgeEnvironmentError=e&&e.message?e.message:String(e)}}const run=c&&typeof c.runner_environment==="string"?c.runner_environment:E.RUNNER_ENVIRONMENT;if(run)s.runnerEnvironment=run;return s}seenOf().then((s)=>{const P=[],C=[],S=[];const fe=x.forgeEnvironment;if(x.forge==="forgejo")S.push("forge environment (Forgejo has none)");else if(fe!==undefined){if(s.forgeEnvironmentError!==undefined)P.push("forge environment: expected "+fe+", and it could not be read: "+s.forgeEnvironmentError);else if(s.forgeEnvironment!==fe)P.push("forge environment: expected "+fe+", found "+shown(s.forgeEnvironment));else C.push("forge environment "+fe)}const b=x.branches.filter((v)=>v!=="");if(b.length===0)P.push("ref: the job names no branch to run from");else if(x.forge==="gitlab"){if(!s.ref||!b.includes(s.ref))P.push("ref: expected "+b.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref);if(s.refProtected!=="true")P.push("ref: expected a protected ref (CI_COMMIT_REF_PROTECTED true), found "+shown(s.refProtected));else C.push("protected ref")}else{const refs=b.map((v)=>"refs/heads/"+v);if(!s.ref||!refs.includes(s.ref))P.push("ref: expected "+refs.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref)}if(x.forge==="gitlab"){const w=x.runnerTags||[];if(w.length>0){const h=s.runnerTags;const miss=w.filter((t)=>!(h||[]).includes(t));if(h===undefined)P.push("runner: expected tags "+w.join(", ")+", and the job has no CI_RUNNER_TAGS");else if(miss.length>0)P.push("runner: expected tags "+w.join(", ")+", found "+(h.length?h.join(", "):"none"));else C.push("runner tags "+w.join(", "))}}else if(x.selfHosted){if(s.runnerEnvironment===undefined)S.push("runner (the runner says nothing about itself: no RUNNER_ENVIRONMENT)");else if(s.runnerEnvironment!=="self-hosted")P.push("runner: expected self-hosted, found "+s.runnerEnvironment);else C.push("self-hosted runner")}const at=(x.product||"chant")+" check-job: "+x.job;if(P.length>0){for(const p of P)console.error(at+": "+p);console.error(at+": this job does not run where the spec set it to run, so it stops before any credential is taken or code checked out");process.exit(9)}console.log(at+": "+(C.length?C.join(", "):"nothing to check")+(S.length?"; not checked: "+S.join(", "):""))}).catch((e)=>fail(e&&e.message?e.message:String(e)));'')'
|
|
37
45
|
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
38
46
|
with:
|
|
39
47
|
fetch-depth: 0
|
|
@@ -70,6 +78,10 @@ jobs:
|
|
|
70
78
|
REGION: us
|
|
71
79
|
CLOUD_TOKEN_AUDIENCE: us.example
|
|
72
80
|
steps:
|
|
81
|
+
- name: Check the job runs where the spec set it to run
|
|
82
|
+
env:
|
|
83
|
+
CHANT_JOB_GUARD: '{"forge":"forgejo","job":"job group us of wave 2 (prod) of migrations","branches":["main"],"selfHosted":true}'
|
|
84
|
+
run: '(cd / && node -e ''"use strict";const E=process.env,A="chant-check-job";const shown=(v)=>v===undefined||v===null||v===""?"none":v;const fail=(m)=>{console.error("check-job: "+m);process.exit(9)};let x;try{x=JSON.parse(E.CHANT_JOB_GUARD||"")}catch(e){fail("CHANT_JOB_GUARD is not JSON")}if(!x||!["github","gitlab","forgejo"].includes(x.forge)||!Array.isArray(x.branches))fail("CHANT_JOB_GUARD is not a guard spec");const tags=(r)=>{if(r===undefined)return undefined;const t=r.trim();if(t.startsWith("[")){try{const v=JSON.parse(t);if(Array.isArray(v))return v.map(String)}catch(e){}}return t===""?[]:t.split(",").map((s)=>s.trim()).filter((s)=>s!=="")};const claimsOf=(tok)=>{const p=tok.split(".")[1];if(!p)throw new Error("the forge\u0027s OIDC token is not a JWT");const c=JSON.parse(Buffer.from(p.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"));if(!c||typeof c!=="object")throw new Error("the forge\u0027s OIDC token has no claims");return c};async function seenOf(){if(x.forge==="gitlab")return{forgeEnvironment:E.CI_ENVIRONMENT_NAME||null,ref:E.CI_COMMIT_REF_NAME,refProtected:E.CI_COMMIT_REF_PROTECTED,runnerTags:tags(E.CI_RUNNER_TAGS)};const s={ref:E.GITHUB_REF};let c;if(x.forge==="github"&&x.forgeEnvironment!==undefined){try{if(!E.ACTIONS_ID_TOKEN_REQUEST_URL||!E.ACTIONS_ID_TOKEN_REQUEST_TOKEN)throw new Error("the job may request no OIDC token (no ACTIONS_ID_TOKEN_REQUEST_URL); it needs the id-token: write permission");const r=await fetch(E.ACTIONS_ID_TOKEN_REQUEST_URL+"&audience="+encodeURIComponent(A),{headers:{authorization:"bearer "+E.ACTIONS_ID_TOKEN_REQUEST_TOKEN}});if(!r.ok)throw new Error("the OIDC token request answered "+r.status);const v=(await r.json()).value;if(typeof v!=="string")throw new Error("the OIDC token request answered no token");c=claimsOf(v);s.forgeEnvironment=typeof c.environment==="string"&&c.environment!==""?c.environment:null}catch(e){s.forgeEnvironmentError=e&&e.message?e.message:String(e)}}const run=c&&typeof c.runner_environment==="string"?c.runner_environment:E.RUNNER_ENVIRONMENT;if(run)s.runnerEnvironment=run;return s}seenOf().then((s)=>{const P=[],C=[],S=[];const fe=x.forgeEnvironment;if(x.forge==="forgejo")S.push("forge environment (Forgejo has none)");else if(fe!==undefined){if(s.forgeEnvironmentError!==undefined)P.push("forge environment: expected "+fe+", and it could not be read: "+s.forgeEnvironmentError);else if(s.forgeEnvironment!==fe)P.push("forge environment: expected "+fe+", found "+shown(s.forgeEnvironment));else C.push("forge environment "+fe)}const b=x.branches.filter((v)=>v!=="");if(b.length===0)P.push("ref: the job names no branch to run from");else if(x.forge==="gitlab"){if(!s.ref||!b.includes(s.ref))P.push("ref: expected "+b.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref);if(s.refProtected!=="true")P.push("ref: expected a protected ref (CI_COMMIT_REF_PROTECTED true), found "+shown(s.refProtected));else C.push("protected ref")}else{const refs=b.map((v)=>"refs/heads/"+v);if(!s.ref||!refs.includes(s.ref))P.push("ref: expected "+refs.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref)}if(x.forge==="gitlab"){const w=x.runnerTags||[];if(w.length>0){const h=s.runnerTags;const miss=w.filter((t)=>!(h||[]).includes(t));if(h===undefined)P.push("runner: expected tags "+w.join(", ")+", and the job has no CI_RUNNER_TAGS");else if(miss.length>0)P.push("runner: expected tags "+w.join(", ")+", found "+(h.length?h.join(", "):"none"));else C.push("runner tags "+w.join(", "))}}else if(x.selfHosted){if(s.runnerEnvironment===undefined)S.push("runner (the runner says nothing about itself: no RUNNER_ENVIRONMENT)");else if(s.runnerEnvironment!=="self-hosted")P.push("runner: expected self-hosted, found "+s.runnerEnvironment);else C.push("self-hosted runner")}const at=(x.product||"chant")+" check-job: "+x.job;if(P.length>0){for(const p of P)console.error(at+": "+p);console.error(at+": this job does not run where the spec set it to run, so it stops before any credential is taken or code checked out");process.exit(9)}console.log(at+": "+(C.length?C.join(", "):"nothing to check")+(S.length?"; not checked: "+S.join(", "):""))}).catch((e)=>fail(e&&e.message?e.message:String(e)));'')'
|
|
73
85
|
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
74
86
|
with:
|
|
75
87
|
fetch-depth: 0
|
|
@@ -97,6 +109,10 @@ jobs:
|
|
|
97
109
|
- wave-2-prod-decide
|
|
98
110
|
container: node:22
|
|
99
111
|
steps:
|
|
112
|
+
- name: Check the job runs where the spec set it to run
|
|
113
|
+
env:
|
|
114
|
+
CHANT_JOB_GUARD: '{"forge":"forgejo","job":"job group eu of wave 2 (prod) of migrations","branches":["main"]}'
|
|
115
|
+
run: '(cd / && node -e ''"use strict";const E=process.env,A="chant-check-job";const shown=(v)=>v===undefined||v===null||v===""?"none":v;const fail=(m)=>{console.error("check-job: "+m);process.exit(9)};let x;try{x=JSON.parse(E.CHANT_JOB_GUARD||"")}catch(e){fail("CHANT_JOB_GUARD is not JSON")}if(!x||!["github","gitlab","forgejo"].includes(x.forge)||!Array.isArray(x.branches))fail("CHANT_JOB_GUARD is not a guard spec");const tags=(r)=>{if(r===undefined)return undefined;const t=r.trim();if(t.startsWith("[")){try{const v=JSON.parse(t);if(Array.isArray(v))return v.map(String)}catch(e){}}return t===""?[]:t.split(",").map((s)=>s.trim()).filter((s)=>s!=="")};const claimsOf=(tok)=>{const p=tok.split(".")[1];if(!p)throw new Error("the forge\u0027s OIDC token is not a JWT");const c=JSON.parse(Buffer.from(p.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"));if(!c||typeof c!=="object")throw new Error("the forge\u0027s OIDC token has no claims");return c};async function seenOf(){if(x.forge==="gitlab")return{forgeEnvironment:E.CI_ENVIRONMENT_NAME||null,ref:E.CI_COMMIT_REF_NAME,refProtected:E.CI_COMMIT_REF_PROTECTED,runnerTags:tags(E.CI_RUNNER_TAGS)};const s={ref:E.GITHUB_REF};let c;if(x.forge==="github"&&x.forgeEnvironment!==undefined){try{if(!E.ACTIONS_ID_TOKEN_REQUEST_URL||!E.ACTIONS_ID_TOKEN_REQUEST_TOKEN)throw new Error("the job may request no OIDC token (no ACTIONS_ID_TOKEN_REQUEST_URL); it needs the id-token: write permission");const r=await fetch(E.ACTIONS_ID_TOKEN_REQUEST_URL+"&audience="+encodeURIComponent(A),{headers:{authorization:"bearer "+E.ACTIONS_ID_TOKEN_REQUEST_TOKEN}});if(!r.ok)throw new Error("the OIDC token request answered "+r.status);const v=(await r.json()).value;if(typeof v!=="string")throw new Error("the OIDC token request answered no token");c=claimsOf(v);s.forgeEnvironment=typeof c.environment==="string"&&c.environment!==""?c.environment:null}catch(e){s.forgeEnvironmentError=e&&e.message?e.message:String(e)}}const run=c&&typeof c.runner_environment==="string"?c.runner_environment:E.RUNNER_ENVIRONMENT;if(run)s.runnerEnvironment=run;return s}seenOf().then((s)=>{const P=[],C=[],S=[];const fe=x.forgeEnvironment;if(x.forge==="forgejo")S.push("forge environment (Forgejo has none)");else if(fe!==undefined){if(s.forgeEnvironmentError!==undefined)P.push("forge environment: expected "+fe+", and it could not be read: "+s.forgeEnvironmentError);else if(s.forgeEnvironment!==fe)P.push("forge environment: expected "+fe+", found "+shown(s.forgeEnvironment));else C.push("forge environment "+fe)}const b=x.branches.filter((v)=>v!=="");if(b.length===0)P.push("ref: the job names no branch to run from");else if(x.forge==="gitlab"){if(!s.ref||!b.includes(s.ref))P.push("ref: expected "+b.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref);if(s.refProtected!=="true")P.push("ref: expected a protected ref (CI_COMMIT_REF_PROTECTED true), found "+shown(s.refProtected));else C.push("protected ref")}else{const refs=b.map((v)=>"refs/heads/"+v);if(!s.ref||!refs.includes(s.ref))P.push("ref: expected "+refs.join(" or ")+", found "+shown(s.ref));else C.push("ref "+s.ref)}if(x.forge==="gitlab"){const w=x.runnerTags||[];if(w.length>0){const h=s.runnerTags;const miss=w.filter((t)=>!(h||[]).includes(t));if(h===undefined)P.push("runner: expected tags "+w.join(", ")+", and the job has no CI_RUNNER_TAGS");else if(miss.length>0)P.push("runner: expected tags "+w.join(", ")+", found "+(h.length?h.join(", "):"none"));else C.push("runner tags "+w.join(", "))}}else if(x.selfHosted){if(s.runnerEnvironment===undefined)S.push("runner (the runner says nothing about itself: no RUNNER_ENVIRONMENT)");else if(s.runnerEnvironment!=="self-hosted")P.push("runner: expected self-hosted, found "+s.runnerEnvironment);else C.push("self-hosted runner")}const at=(x.product||"chant")+" check-job: "+x.job;if(P.length>0){for(const p of P)console.error(at+": "+p);console.error(at+": this job does not run where the spec set it to run, so it stops before any credential is taken or code checked out");process.exit(9)}console.log(at+": "+(C.length?C.join(", "):"nothing to check")+(S.length?"; not checked: "+S.join(", "):""))}).catch((e)=>fail(e&&e.message?e.message:String(e)));'')'
|
|
100
116
|
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
101
117
|
with:
|
|
102
118
|
fetch-depth: 0
|
|
@@ -77,5 +77,6 @@ export function forgejoWorkflowDoc(doc: GithubOpPipelineDoc, dialect: ForgejoDia
|
|
|
77
77
|
jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialect),
|
|
78
78
|
...(doc.gatedNoticeDoc ? { gatedNoticeDoc: forgejoize(doc.gatedNoticeDoc, dialect) } : {}),
|
|
79
79
|
...(doc.reviewNoteDoc ? { reviewNoteDoc: forgejoize(doc.reviewNoteDoc, dialect) } : {}),
|
|
80
|
+
...(doc.agentDoc ? { agentDoc: forgejoize(doc.agentDoc, dialect) } : {}),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control repo's own workflow (#4568) on Forgejo: github's document
|
|
3
|
+
* (`controlGithubDoc`) with the Forgejo dialect applied, the way
|
|
4
|
+
* ./generate-pipeline.ts reuses github's pipeline. Forgejo has no
|
|
5
|
+
* `repository_dispatch`, so a member's dispatch is a workflow dispatch of
|
|
6
|
+
* this file by name.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
controlGithubDoc,
|
|
11
|
+
type ControlPipelineFile,
|
|
12
|
+
type ControlPipelineOptions,
|
|
13
|
+
} from "@intentius/chant-lexicon-github/components/control-pipeline";
|
|
14
|
+
import { emitWorkflowYAML } from "@intentius/chant-lexicon-github/components/ci-jobs";
|
|
15
|
+
import type { ForgejoDialectOptions } from "../dialect";
|
|
16
|
+
import { forgejoize } from "./ci-jobs";
|
|
17
|
+
|
|
18
|
+
/** The control repo's workflow on Forgejo: `.forgejo/workflows/<product>-dependents.yml`. */
|
|
19
|
+
export function generateControlPipeline(options: ControlPipelineOptions = {}, dialect: ForgejoDialectOptions = {}): ControlPipelineFile {
|
|
20
|
+
const { doc, path, notes } = controlGithubDoc("forgejo", options);
|
|
21
|
+
const content = emitWorkflowYAML({
|
|
22
|
+
...doc,
|
|
23
|
+
on: forgejoize(doc.on, dialect),
|
|
24
|
+
jobsDoc: forgejoize(doc.jobsDoc, dialect),
|
|
25
|
+
});
|
|
26
|
+
return { path, content, notes };
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent jobs on Forgejo (#4616): github's jobs in the Forgejo dialect,
|
|
3
|
+
* with the commenter's access asked of the forge, and a runner a label.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
8
|
+
import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
9
|
+
import { generateForgejoOpPipeline } from "./generate-op-pipeline";
|
|
10
|
+
|
|
11
|
+
const doc = (spec: ScheduledOpSpec) => parseYAML(generateForgejoOpPipeline([spec]).files[0]!.yaml) as unknown as { jobs: Record<string, { if?: string; "runs-on"?: unknown; steps: Array<{ run?: string; env?: Record<string, string> }> }> };
|
|
12
|
+
|
|
13
|
+
describe("the comment agent on Forgejo", () => {
|
|
14
|
+
const d = doc({ name: "chant-agent", runner: "agents", agent: { on: "comment", comment: "/chant agent" } });
|
|
15
|
+
|
|
16
|
+
test("answers a comment on a pull request with no author association to read", () => {
|
|
17
|
+
const agent = d.jobs["chant-agent"]!;
|
|
18
|
+
expect(agent.if).toContain("github.event.is_pull");
|
|
19
|
+
expect(agent.if).not.toContain("author_association");
|
|
20
|
+
expect(agent["runs-on"]).toBe("agents");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("the push job hands the forge client its token", () => {
|
|
24
|
+
const land = d.jobs["chant-agent-push"]!.steps.at(-1)!;
|
|
25
|
+
expect(land.run).toContain("chant agent land --forge forgejo");
|
|
26
|
+
expect(land.env?.CHANT_FORGEJO_TOKEN).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("refuses a runner group, which only GitHub has", () => {
|
|
30
|
+
expect(() => doc({ name: "drift-app", trigger: { kind: "cron", schedule: "0 6 * * *" }, runner: { group: "infra" } })).toThrow(/runner group/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// #4619: on Forgejo too, a member's drift Op hands its own output on, and the agent job takes it at the root.
|
|
35
|
+
describe("the drift agent of a workspace member on Forgejo", () => {
|
|
36
|
+
const out = generateForgejoOpPipeline([{ name: "drift-app", trigger: { kind: "cron", schedule: "0 6 * * *" }, agent: { on: "drift" } }], { member: { name: "app", dir: "stacks/app" } });
|
|
37
|
+
const d = parseYAML(out.files[0]!.yaml) as unknown as { jobs: Record<string, { defaults?: { run?: { "working-directory"?: string } }; steps: Array<{ name?: string; run?: string }> }> };
|
|
38
|
+
|
|
39
|
+
test("the drift check runs in the member's directory, and the agent job reads what it handed on", () => {
|
|
40
|
+
expect(d.jobs["drift-app"]!.defaults?.run?.["working-directory"]).toBe("stacks/app");
|
|
41
|
+
const agent = d.jobs["drift-app-agent"]!;
|
|
42
|
+
expect(agent.defaults).toBeUndefined();
|
|
43
|
+
expect(agent.steps.find((s) => s.name === "Take the drift from the drift check")!.run).toContain(" .chant/agent <<");
|
|
44
|
+
expect(agent.steps.some((s) => s.run?.includes("chant agent prepare --forge forgejo --output .chant/agent"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** #4631: the Forgejo scheduled Op job trades for its cloud identities first, and an update on drift runs in its own job. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import type { ScheduledOpSpec } from "@intentius/chant/lexicon";
|
|
4
|
+
import { generateForgejoOpPipeline } from "./generate-op-pipeline";
|
|
5
|
+
|
|
6
|
+
const PREVIEW = { aws: { role: "arn:aws:iam::1:role/preview", region: "us-east-1", audience: "sts.amazonaws.com" } };
|
|
7
|
+
const UPDATE = { aws: { role: "arn:aws:iam::1:role/update", region: "us-east-1", audience: "sts.amazonaws.com" } };
|
|
8
|
+
|
|
9
|
+
describe("cloud identities on a Forgejo scheduled Op (#4631)", () => {
|
|
10
|
+
test("the trade, id-token: write on the job, and the update job", () => {
|
|
11
|
+
const spec: ScheduledOpSpec = { name: "drift-dev", trigger: { kind: "cron", schedule: "0 6 * * *" }, findingMode: "issue", cloud: PREVIEW, update: { command: "plywood up", cloud: UPDATE } };
|
|
12
|
+
const yaml = generateForgejoOpPipeline([spec]).files[0]!.yaml;
|
|
13
|
+
expect(yaml).toContain("Trade the job's OIDC token for its cloud identities");
|
|
14
|
+
expect(yaml).toContain("id-token: write");
|
|
15
|
+
expect(yaml).toContain("drift-dev-update:");
|
|
16
|
+
expect(yaml).toContain("needs.drift-dev.outputs.drift == 'true'");
|
|
17
|
+
expect(yaml).toContain("role/update");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("an update with contents: write renders, and the refusal of contents: read holds (2026-10-28 ruling)", () => {
|
|
21
|
+
const spec: ScheduledOpSpec = { name: "drift-dev", trigger: { kind: "cron", schedule: "0 6 * * *" }, findingMode: "issue", cloud: PREVIEW, update: { command: "plywood up", cloud: UPDATE, permissions: { contents: "write" } } };
|
|
22
|
+
expect(generateForgejoOpPipeline([spec]).files[0]!.yaml).toContain("drift-dev-update:");
|
|
23
|
+
expect(() => generateForgejoOpPipeline([{ ...spec, update: { command: "plywood up", permissions: { contents: "read" } } }])).toThrow(/contents may only be raised to write/);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -95,6 +95,7 @@ import type {
|
|
|
95
95
|
OpPipelineResult,
|
|
96
96
|
ScheduledOpSpec,
|
|
97
97
|
} from "@intentius/chant/lexicon";
|
|
98
|
+
import { assertRunnerLabels } from "@intentius/chant/ci-jobs";
|
|
98
99
|
import type { ForgejoDialectOptions } from "../dialect";
|
|
99
100
|
import { droppedEnvironmentHeader, forgejoWorkflowDoc } from "./ci-jobs";
|
|
100
101
|
|
|
@@ -116,7 +117,9 @@ export function generateForgejoOpPipeline(
|
|
|
116
117
|
// `findingMode: "issue"` from #2332 to #2351. Neither is now: both post
|
|
117
118
|
// through the Forgejo client — see the module doc above.
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
// #4616: a spec's runner is a label or a list of labels here.
|
|
121
|
+
for (const spec of ops) assertRunnerLabels(`Scheduled Op "${spec.name}"`, "runner", spec.runner, "Forgejo");
|
|
122
|
+
const { files, jobs } = buildGithubOpPipelineDocs(ops, options, "forgejo");
|
|
120
123
|
|
|
121
124
|
return {
|
|
122
125
|
// One file per spec, in spec order, which is what lets the header below
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** #4630: the Forgejo plans workflow posts the approval status with `--forge forgejo`. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
4
|
+
import { generateForgejoOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
5
|
+
|
|
6
|
+
const spec: OpWavesSpec = {
|
|
7
|
+
name: "plywood-up",
|
|
8
|
+
op: "plywood-up",
|
|
9
|
+
plan: ["./bin/plan", "{target}", "{plan}"],
|
|
10
|
+
waves: [{ name: "prod", runs: [{ target: "app" }], approval: "pr-review" }],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("the approval status job on Forgejo (#4630)", () => {
|
|
14
|
+
test("runs on reviews and after the plans with --forge forgejo", () => {
|
|
15
|
+
const plans = generateForgejoOpWavesPipeline(spec, { specFile: "waves.json" }).files.find((f) => f.name === "plywood-up-plans.yml")!.yaml;
|
|
16
|
+
expect(plans).toContain("pull_request_review:");
|
|
17
|
+
expect(plans).toContain('run wave --spec waves.json --approval-status --pr "$PR_NUMBER" --head "$HEAD_SHA" --base "$BASE_SHA" --forge forgejo');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a pull-request wave gets the plans workflow too (2026-10-28 ruling)", () => {
|
|
21
|
+
const pullRequest: OpWavesSpec = { ...spec, waves: [{ name: "prod", runs: [{ target: "app" }], approval: "pull-request" }], approvals: { pullRequest: { gate: "up" } } };
|
|
22
|
+
const plans = generateForgejoOpWavesPipeline(pullRequest, { specFile: "waves.json" }).files.find((f) => f.name === "plywood-up-plans.yml")!.yaml;
|
|
23
|
+
expect(plans).toContain("run wave --spec waves.json --record-plans");
|
|
24
|
+
expect(plans).toContain("--approval-status --pr");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Op waves spec's `control` on Forgejo (#4617): github's control job
|
|
3
|
+
* under the dialect, dispatching the control repo's dependents workflow by
|
|
4
|
+
* name with the outcome as an input. Written for the ci-products session;
|
|
5
|
+
* run at epic-ready.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
10
|
+
import { generateForgejoOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
11
|
+
|
|
12
|
+
const spec: OpWavesSpec = {
|
|
13
|
+
name: "stacks",
|
|
14
|
+
op: "pulumi-up",
|
|
15
|
+
plan: ["./bin/plan", "{target}", "{plan}"],
|
|
16
|
+
waves: [{ name: "all", runs: [{ target: "dev" }, { target: "prod" }] }],
|
|
17
|
+
control: { repo: "forgejo.example.com/acme/control", op: "deploy", member: "network", ref: "trunk" },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const options = { specFile: "waves.json", bin: "npx --no-install plywood", words: { product: "plywood" } };
|
|
21
|
+
|
|
22
|
+
describe("the control dispatch after the last wave on Forgejo (#4617)", () => {
|
|
23
|
+
test("a workflow dispatch of the control repo's dependents file, the outcome among its inputs", () => {
|
|
24
|
+
const yaml = generateForgejoOpWavesPipeline(spec, options).files[0]!.yaml;
|
|
25
|
+
expect(yaml).toMatch(/control-dispatch:[\s\S]*needs:\n\s+- wave-1-all\n\s+if: '!cancelled\(\)'/);
|
|
26
|
+
expect(yaml).toContain("https://forgejo.example.com/api/v1/repos/acme/control/actions/workflows/plywood-dependents.yml/dispatches");
|
|
27
|
+
expect(yaml).toContain(`{"ref":"trunk","inputs":{"member":"'"$member"'","op":"'"$op"'","sha":"'"$sha"'","outcome":"'"$outcome"'"}}`);
|
|
28
|
+
expect(yaml).toContain("Authorization: token %s");
|
|
29
|
+
expect(yaml).toContain("CHANT_CONTROL_TOKEN: '${{ secrets.CHANT_CONTROL_TOKEN }}'");
|
|
30
|
+
expect(yaml).toContain("npx --no-install plywood run wave --spec waves.json --waves-outcome");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("the dialect drops permissions and rewrites the download action", () => {
|
|
34
|
+
const yaml = generateForgejoOpWavesPipeline(spec, options).files[0]!.yaml;
|
|
35
|
+
expect(yaml).not.toContain("permissions:");
|
|
36
|
+
expect(yaml).toContain("name: Download the waves' outcomes");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The apply-job guard in Forgejo op-wave jobs (#4624): the GitHub step with
|
|
3
|
+
* the forge set to Forgejo, which checks the ref and runner and no
|
|
4
|
+
* environment. Written for the ci-products post-merge pass.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
9
|
+
import { JOB_GUARD_ENV } from "@intentius/chant/ci-job-guard";
|
|
10
|
+
import { generateForgejoOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
11
|
+
|
|
12
|
+
const spec: OpWavesSpec = {
|
|
13
|
+
name: "deploy",
|
|
14
|
+
op: "pulumi-up",
|
|
15
|
+
plan: ["./bin/plan", "{target}", "{plan}"],
|
|
16
|
+
runner: ["self-hosted", "docker"],
|
|
17
|
+
waves: [{ name: "prod", runs: [{ target: "prod" }], environment: { name: "production" }, idTokens: { CLOUD_TOKEN: "aud" } }],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("the apply-job guard on Forgejo (#4624)", () => {
|
|
21
|
+
test("the first step of the wave job, with the forge set to Forgejo and no environment", () => {
|
|
22
|
+
const yaml = generateForgejoOpWavesPipeline(spec, { specFile: "waves.json" }).files[0]!.yaml;
|
|
23
|
+
const steps = yaml.slice(yaml.indexOf("steps:"));
|
|
24
|
+
expect(steps.indexOf("Check the job runs where the spec set it to run")).toBeLessThan(steps.indexOf("checkout"));
|
|
25
|
+
expect(yaml).toContain(JOB_GUARD_ENV);
|
|
26
|
+
const json = /CHANT_JOB_GUARD: '(\{.*?\})'/.exec(yaml)?.[1] ?? /CHANT_JOB_GUARD: "(.*?)"\n/.exec(yaml)?.[1]?.replace(/\\"/g, '"');
|
|
27
|
+
expect(json).toBeDefined();
|
|
28
|
+
const guard = JSON.parse(json!) as Record<string, unknown>;
|
|
29
|
+
expect(guard).toEqual({ forge: "forgejo", job: "wave 1 (prod) of deploy", branches: ["main"], selfHosted: true });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -115,8 +115,26 @@ describe("an Op wave's jobs by cloud identity (#4332)", () => {
|
|
|
115
115
|
expect(Object.keys(jobs)).toEqual(["wave-1-prod-decide", "wave-1-prod-a", "wave-1-prod-default"]);
|
|
116
116
|
for (const job of Object.values(jobs)) {
|
|
117
117
|
expect(job.permissions).toEqual({ "id-token": "write" });
|
|
118
|
-
|
|
119
|
-
expect(job.steps[1]!.
|
|
118
|
+
// #4624: the apply-job guard comes first, then the trade, then the checkout.
|
|
119
|
+
expect(job.steps[1]!.env![TRADE_SPEC_ENV]).toBeDefined();
|
|
120
|
+
expect(job.steps[2]!.uses).toContain("checkout");
|
|
120
121
|
}
|
|
121
122
|
});
|
|
123
|
+
|
|
124
|
+
test("words.bin is the command in every job line: the waves, resume and the plans job (#4607)", () => {
|
|
125
|
+
const bin = "npx --no-install plywood";
|
|
126
|
+
const withBin = { ...spec, resume: { schedule: "*/10 * * * *" }, waves: spec.waves.map((w, i) => (i === spec.waves.length - 1 ? { ...w, approval: "pr-review" as const } : w)) };
|
|
127
|
+
const { files, jobs } = generateForgejoOpWavesPipeline(withBin, { specFile: "waves.json", bin });
|
|
128
|
+
expect(jobs.every((j) => j.command.slice(0, 3).join(" ") === bin)).toBe(true);
|
|
129
|
+
const yaml = files.map((f) => f.yaml).join("\n");
|
|
130
|
+
expect(yaml).toContain("run: npx --no-install plywood run wave --spec waves.json --wave 1");
|
|
131
|
+
expect(yaml).toContain("npx --no-install plywood run resume --op migrations");
|
|
132
|
+
expect(yaml).toContain("npx --no-install plywood run wave --spec waves.json --record-plans");
|
|
133
|
+
expect(yaml).not.toMatch(/(^|\s)chant run (wave|resume)/m);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("no bin keeps chant (#4607)", () => {
|
|
137
|
+
const { jobs } = generateForgejoOpWavesPipeline(spec, { specFile: "waves.json" });
|
|
138
|
+
expect(jobs.every((j) => j.command[0] === "chant")).toBe(true);
|
|
139
|
+
});
|
|
122
140
|
});
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* so it dispatches the waves workflow again on its branch, with the
|
|
13
13
|
* `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
|
|
14
14
|
*
|
|
15
|
-
* A `pr-review` wave (#3684)
|
|
16
|
-
* GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
15
|
+
* A `pr-review` wave (#3684), or a `pull-request` wave (#4630), gets
|
|
16
|
+
* `<name>-plans.yml` on pull requests, as on GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
17
17
|
*
|
|
18
18
|
* `runner` and a wave's `jobs` (#3914) render as on GitHub, with two limits:
|
|
19
19
|
* a runner is a label or a list of labels (a GitHub runner group is a spec
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* none to put in front of it. A job with id tokens keeps `permissions:
|
|
22
22
|
* id-token: write`, the one scope the dialect leaves, and reads each audience
|
|
23
23
|
* from `<NAME>_AUDIENCE` when it requests its token.
|
|
24
|
+
*
|
|
25
|
+
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
|
+
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
|
+
* the runner, and no forge environment, since Forgejo has none.
|
|
24
28
|
*/
|
|
25
29
|
|
|
26
30
|
import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
|
|
@@ -38,7 +42,7 @@ export function generateForgejoOpWavesPipeline(
|
|
|
38
42
|
dialectOptions: ForgejoDialectOptions = {},
|
|
39
43
|
): OpWavesPipelineResult {
|
|
40
44
|
assertForgejoWaves(spec);
|
|
41
|
-
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
|
|
45
|
+
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options, { forge: "forgejo" });
|
|
42
46
|
const dropped = spec.waves.filter((w) => w.environment).map((w) => `${w.name} -> ${w.environment!.name}`);
|
|
43
47
|
const header = dropped.length > 0 ? droppedWaveEnvironmentsHeader(dropped) : undefined;
|
|
44
48
|
const files = [{ name: `${spec.name}.yml`, yaml: emitOpPipelineYAML(forgejoWorkflowDoc(doc, dialectOptions, header)) }];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `/<product> lock` and `/<product> unlock` on Forgejo (#4588): the GitHub generator's comment jobs, reading Forgejo's `is_pull`, with no author check. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
5
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
6
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
7
|
+
|
|
8
|
+
const ESTATE: DriverComponent[] = [
|
|
9
|
+
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const WORDS = { product: "plywood", commands: { lock: "lock" as const, unlock: "unlock" as const } };
|
|
13
|
+
|
|
14
|
+
type Doc = { jobs: Record<string, { if?: string; steps: Array<{ run?: string }> }> };
|
|
15
|
+
|
|
16
|
+
describe("the Forgejo workflow's lock and unlock comments", () => {
|
|
17
|
+
const doc = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { locks: "plan", words: WORDS } }).yaml) as Doc;
|
|
18
|
+
|
|
19
|
+
test("each comment job reads is_pull and runs on forgejo", () => {
|
|
20
|
+
expect(doc.jobs["comment-lock"]!.if).toContain("github.event.is_pull");
|
|
21
|
+
expect(doc.jobs["comment-lock"]!.if).not.toContain("author_association");
|
|
22
|
+
expect(doc.jobs["comment-lock"]!.steps.some((s) => s.run === 'chant lock --keep --pr "$PR_NUMBER" --forge forgejo')).toBe(true);
|
|
23
|
+
expect(doc.jobs["comment-unlock"]!.steps.some((s) => s.run === 'chant unlock --pr "$PR_NUMBER"')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The preview comment on Forgejo (#4620): the GitHub generator's jobs, reading Forgejo's `is_pull`, with no author check. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
5
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
6
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
7
|
+
|
|
8
|
+
const ESTATE: DriverComponent[] = [
|
|
9
|
+
{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const } };
|
|
13
|
+
|
|
14
|
+
type Doc = { jobs: Record<string, { if?: string; permissions?: Record<string, string>; steps: Array<{ run?: string }> }> };
|
|
15
|
+
|
|
16
|
+
describe("the Forgejo workflow's preview comment", () => {
|
|
17
|
+
const doc = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { previewComment: true, words: WORDS } }).yaml) as Doc;
|
|
18
|
+
|
|
19
|
+
test("the preview job reads is_pull, drops the tokens before the change's code runs, and the note job posts on forgejo", () => {
|
|
20
|
+
const preview = doc.jobs["comment-preview"]!;
|
|
21
|
+
expect(preview.if).toContain("github.event.is_pull");
|
|
22
|
+
expect(preview.if).not.toContain("author_association");
|
|
23
|
+
// The Forgejo runner ignores `permissions`, so the dialect drops it (bar id-token), as for every job.
|
|
24
|
+
expect(preview.permissions).toBeUndefined();
|
|
25
|
+
const drop = preview.steps.findIndex((s) => s.run?.includes("-u CHANT_FORGE_TOKEN") ?? false);
|
|
26
|
+
const plan = preview.steps.findIndex((s) => s.run?.includes("pr-plan") ?? false);
|
|
27
|
+
expect(drop).toBeGreaterThan(0);
|
|
28
|
+
expect(plan).toBeGreaterThan(drop);
|
|
29
|
+
expect(doc.jobs["comment-preview-note"]!.steps.at(-1)!.run).toBe('chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preview comment's --target on Forgejo (#4626): the GitHub generator's
|
|
3
|
+
* preview step, with the comment body in CHANT_PREVIEW_COMMENT. Written for
|
|
4
|
+
* the ci-products post-merge pass.
|
|
5
|
+
*/
|
|
6
|
+
import { expect, test } from "vitest";
|
|
7
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
8
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
9
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
10
|
+
|
|
11
|
+
const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
|
|
12
|
+
const WORDS = { product: "plywood", stages: { plan: "preview", apply: "up" }, commands: { preview: "plan" as const } };
|
|
13
|
+
|
|
14
|
+
test("the preview step reads the comment from a variable (#4626)", () => {
|
|
15
|
+
const doc = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { previewComment: true, words: WORDS } }).yaml) as { jobs: Record<string, { steps: Array<{ run?: string; env?: Record<string, string> }> }> };
|
|
16
|
+
const step = doc.jobs["comment-preview"]!.steps.find((s) => s.run?.includes("--comment-targets"))!;
|
|
17
|
+
expect(step.env!.CHANT_PREVIEW_COMMENT).toBe("${{ github.event.comment.body }}");
|
|
18
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4619: with a control repo, the Forgejo pull-request workflow's plan-note
|
|
3
|
+
* job fetches the control repo's chant/lifecycle, read-only, and sets
|
|
4
|
+
* CHANT_CONTROL_DIR before it posts, through the Forgejo dialect. Generated
|
|
5
|
+
* YAML only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
10
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
11
|
+
import { resolveControlTarget } from "@intentius/chant/control/dispatch";
|
|
12
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
13
|
+
|
|
14
|
+
const root = (name: string, dependsOn: string[] = []): DriverComponent => ({ name, dependsOn, deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] });
|
|
15
|
+
const ESTATE: DriverComponent[] = [root("net"), root("app", ["net"])];
|
|
16
|
+
const control = { target: resolveControlTarget({ repo: "forgejo.example.com/acme/control", op: "deploy", readTokenSecret: "PLYWOOD_CONTROL_READ" }, "network") };
|
|
17
|
+
|
|
18
|
+
describe("the Forgejo plan-note job's read of the control repo (#4619)", () => {
|
|
19
|
+
test("fetches the control repo's chant/lifecycle with the read secret before it posts", () => {
|
|
20
|
+
const { yaml } = generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, control, words: { product: "plywood" } } });
|
|
21
|
+
const doc = parseYAML(yaml) as { jobs: Record<string, { steps: Array<{ run?: string; env?: Record<string, string> }> }> };
|
|
22
|
+
const steps = doc.jobs["plan-note"]!.steps;
|
|
23
|
+
const read = steps.findIndex((s) => s.run?.includes("refs/heads/chant/lifecycle"));
|
|
24
|
+
expect(read).toBeGreaterThan(-1);
|
|
25
|
+
expect(steps[read + 1]!.run).toContain("components pr-post");
|
|
26
|
+
expect(steps[read]!.env?.CHANT_CONTROL_READ_TOKEN).toBe("${{ secrets.PLYWOOD_CONTROL_READ || github.token }}");
|
|
27
|
+
expect(steps[read]!.run).toContain("'https://forgejo.example.com/acme/control.git'");
|
|
28
|
+
expect(steps[read]!.run).toContain(">>\"$GITHUB_ENV\"");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pull request loop's runners and passed secrets on Forgejo (#4625):
|
|
3
|
+
* the GitHub generator's, through the dialect. Written for the ci-products
|
|
4
|
+
* post-merge pass.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
10
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
11
|
+
|
|
12
|
+
const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
|
|
13
|
+
|
|
14
|
+
type Doc = { jobs: Record<string, { "runs-on": unknown; steps: Array<{ name?: string; env?: Record<string, string> }> }> };
|
|
15
|
+
|
|
16
|
+
describe("the Forgejo pull request loop's runners and secrets (#4625)", () => {
|
|
17
|
+
test("the plan runs on its stage's runner with the passed secret", () => {
|
|
18
|
+
const doc = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { runners: { plan: "big", default: "small" }, secrets: { DB_PASSWORD: "PROD_DB" } } }).yaml) as Doc;
|
|
19
|
+
expect(doc.jobs.plan!["runs-on"]).toBe("big");
|
|
20
|
+
expect(doc.jobs["plan-note"]!["runs-on"]).toBe("small");
|
|
21
|
+
const step = doc.jobs.plan!.steps.find((s) => s.name === "Plan the members this pull request reaches")!;
|
|
22
|
+
expect(step.env).toMatchObject({ DB_PASSWORD: "${{ secrets.PROD_DB }}" });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Stale marking on Forgejo (#4628): the GitHub generator's stale-notes job, posting on forgejo. Written for the ci-products post-merge pass. */
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
4
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
5
|
+
import { generateForgejoPipeline } from "./generate-pipeline";
|
|
6
|
+
|
|
7
|
+
const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
|
|
8
|
+
|
|
9
|
+
test("a stale-notes job runs pr-post --stale on forgejo (#4628)", () => {
|
|
10
|
+
const doc = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { staleNotes: true } }).yaml) as { jobs: Record<string, { steps: Array<{ run?: string }> }> };
|
|
11
|
+
expect(doc.jobs["stale-notes"]!.steps.at(-1)!.run).toBe('chant components pr-post --stale --tip "$TIP_SHA" --env prod --forge forgejo');
|
|
12
|
+
});
|