@intentius/chant-lexicon-forgejo 0.124.40 → 0.124.41

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "0d175904052c35f2b9b792460f3c9164c17a6a6278db9cd9221f05c2752efe8f",
4
+ "manifest.json": "a1968f12e02658d1b3745b5e3526b021f146dfa8a1b2d3efd28811e78964fc29",
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": "4cf55fe42c456f9238f1f380bbe90829c5753e19a22471fd08dee0bb360e5370"
12
+ "composite": "e9da8444b356cf4824fb1e23b30fdebd4c8d019cac5b20959f84eb2f76a6b4ba"
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgejo",
3
- "version": "0.124.40",
3
+ "version": "0.124.41",
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.40",
3
+ "version": "0.124.41",
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.40",
58
- "@intentius/chant-lexicon-github": "^0.124.40"
57
+ "@intentius/chant": "^0.124.41",
58
+ "@intentius/chant-lexicon-github": "^0.124.41"
59
59
  },
60
60
  "scripts": {
61
61
  "generate": "tsx src/codegen/generate-cli.ts",
@@ -18,22 +18,83 @@ jobs:
18
18
  if: github.event_name == 'pull_request'
19
19
  runs-on: docker
20
20
  container: node:22
21
+ outputs:
22
+ report_0: '${{ steps.plan.outputs.report_0 }}'
23
+ report_1: '${{ steps.plan.outputs.report_1 }}'
24
+ report_2: '${{ steps.plan.outputs.report_2 }}'
25
+ report_3: '${{ steps.plan.outputs.report_3 }}'
21
26
  steps:
22
- - uses: https://code.forgejo.org/actions/checkout@v7
23
- with:
24
- fetch-depth: 0
25
- - 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)
27
+ - name: Check out the change
28
+ env:
29
+ CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
30
+ run: |
31
+ set -eu
32
+ git config --global --add safe.directory "$PWD" 2>/dev/null || true
33
+ [ -d .git ] || git init -q .
34
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
35
+ if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
36
+ chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
37
+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
38
+ unset chant_auth
39
+ fi
40
+ ref="$GITHUB_REF"
41
+ want="$GITHUB_SHA"
42
+ case "$ref" in
43
+ refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
44
+ refs/tags/*) target="$ref" ;;
45
+ refs/*) target="refs/remotes/${ref#refs/}" ;;
46
+ *) target="refs/remotes/origin/chant-head" ;;
47
+ esac
48
+ if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
49
+ git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
50
+ git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
51
+ case "$ref" in
52
+ refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
53
+ *) git checkout -q --force --detach "$target" ;;
54
+ esac
55
+ - run: |
56
+ 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
57
+ 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)
26
58
  - name: Build the templates
27
- run: chant build --env prod
59
+ run: |
60
+ 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
61
+ chant build --env prod
28
62
  - name: Lint the project
29
- run: chant lint
63
+ run: |
64
+ 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
65
+ chant lint
30
66
  - name: Plan the members this pull request reaches
67
+ id: plan
31
68
  env:
32
69
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
33
70
  PR_NUMBER: '${{ github.event.pull_request.number }}'
34
- GITHUB_TOKEN: '${{ github.token }}'
35
71
  CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
36
- run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
72
+ run: |
73
+ 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
74
+ chant_report="$PWD/.chant/pr"
75
+ 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; }
76
+ trap chant_handover EXIT
77
+ chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
78
+ plan-note:
79
+ needs:
80
+ - plan
81
+ if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
82
+ runs-on: docker
83
+ container: node:22
84
+ steps:
85
+ - name: Check out the target branch's commit
86
+ uses: https://code.forgejo.org/actions/checkout@v7
87
+ with:
88
+ ref: '${{ github.event.pull_request.base.sha }}'
89
+ - name: Take the plan report from the plan job
90
+ run: |
91
+ set -u
92
+ 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'
93
+ ${{ toJSON(needs.plan.outputs.report_0) }}
94
+ ${{ toJSON(needs.plan.outputs.report_1) }}
95
+ ${{ toJSON(needs.plan.outputs.report_2) }}
96
+ ${{ toJSON(needs.plan.outputs.report_3) }}
97
+ END_OF_REPORT
37
98
  - name: Keep the plan report
38
99
  if: always()
39
100
  uses: https://code.forgejo.org/actions/upload-artifact@v3
@@ -41,6 +102,14 @@ jobs:
41
102
  name: chant-pr-plan
42
103
  path: .chant/pr
43
104
  if-no-files-found: ignore
105
+ - name: Post the plan note and chant/plan
106
+ if: always()
107
+ env:
108
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
109
+ HEAD_SHA: '${{ github.sha }}'
110
+ PLAN_RESULT: '${{ needs.plan.result }}'
111
+ GITHUB_TOKEN: '${{ github.token }}'
112
+ run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"
44
113
  record:
45
114
  if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')
46
115
  runs-on: docker
@@ -16,18 +16,75 @@ jobs:
16
16
  if: github.event_name == 'pull_request'
17
17
  runs-on: docker
18
18
  container: node:22
19
+ outputs:
20
+ report_0: '${{ steps.plan.outputs.report_0 }}'
21
+ report_1: '${{ steps.plan.outputs.report_1 }}'
22
+ report_2: '${{ steps.plan.outputs.report_2 }}'
23
+ report_3: '${{ steps.plan.outputs.report_3 }}'
19
24
  steps:
20
- - uses: https://code.forgejo.org/actions/checkout@v7
21
- with:
22
- fetch-depth: 0
23
- - 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)
25
+ - name: Check out the change
26
+ env:
27
+ CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
28
+ run: |
29
+ set -eu
30
+ git config --global --add safe.directory "$PWD" 2>/dev/null || true
31
+ [ -d .git ] || git init -q .
32
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
33
+ if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
34
+ chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
35
+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
36
+ unset chant_auth
37
+ fi
38
+ ref="$GITHUB_REF"
39
+ want="$GITHUB_SHA"
40
+ case "$ref" in
41
+ refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
42
+ refs/tags/*) target="$ref" ;;
43
+ refs/*) target="refs/remotes/${ref#refs/}" ;;
44
+ *) target="refs/remotes/origin/chant-head" ;;
45
+ esac
46
+ if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
47
+ git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
48
+ git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
49
+ case "$ref" in
50
+ refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
51
+ *) git checkout -q --force --detach "$target" ;;
52
+ esac
53
+ - run: |
54
+ 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
55
+ 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)
24
56
  - name: Plan the members this pull request reaches
57
+ id: plan
25
58
  env:
26
59
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
27
60
  PR_NUMBER: '${{ github.event.pull_request.number }}'
28
- GITHUB_TOKEN: '${{ github.token }}'
29
61
  CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
30
- run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
62
+ run: |
63
+ 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
64
+ chant_report="$PWD/.chant/pr"
65
+ 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; }
66
+ trap chant_handover EXIT
67
+ chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
68
+ plan-note:
69
+ needs:
70
+ - plan
71
+ if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
72
+ runs-on: docker
73
+ container: node:22
74
+ steps:
75
+ - name: Check out the target branch's commit
76
+ uses: https://code.forgejo.org/actions/checkout@v7
77
+ with:
78
+ ref: '${{ github.event.pull_request.base.sha }}'
79
+ - name: Take the plan report from the plan job
80
+ run: |
81
+ set -u
82
+ 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'
83
+ ${{ toJSON(needs.plan.outputs.report_0) }}
84
+ ${{ toJSON(needs.plan.outputs.report_1) }}
85
+ ${{ toJSON(needs.plan.outputs.report_2) }}
86
+ ${{ toJSON(needs.plan.outputs.report_3) }}
87
+ END_OF_REPORT
31
88
  - name: Keep the plan report
32
89
  if: always()
33
90
  uses: https://code.forgejo.org/actions/upload-artifact@v3
@@ -35,6 +92,14 @@ jobs:
35
92
  name: chant-pr-plan
36
93
  path: .chant/pr
37
94
  if-no-files-found: ignore
95
+ - name: Post the plan note and chant/plan
96
+ if: always()
97
+ env:
98
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
99
+ HEAD_SHA: '${{ github.sha }}'
100
+ PLAN_RESULT: '${{ needs.plan.result }}'
101
+ GITHUB_TOKEN: '${{ github.token }}'
102
+ run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"
38
103
  record:
39
104
  if: github.event_name == 'push'
40
105
  runs-on: docker
@@ -18,18 +18,75 @@ jobs:
18
18
  if: github.event_name == 'pull_request'
19
19
  runs-on: docker
20
20
  container: node:22
21
+ outputs:
22
+ report_0: '${{ steps.plan.outputs.report_0 }}'
23
+ report_1: '${{ steps.plan.outputs.report_1 }}'
24
+ report_2: '${{ steps.plan.outputs.report_2 }}'
25
+ report_3: '${{ steps.plan.outputs.report_3 }}'
21
26
  steps:
22
- - uses: https://code.forgejo.org/actions/checkout@v7
23
- with:
24
- fetch-depth: 0
25
- - 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)
27
+ - name: Check out the change
28
+ env:
29
+ CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
30
+ run: |
31
+ set -eu
32
+ git config --global --add safe.directory "$PWD" 2>/dev/null || true
33
+ [ -d .git ] || git init -q .
34
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
35
+ if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
36
+ chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
37
+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
38
+ unset chant_auth
39
+ fi
40
+ ref="$GITHUB_REF"
41
+ want="$GITHUB_SHA"
42
+ case "$ref" in
43
+ refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
44
+ refs/tags/*) target="$ref" ;;
45
+ refs/*) target="refs/remotes/${ref#refs/}" ;;
46
+ *) target="refs/remotes/origin/chant-head" ;;
47
+ esac
48
+ if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
49
+ git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
50
+ git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
51
+ case "$ref" in
52
+ refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
53
+ *) git checkout -q --force --detach "$target" ;;
54
+ esac
55
+ - run: |
56
+ 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
57
+ 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)
26
58
  - name: Plan the members this pull request reaches
59
+ id: plan
27
60
  env:
28
61
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
29
62
  PR_NUMBER: '${{ github.event.pull_request.number }}'
30
- GITHUB_TOKEN: '${{ github.token }}'
31
63
  CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
32
- run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo --wave-gate release
64
+ run: |
65
+ 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
66
+ chant_report="$PWD/.chant/pr"
67
+ 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; }
68
+ trap chant_handover EXIT
69
+ chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo --wave-gate release
70
+ plan-note:
71
+ needs:
72
+ - plan
73
+ if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
74
+ runs-on: docker
75
+ container: node:22
76
+ steps:
77
+ - name: Check out the target branch's commit
78
+ uses: https://code.forgejo.org/actions/checkout@v7
79
+ with:
80
+ ref: '${{ github.event.pull_request.base.sha }}'
81
+ - name: Take the plan report from the plan job
82
+ run: |
83
+ set -u
84
+ 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'
85
+ ${{ toJSON(needs.plan.outputs.report_0) }}
86
+ ${{ toJSON(needs.plan.outputs.report_1) }}
87
+ ${{ toJSON(needs.plan.outputs.report_2) }}
88
+ ${{ toJSON(needs.plan.outputs.report_3) }}
89
+ END_OF_REPORT
33
90
  - name: Keep the plan report
34
91
  if: always()
35
92
  uses: https://code.forgejo.org/actions/upload-artifact@v3
@@ -37,6 +94,14 @@ jobs:
37
94
  name: chant-pr-plan
38
95
  path: .chant/pr
39
96
  if-no-files-found: ignore
97
+ - name: Post the plan note and chant/plan
98
+ if: always()
99
+ env:
100
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
101
+ HEAD_SHA: '${{ github.sha }}'
102
+ PLAN_RESULT: '${{ needs.plan.result }}'
103
+ GITHUB_TOKEN: '${{ github.token }}'
104
+ run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"
40
105
  record:
41
106
  if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')
42
107
  runs-on: docker
@@ -107,7 +172,7 @@ jobs:
107
172
  path: .chant/pr
108
173
  if-no-files-found: ignore
109
174
  apply-wave-2:
110
- if: startsWith(github.ref, 'refs/tags/ci/green/')
175
+ if: '!cancelled() && needs.apply-wave-1.result != ''skipped'' && (startsWith(github.ref, ''refs/tags/ci/green/''))'
111
176
  runs-on: docker
112
177
  needs:
113
178
  - apply-wave-1
@@ -18,18 +18,75 @@ jobs:
18
18
  if: github.event_name == 'pull_request'
19
19
  runs-on: docker
20
20
  container: node:22
21
+ outputs:
22
+ report_0: '${{ steps.plan.outputs.report_0 }}'
23
+ report_1: '${{ steps.plan.outputs.report_1 }}'
24
+ report_2: '${{ steps.plan.outputs.report_2 }}'
25
+ report_3: '${{ steps.plan.outputs.report_3 }}'
21
26
  steps:
22
- - uses: https://code.forgejo.org/actions/checkout@v7
23
- with:
24
- fetch-depth: 0
25
- - 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)
27
+ - name: Check out the change
28
+ env:
29
+ CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
30
+ run: |
31
+ set -eu
32
+ git config --global --add safe.directory "$PWD" 2>/dev/null || true
33
+ [ -d .git ] || git init -q .
34
+ git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
35
+ if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
36
+ chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
37
+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
38
+ unset chant_auth
39
+ fi
40
+ ref="$GITHUB_REF"
41
+ want="$GITHUB_SHA"
42
+ case "$ref" in
43
+ refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
44
+ refs/tags/*) target="$ref" ;;
45
+ refs/*) target="refs/remotes/${ref#refs/}" ;;
46
+ *) target="refs/remotes/origin/chant-head" ;;
47
+ esac
48
+ if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
49
+ git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
50
+ git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
51
+ case "$ref" in
52
+ refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
53
+ *) git checkout -q --force --detach "$target" ;;
54
+ esac
55
+ - run: |
56
+ 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
57
+ 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)
26
58
  - name: Plan the members this pull request reaches
59
+ id: plan
27
60
  env:
28
61
  BASE_SHA: '${{ github.event.pull_request.base.sha }}'
29
62
  PR_NUMBER: '${{ github.event.pull_request.number }}'
30
- GITHUB_TOKEN: '${{ github.token }}'
31
63
  CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
32
- run: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
64
+ run: |
65
+ 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
66
+ chant_report="$PWD/.chant/pr"
67
+ 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; }
68
+ trap chant_handover EXIT
69
+ chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo
70
+ plan-note:
71
+ needs:
72
+ - plan
73
+ if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
74
+ runs-on: docker
75
+ container: node:22
76
+ steps:
77
+ - name: Check out the target branch's commit
78
+ uses: https://code.forgejo.org/actions/checkout@v7
79
+ with:
80
+ ref: '${{ github.event.pull_request.base.sha }}'
81
+ - name: Take the plan report from the plan job
82
+ run: |
83
+ set -u
84
+ 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'
85
+ ${{ toJSON(needs.plan.outputs.report_0) }}
86
+ ${{ toJSON(needs.plan.outputs.report_1) }}
87
+ ${{ toJSON(needs.plan.outputs.report_2) }}
88
+ ${{ toJSON(needs.plan.outputs.report_3) }}
89
+ END_OF_REPORT
33
90
  - name: Keep the plan report
34
91
  if: always()
35
92
  uses: https://code.forgejo.org/actions/upload-artifact@v3
@@ -37,6 +94,14 @@ jobs:
37
94
  name: chant-pr-plan
38
95
  path: .chant/pr
39
96
  if-no-files-found: ignore
97
+ - name: Post the plan note and chant/plan
98
+ if: always()
99
+ env:
100
+ PR_NUMBER: '${{ github.event.pull_request.number }}'
101
+ HEAD_SHA: '${{ github.sha }}'
102
+ PLAN_RESULT: '${{ needs.plan.result }}'
103
+ GITHUB_TOKEN: '${{ github.token }}'
104
+ run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge forgejo --result "$PLAN_RESULT"
40
105
  record:
41
106
  if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/')
42
107
  runs-on: docker
@@ -0,0 +1,48 @@
1
+ /**
2
+ * #4296 on Forgejo: the dialect keeps `id-token: write` on the jobs that
3
+ * trade, the plan job trades for the plan identity before its checkout, and
4
+ * the plan job names no apply identity. The steps are github's, which
5
+ * lexicons/github's identity test runs against the emulator.
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 { dropForgeTokens } from "@intentius/chant/ci-handover";
12
+ import { OIDC_REQUEST_VARS, resolveCloudIdentity, TRADE_SPEC_ENV, tradeIdentities } from "@intentius/chant/ci-identity";
13
+ import { generateForgejoPipeline } from "./generate-pipeline";
14
+
15
+ const ESTATE: DriverComponent[] = [{ name: "net", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: "net" }] }] }];
16
+
17
+ const IDENTITY = resolveCloudIdentity(
18
+ { gcp: { provider: "projects/123/locations/global/workloadIdentityPools/ci/providers/forgejo", rules: [{ plan: "plan@test.iam.gserviceaccount.com", apply: "apply@test.iam.gserviceaccount.com" }] } },
19
+ "prod",
20
+ )!;
21
+
22
+ interface Job {
23
+ permissions?: Record<string, string>;
24
+ steps: Array<{ name?: string; run?: string; env?: Record<string, string> }>;
25
+ }
26
+
27
+ describe("the generated Forgejo jobs' cloud identities (#4296)", () => {
28
+ const { yaml } = generateForgejoPipeline(ESTATE, { env: "prod", prLoop: { applyOnMerge: true, identity: IDENTITY } });
29
+ const jobs = (parseYAML(yaml) as { jobs: Record<string, Job> }).jobs;
30
+
31
+ test("the jobs that trade keep id-token: write and nothing else", () => {
32
+ for (const name of ["plan", "record", "apply"]) expect(jobs[name]!.permissions, name).toEqual({ "id-token": "write" });
33
+ expect(jobs["plan-note"]!.permissions).toBeUndefined();
34
+ });
35
+
36
+ test("the plan job trades for the plan identity first, drops the request variables after its checkout, and names no apply identity", () => {
37
+ const plan = jobs.plan!;
38
+ expect(plan.steps[0]!.name).toBe("Trade the job's OIDC token for the plan identity");
39
+ expect(JSON.parse(plan.steps[0]!.env![TRADE_SPEC_ENV]!)).toEqual(IDENTITY.plan);
40
+ expect(plan.steps[1]!.name).toBe("Check out the change");
41
+ for (const step of plan.steps.slice(2)) expect(step.run!.split("\n")[0]).toBe(dropForgeTokens("sh", OIDC_REQUEST_VARS));
42
+ for (const id of tradeIdentities(IDENTITY.apply)) expect(JSON.stringify(plan)).not.toContain(id);
43
+ });
44
+
45
+ test("the apply job trades for the apply identity", () => {
46
+ expect(JSON.parse(jobs.apply!.steps[0]!.env![TRADE_SPEC_ENV]!)).toEqual(IDENTITY.apply);
47
+ });
48
+ });
@@ -15,6 +15,7 @@ import { parseYAML } from "@intentius/chant/yaml";
15
15
  import type { DriverComponent } from "@intentius/chant/components/driver";
16
16
  import { generateGithubPipeline } from "@intentius/chant-lexicon-github/components/generate-pipeline";
17
17
  import { githubBaseFallback, greenCheckout } from "@intentius/chant/components/pr-pipeline";
18
+ import { dropForgeTokens } from "@intentius/chant/ci-handover";
18
19
  import { generateForgejoPipeline } from "./generate-pipeline";
19
20
 
20
21
  const ESTATE: DriverComponent[] = [
@@ -44,7 +45,10 @@ describe("the Forgejo pull-request workflow", () => {
44
45
  test("both jobs run chant ci run with the runner key secret, as on GitHub (#4156)", () => {
45
46
  expect(result.yaml).toContain("chant ci run plan --in-place --no-push");
46
47
  expect(result.yaml.match(/CHANT_RUNNER_KEY: '\$\{\{ secrets\.CHANT_RUNNER_KEY \}\}'/g)).toHaveLength(3);
47
- expect(result.yaml).not.toContain("chant components pr-");
48
+ expect(result.yaml).not.toContain("chant components pr-plan");
49
+ expect(result.yaml).not.toContain("chant components pr-apply");
50
+ // #4295: the plan-note job posts with pr-post, to Forgejo.
51
+ expect(result.yaml).toContain("chant components pr-post --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" --env prod --output .chant/pr --forge forgejo");
48
52
  });
49
53
 
50
54
  test("its commands talk to Forgejo, on Forgejo's runner, with the job permissions dropped", () => {
@@ -125,8 +129,9 @@ describe("the forgejo pull-request pipeline with build (#4088)", () => {
125
129
 
126
130
  test("builds, then lints, before pr-plan and pr-apply", () => {
127
131
  for (const job of ["plan", "apply"]) {
128
- const runs = doc.jobs[job].steps.map((s: { run?: string }) => s.run ?? "").filter(Boolean);
129
- const i = runs.findIndex((r: string) => r.startsWith("chant components pr-"));
132
+ const drop = dropForgeTokens("sh") + "\n";
133
+ const runs = doc.jobs[job].steps.map((s: { run?: string }) => (s.run ?? "").replace(drop, "").trimEnd()).filter(Boolean);
134
+ const i = runs.findIndex((r: string) => r.includes(`chant ci run ${job}`));
130
135
  expect(runs.slice(i - 2, i)).toEqual(["chant build --env prod", "chant lint"]);
131
136
  }
132
137
  });
@@ -154,7 +159,7 @@ describe("the Forgejo pull-request workflow records on merge by default (#4168)"
154
159
  });
155
160
 
156
161
  test("a push runs ci run record against Forgejo, and nothing applies", () => {
157
- expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "record"]);
162
+ expect(result.jobs.map((j) => j.jobName)).toEqual(["plan", "plan-note", "record"]);
158
163
  expect(result.yaml).toContain('chant ci run record --in-place --base "$BASE_SHA" --env prod --gate pr-apply --forge forgejo');
159
164
  expect(result.yaml).not.toContain("ci run apply");
160
165
  expect(result.yaml).toContain("group: chant-record-prod");
@@ -50,7 +50,7 @@ describe("the Forgejo pull-request workflow with a gate per wave (#4003)", () =>
50
50
  });
51
51
 
52
52
  test("an apply job per wave on Forgejo's runner, each passing its wave and talking to Forgejo", () => {
53
- expect(Object.keys(doc.jobs)).toEqual(["plan", "record", "apply-wave-1", "apply-wave-2"]);
53
+ expect(Object.keys(doc.jobs)).toEqual(["plan", "plan-note", "record", "apply-wave-1", "apply-wave-2"]);
54
54
  expect(doc.jobs["apply-wave-2"]!.needs).toEqual(["apply-wave-1"]);
55
55
  for (const k of [1, 2]) {
56
56
  const job = doc.jobs[`apply-wave-${k}`]!;