@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.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "d8927356783df4ae21c431e0a016299273a031fecc05ead9df07f900b57ed4c7",
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": "7b0449ca0d3675a7e8c15a30fc85f3de807f6eb7eb538536867e77edd16ee133"
12
+ "composite": "25e5d73ecbdcf0cc5b25304bc3aa47baed7872ac3a968ba3ede47fbfac4d96d0"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.124.56",
3
+ "version": "0.124.58",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Forgejo",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-forgejo",
3
- "version": "0.124.56",
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.56",
58
- "@intentius/chant-lexicon-github": "^0.124.56"
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");