@intentius/chant-lexicon-forgejo 0.124.56 → 0.124.58
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.js +1 -1
- package/dist/components/generate-op-waves-pipeline.d.ts +8 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/components/generate-op-waves-pipeline.js +8 -0
- 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 +11 -4
- package/src/components/__golden__/pr-loop-action.forgejo.yml +169 -0
- package/src/components/ci-jobs.ts +1 -1
- package/src/components/generate-op-waves-pipeline.test.ts +11 -2
- package/src/components/generate-op-waves-pipeline.ts +8 -0
- package/src/components/generate-pr-pipeline.action.test.ts +30 -0
- package/src/components/generate-pr-pipeline.locks.test.ts +7 -0
|
@@ -63,7 +63,7 @@ export function forgejoWorkflowDoc(doc, dialect, header) {
|
|
|
63
63
|
...(doc.name ? { name: doc.name } : {}),
|
|
64
64
|
on: forgejoize(doc.on, dialect),
|
|
65
65
|
...(doc.env ? { env: forgejoize(doc.env, dialect) } : {}),
|
|
66
|
-
concurrency: forgejoize(doc.concurrency, dialect),
|
|
66
|
+
...(doc.concurrency ? { concurrency: forgejoize(doc.concurrency, dialect) } : {}),
|
|
67
67
|
permissions: {},
|
|
68
68
|
jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialect),
|
|
69
69
|
...(doc.gatedNoticeDoc ? { gatedNoticeDoc: forgejoize(doc.gatedNoticeDoc, dialect) } : {}),
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
26
|
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
27
|
* the runner, and no forge environment, since Forgejo has none.
|
|
28
|
+
*
|
|
29
|
+
* Like GitHub's, the workflow carries no concurrency group (ruling for
|
|
30
|
+
* ci-products 2026-10-28, "Ledger lock"; Forgejo 16.0.5 has no job-level
|
|
31
|
+
* concurrency either). Two pushes run their waves side by side: a stock
|
|
32
|
+
* Pulumi stack or a Terraform root waits for its row on chant's ledger,
|
|
33
|
+
* oldest commit first, up to the job's `timeout-minutes`, and a plunge stack
|
|
34
|
+
* on the resource grain leases its resource rows and never waits on another
|
|
35
|
+
* stack.
|
|
28
36
|
*/
|
|
29
37
|
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
30
38
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;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"}
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
26
|
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
27
|
* the runner, and no forge environment, since Forgejo has none.
|
|
28
|
+
*
|
|
29
|
+
* Like GitHub's, the workflow carries no concurrency group (ruling for
|
|
30
|
+
* ci-products 2026-10-28, "Ledger lock"; Forgejo 16.0.5 has no job-level
|
|
31
|
+
* concurrency either). Two pushes run their waves side by side: a stock
|
|
32
|
+
* Pulumi stack or a Terraform root waits for its row on chant's ledger,
|
|
33
|
+
* oldest commit first, up to the job's `timeout-minutes`, and a plunge stack
|
|
34
|
+
* on the resource grain leases its resource rows and never waits on another
|
|
35
|
+
* stack.
|
|
28
36
|
*/
|
|
29
37
|
import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
|
|
30
38
|
import { emitOpPipelineYAML } from "@intentius/chant-lexicon-github/components/generate-op-pipeline";
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "b777da2653c844ff4bbc29657ffe48e5e540412f1b6622e40ebe8d3b2f0cccec",
|
|
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": "25e5d73ecbdcf0cc5b25304bc3aa47baed7872ac3a968ba3ede47fbfac4d96d0"
|
|
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.58",
|
|
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.58",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.124.58"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"generate": "tsx src/codegen/generate-cli.ts",
|
|
@@ -6,14 +6,13 @@ on:
|
|
|
6
6
|
- main
|
|
7
7
|
workflow_dispatch: {}
|
|
8
8
|
|
|
9
|
-
concurrency:
|
|
10
|
-
group: migrations
|
|
11
|
-
cancel-in-progress: false
|
|
12
|
-
|
|
13
9
|
jobs:
|
|
14
10
|
wave-1-dev:
|
|
15
11
|
runs-on: plan
|
|
16
12
|
container: node:22
|
|
13
|
+
timeout-minutes: 180
|
|
14
|
+
env:
|
|
15
|
+
CHANT_LOCK_WAIT: '10500'
|
|
17
16
|
steps:
|
|
18
17
|
- name: Check the job runs where the spec set it to run
|
|
19
18
|
env:
|
|
@@ -37,6 +36,9 @@ jobs:
|
|
|
37
36
|
- wave-1-dev
|
|
38
37
|
if: '!cancelled()'
|
|
39
38
|
container: node:22
|
|
39
|
+
timeout-minutes: 180
|
|
40
|
+
env:
|
|
41
|
+
CHANT_LOCK_WAIT: '10500'
|
|
40
42
|
steps:
|
|
41
43
|
- name: Check the job runs where the spec set it to run
|
|
42
44
|
env:
|
|
@@ -74,7 +76,9 @@ jobs:
|
|
|
74
76
|
container: node:22
|
|
75
77
|
permissions:
|
|
76
78
|
id-token: write
|
|
79
|
+
timeout-minutes: 180
|
|
77
80
|
env:
|
|
81
|
+
CHANT_LOCK_WAIT: '10500'
|
|
78
82
|
REGION: us
|
|
79
83
|
CLOUD_TOKEN_AUDIENCE: us.example
|
|
80
84
|
steps:
|
|
@@ -108,6 +112,9 @@ jobs:
|
|
|
108
112
|
needs:
|
|
109
113
|
- wave-2-prod-decide
|
|
110
114
|
container: node:22
|
|
115
|
+
timeout-minutes: 180
|
|
116
|
+
env:
|
|
117
|
+
CHANT_LOCK_WAIT: '10500'
|
|
111
118
|
steps:
|
|
112
119
|
- name: Check the job runs where the spec set it to run
|
|
113
120
|
env:
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
name: chant-pr-prod
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
issue_comment:
|
|
11
|
+
types:
|
|
12
|
+
- created
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
stack:
|
|
16
|
+
description: The stack the action runs on
|
|
17
|
+
required: false
|
|
18
|
+
type: string
|
|
19
|
+
default: ''
|
|
20
|
+
action:
|
|
21
|
+
description: The action to run, by its name
|
|
22
|
+
required: false
|
|
23
|
+
type: string
|
|
24
|
+
default: ''
|
|
25
|
+
|
|
26
|
+
env:
|
|
27
|
+
CHANT_ENV: prod
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
plan:
|
|
31
|
+
if: github.event_name == 'pull_request'
|
|
32
|
+
runs-on: docker
|
|
33
|
+
container: node:22
|
|
34
|
+
outputs:
|
|
35
|
+
report_0: '${{ steps.plan.outputs.report_0 }}'
|
|
36
|
+
report_1: '${{ steps.plan.outputs.report_1 }}'
|
|
37
|
+
report_2: '${{ steps.plan.outputs.report_2 }}'
|
|
38
|
+
report_3: '${{ steps.plan.outputs.report_3 }}'
|
|
39
|
+
steps:
|
|
40
|
+
- name: Check out the change
|
|
41
|
+
env:
|
|
42
|
+
CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
|
|
43
|
+
run: |
|
|
44
|
+
set -eu
|
|
45
|
+
git config --global --add safe.directory "$PWD" 2>/dev/null || true
|
|
46
|
+
[ -d .git ] || git init -q .
|
|
47
|
+
git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
48
|
+
if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
|
|
49
|
+
chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
50
|
+
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
|
|
51
|
+
unset chant_auth
|
|
52
|
+
fi
|
|
53
|
+
ref="$GITHUB_REF"
|
|
54
|
+
want="$GITHUB_SHA"
|
|
55
|
+
case "$ref" in
|
|
56
|
+
refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
|
|
57
|
+
refs/tags/*) target="$ref" ;;
|
|
58
|
+
refs/*) target="refs/remotes/${ref#refs/}" ;;
|
|
59
|
+
*) target="refs/remotes/origin/chant-head" ;;
|
|
60
|
+
esac
|
|
61
|
+
if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
|
|
62
|
+
git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
|
|
63
|
+
git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
|
|
64
|
+
case "$ref" in
|
|
65
|
+
refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
|
|
66
|
+
*) git checkout -q --force --detach "$target" ;;
|
|
67
|
+
esac
|
|
68
|
+
- run: |
|
|
69
|
+
if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
|
|
70
|
+
command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
71
|
+
- name: Plan the members this pull request reaches
|
|
72
|
+
id: plan
|
|
73
|
+
env:
|
|
74
|
+
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
75
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
76
|
+
run: |
|
|
77
|
+
if [ -z "${CHANT_NO_FORGE_TOKEN:-}" ] && [ "${0#*/}" != "$0" ] && [ -f "$0" ] && ! { [ "${0##*/}" = sh ] || [ "${0##*/}" = bash ] || [ "${0##*/}" = dash ]; } && command -v env >/dev/null 2>&1 && command -v sh >/dev/null 2>&1; then exec env -u CHANT_FORGE_TOKEN -u CHANT_GITHUB_TOKEN -u CHANT_FORGEJO_TOKEN -u CHANT_GITLAB_TOKEN -u CHANT_CHECKOUT_TOKEN -u GITHUB_TOKEN -u GH_TOKEN -u FORGEJO_TOKEN -u GITEA_TOKEN -u GITLAB_TOKEN -u CI_JOB_TOKEN -u ACTIONS_RUNTIME_TOKEN CHANT_NO_FORGE_TOKEN=1 sh -e "$0"; fi; unset CHANT_FORGE_TOKEN CHANT_GITHUB_TOKEN CHANT_FORGEJO_TOKEN CHANT_GITLAB_TOKEN CHANT_CHECKOUT_TOKEN GITHUB_TOKEN GH_TOKEN FORGEJO_TOKEN GITEA_TOKEN GITLAB_TOKEN CI_JOB_TOKEN ACTIONS_RUNTIME_TOKEN
|
|
78
|
+
chant_report="$PWD/.chant/pr"
|
|
79
|
+
chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=1000000,PARTS=4,NOTE=["pr-plan.json","pr-note.md"];const all={};const walk=(d,r)=>{let n=[];try{n=fs.readdirSync(d).sort()}catch{return}for(const x of n){const f=p.join(d,x),k=r?r+"/"+x:x;let s;try{s=fs.lstatSync(f)}catch{continue}if(s.isDirectory())walk(f,k);else if(s.isFile())all[k]=fs.readFileSync(f).toString("base64")}};walk(dir,"");const pack=(o)=>z.gzipSync(JSON.stringify(o)).toString("base64");let b=pack(all);if(b.length>CHUNK*PARTS){b=pack(Object.fromEntries(Object.entries(all).filter(([k])=>NOTE.includes(k))));if(b.length>CHUNK*PARTS){b=pack({});console.error("chant: the plan report is too large to hand to the note job, even cut to the report and the note; it hands on none of it")}else console.error("chant: the plan report is too large to hand to the note job whole; it hands on pr-plan.json and pr-note.md")}let o="";for(let i=0;i*CHUNK<b.length;i++)o+="report_"+i+"="+b.slice(i*CHUNK,(i+1)*CHUNK)+"\n";fs.appendFileSync(out,o);' "$chant_report" "${GITHUB_OUTPUT:-/dev/null}" || echo "chant: the plan report could not be handed to the note job" >&2; }
|
|
80
|
+
trap chant_handover EXIT
|
|
81
|
+
npx --no-install plywood ci run plan --chant "npx --no-install plywood" --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
|
|
82
|
+
plan-note:
|
|
83
|
+
needs:
|
|
84
|
+
- plan
|
|
85
|
+
if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
|
|
86
|
+
runs-on: docker
|
|
87
|
+
container: node:22
|
|
88
|
+
steps:
|
|
89
|
+
- name: Check out the target branch's commit
|
|
90
|
+
uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
91
|
+
with:
|
|
92
|
+
ref: '${{ github.event.pull_request.base.sha }}'
|
|
93
|
+
- name: Take the plan report from the plan job
|
|
94
|
+
run: |
|
|
95
|
+
set -u
|
|
96
|
+
node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const dir=process.argv[1];let b="";for(const l of fs.readFileSync(0,"utf8").split("\n")){if(!l.trim())continue;let s="";try{s=JSON.parse(l)}catch{}if(typeof s==="string")b+=s}if(!b){console.log("chant: the plan job handed on no report");process.exit(0)}let files;try{files=JSON.parse(z.gunzipSync(Buffer.from(b,"base64"),{maxOutputLength:268435456}).toString("utf8"))}catch(e){console.log("chant: the report the plan job handed on does not read: "+e.message);process.exit(0)}if(!files||typeof files!=="object"||Array.isArray(files)){console.log("chant: the report the plan job handed on is not a set of files");process.exit(0)}let n=0;for(const [k,v] of Object.entries(files)){const parts=k.split("/");if(typeof v!=="string"||k.length>1024||parts.some((x)=>!x||x==="."||x===".."||/[\\\x00-\x1f]/.test(x))){console.log("chant: left out "+JSON.stringify(k));continue}try{const f=p.join(dir,...parts);fs.mkdirSync(p.dirname(f),{recursive:true});fs.writeFileSync(f,Buffer.from(v,"base64"));n++}catch(e){console.log("chant: could not write "+JSON.stringify(k)+": "+e.message)}}console.log("chant: took "+n+" report files from the plan job")' .chant/pr <<'END_OF_REPORT'
|
|
97
|
+
${{ toJSON(needs.plan.outputs.report_0) }}
|
|
98
|
+
${{ toJSON(needs.plan.outputs.report_1) }}
|
|
99
|
+
${{ toJSON(needs.plan.outputs.report_2) }}
|
|
100
|
+
${{ toJSON(needs.plan.outputs.report_3) }}
|
|
101
|
+
END_OF_REPORT
|
|
102
|
+
- name: Keep the plan report
|
|
103
|
+
if: always()
|
|
104
|
+
uses: https://code.forgejo.org/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
|
105
|
+
with:
|
|
106
|
+
name: chant-pr-plan
|
|
107
|
+
path: .chant/pr
|
|
108
|
+
if-no-files-found: ignore
|
|
109
|
+
- name: Post the plan note and plywood/plan
|
|
110
|
+
if: always()
|
|
111
|
+
env:
|
|
112
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
113
|
+
HEAD_SHA: '${{ github.sha }}'
|
|
114
|
+
PLAN_RESULT: '${{ needs.plan.result }}'
|
|
115
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
116
|
+
run: npx --no-install plywood components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"
|
|
117
|
+
action:
|
|
118
|
+
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.action != '') || (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/plywood action '))
|
|
119
|
+
runs-on: docker
|
|
120
|
+
container: node:22
|
|
121
|
+
concurrency:
|
|
122
|
+
group: chant-action-prod
|
|
123
|
+
cancel-in-progress: false
|
|
124
|
+
steps:
|
|
125
|
+
- name: Check out the target branch's commit
|
|
126
|
+
uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
127
|
+
with:
|
|
128
|
+
fetch-depth: 0
|
|
129
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
130
|
+
- name: Run the requested action under its gated request
|
|
131
|
+
env:
|
|
132
|
+
CHANT_ACTION_COMMENT: '${{ github.event.comment.body }}'
|
|
133
|
+
CHANT_ACTION_MEMBER: '${{ github.event.inputs.stack }}'
|
|
134
|
+
CHANT_ACTION: '${{ github.event.inputs.action }}'
|
|
135
|
+
PR_NUMBER: '${{ github.event.issue.number }}'
|
|
136
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
137
|
+
run: |
|
|
138
|
+
set +e
|
|
139
|
+
npx --no-install plywood action --from-event
|
|
140
|
+
code="$?"
|
|
141
|
+
if [ "$code" -eq 3 ]; then echo "The action waits for an approval."; exit 0; fi
|
|
142
|
+
exit "$code"
|
|
143
|
+
record:
|
|
144
|
+
if: github.event_name == 'push'
|
|
145
|
+
runs-on: docker
|
|
146
|
+
container: node:22
|
|
147
|
+
concurrency:
|
|
148
|
+
group: chant-record-prod
|
|
149
|
+
cancel-in-progress: false
|
|
150
|
+
steps:
|
|
151
|
+
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
152
|
+
with:
|
|
153
|
+
fetch-depth: 0
|
|
154
|
+
- run: command -v tofu >/dev/null 2>&1 || (curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh && sh /tmp/install-opentofu.sh --install-method standalone)
|
|
155
|
+
- name: Record the plan on the merged commit; nothing applies
|
|
156
|
+
env:
|
|
157
|
+
BASE_SHA: '${{ github.event.before }}'
|
|
158
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
159
|
+
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
160
|
+
run: |
|
|
161
|
+
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then git fetch origin "main" && BASE_SHA="$(git merge-base FETCH_HEAD "$GITHUB_SHA")"; if [ "$BASE_SHA" = "$GITHUB_SHA" ]; then BASE_SHA="$(git rev-parse --verify --quiet "$GITHUB_SHA^" || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"; fi; fi; export BASE_SHA
|
|
162
|
+
npx --no-install plywood ci run record --chant "npx --no-install plywood" --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge forgejo
|
|
163
|
+
- name: Keep the record report
|
|
164
|
+
if: always()
|
|
165
|
+
uses: https://code.forgejo.org/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
|
166
|
+
with:
|
|
167
|
+
name: chant-pr-record
|
|
168
|
+
path: .chant/pr
|
|
169
|
+
if-no-files-found: ignore
|
|
@@ -72,7 +72,7 @@ export function forgejoWorkflowDoc(doc: GithubOpPipelineDoc, dialect: ForgejoDia
|
|
|
72
72
|
...(doc.name ? { name: doc.name } : {}),
|
|
73
73
|
on: forgejoize(doc.on, dialect),
|
|
74
74
|
...(doc.env ? { env: forgejoize(doc.env, dialect) } : {}),
|
|
75
|
-
concurrency: forgejoize(doc.concurrency, dialect),
|
|
75
|
+
...(doc.concurrency ? { concurrency: forgejoize(doc.concurrency, dialect) } : {}),
|
|
76
76
|
permissions: {},
|
|
77
77
|
jobsDoc: forgejoize(withoutEnvironment(doc.jobsDoc), dialect),
|
|
78
78
|
...(doc.gatedNoticeDoc ? { gatedNoticeDoc: forgejoize(doc.gatedNoticeDoc, dialect) } : {}),
|
|
@@ -90,8 +90,8 @@ describe("a runner per wave and job groups per wave (#3914)", () => {
|
|
|
90
90
|
expect(jobs.map((j) => j.jobName)).toEqual(["wave-1-dev", "wave-2-prod-decide", "wave-2-prod-us", "wave-2-prod-eu"]);
|
|
91
91
|
const yaml = files[0]!.yaml;
|
|
92
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:/);
|
|
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+timeout-minutes: 180\n\s+env:\n\s+CHANT_LOCK_WAIT: '10500'\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+timeout-minutes: 180\n\s+env:\n\s+CHANT_LOCK_WAIT: '10500'\n\s+steps:/);
|
|
95
95
|
expect(yaml).not.toContain("contents: write");
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -138,3 +138,12 @@ describe("an Op wave's jobs by cloud identity (#4332)", () => {
|
|
|
138
138
|
expect(jobs.every((j) => j.command[0] === "chant")).toBe(true);
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("the ledger lock (ruling for ci-products 2026-10-28)", () => {
|
|
143
|
+
test("the waves workflow carries no concurrency group, workflow or job; each wave job carries its timeout and the lock wait", () => {
|
|
144
|
+
const yaml = generateForgejoOpWavesPipeline(spec, { specFile: "waves.json" }).files[0]!.yaml;
|
|
145
|
+
expect(yaml).not.toContain("concurrency:");
|
|
146
|
+
expect(yaml.match(/timeout-minutes: 180\n/g)).toHaveLength(3);
|
|
147
|
+
expect(yaml.match(/CHANT_LOCK_WAIT: '10500'\n/g)).toHaveLength(3);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* The apply-job guard (#4624) starts each wave job as on GitHub, with the
|
|
26
26
|
* forge set to Forgejo: it checks the ref and, for a `self-hosted` label,
|
|
27
27
|
* the runner, and no forge environment, since Forgejo has none.
|
|
28
|
+
*
|
|
29
|
+
* Like GitHub's, the workflow carries no concurrency group (ruling for
|
|
30
|
+
* ci-products 2026-10-28, "Ledger lock"; Forgejo 16.0.5 has no job-level
|
|
31
|
+
* concurrency either). Two pushes run their waves side by side: a stock
|
|
32
|
+
* Pulumi stack or a Terraform root waits for its row on chant's ledger,
|
|
33
|
+
* oldest commit first, up to the job's `timeout-minutes`, and a plunge stack
|
|
34
|
+
* on the resource grain leases its resource rows and never waits on another
|
|
35
|
+
* stack.
|
|
28
36
|
*/
|
|
29
37
|
|
|
30
38
|
import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The action job on Forgejo (#4654): the GitHub generator's job, reading Forgejo's payload, with no author check, and the same dispatch inputs. */
|
|
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: "shop", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "pulumi-up", stack: "shop/prod-us" }] }] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const WORDS = { product: "plywood", noun: ["stack", "stacks"] as [string, string], commands: { preview: "plan" as const, action: "action" as const }, bin: "npx --no-install plywood" };
|
|
13
|
+
|
|
14
|
+
type Doc = { on: Record<string, { inputs?: Record<string, unknown> }>; jobs: Record<string, { if?: string; steps: Array<{ run?: string }> }> };
|
|
15
|
+
|
|
16
|
+
describe("the Forgejo workflow's action job", () => {
|
|
17
|
+
const yaml = generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { words: WORDS, actionJob: true } }).yaml;
|
|
18
|
+
const doc = parseYAML(yaml) as Doc;
|
|
19
|
+
|
|
20
|
+
test("the dispatch carries the stack and action inputs, and the comment check asks no author", () => {
|
|
21
|
+
expect(Object.keys(doc.on.workflow_dispatch!.inputs!)).toEqual(["stack", "action"]);
|
|
22
|
+
expect(doc.jobs.action!.if).toContain("startsWith(github.event.comment.body, '/plywood action ')");
|
|
23
|
+
expect(doc.jobs.action!.if).not.toContain("author_association");
|
|
24
|
+
expect(doc.jobs.action!.steps.at(-1)!.run).toContain("npx --no-install plywood action --from-event");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("golden", async () => {
|
|
28
|
+
await expect(yaml).toMatchFileSnapshot("./__golden__/pr-loop-action.forgejo.yml");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -22,6 +22,13 @@ describe("the Forgejo workflow with ci.locks: plan", () => {
|
|
|
22
22
|
expect(doc.jobs.unlock!.steps.some((s) => s.run === 'chant unlock --pr "$PR_NUMBER"')).toBe(true);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("with plywood's words the lines run through its bin (plywood#22)", () => {
|
|
26
|
+
const words = { product: "plywood", bin: "npx --no-install plywood" };
|
|
27
|
+
const own = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { locks: "plan", words } }).yaml) as Doc;
|
|
28
|
+
expect(own.jobs.lock!.steps.some((s) => s.run?.startsWith("npx --no-install plywood lock --from-report ") && s.run.endsWith("--forge forgejo"))).toBe(true);
|
|
29
|
+
expect(own.jobs.unlock!.steps.some((s) => s.run === 'npx --no-install plywood unlock --pr "$PR_NUMBER"')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
25
32
|
test("without ci.locks neither job is written", () => {
|
|
26
33
|
const plain = parseYAML(generateForgejoPipeline(ESTATE, { env: "prod", prLoop: {} }).yaml) as Doc;
|
|
27
34
|
expect(Object.keys(plain.jobs)).not.toContain("lock");
|