@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.
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/__fixtures__/pr-loop-build.forgejo.golden.yml +77 -8
- package/src/components/__fixtures__/pr-loop-record.forgejo.golden.yml +71 -6
- package/src/components/__fixtures__/pr-loop-waves.forgejo.golden.yml +72 -7
- package/src/components/__fixtures__/pr-loop.forgejo.golden.yml +71 -6
- package/src/components/generate-pr-pipeline.identity.test.ts +48 -0
- package/src/components/generate-pr-pipeline.test.ts +9 -4
- package/src/components/generate-pr-waves-pipeline.test.ts +1 -1
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
12
|
+
"composite": "e9da8444b356cf4824fb1e23b30fdebd4c8d019cac5b20959f84eb2f76a6b4ba"
|
|
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.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.
|
|
58
|
-
"@intentius/chant-lexicon-github": "^0.124.
|
|
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
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
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
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
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
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
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
|
|
129
|
-
const
|
|
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}`]!;
|