@intentius/chant-lexicon-github 0.124.40 → 0.124.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ci-jobs.d.ts +9 -0
- package/dist/components/ci-jobs.d.ts.map +1 -1
- package/dist/components/ci-jobs.js +12 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +229 -16
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +76 -10
- package/src/components/__fixtures__/pr-loop-record.github.golden.yml +70 -8
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +72 -10
- package/src/components/__fixtures__/pr-loop.github.golden.yml +70 -8
- package/src/components/ci-jobs.ts +13 -0
- package/src/components/generate-pipeline.test.ts +3 -0
- package/src/components/generate-pipeline.ts +245 -15
- package/src/components/generate-pr-env-pipeline.test.ts +206 -0
- package/src/components/generate-pr-pipeline.before-merge.test.ts +67 -0
- package/src/components/generate-pr-pipeline.handover.test.ts +167 -0
- package/src/components/generate-pr-pipeline.identity.test.ts +219 -0
- package/src/components/generate-pr-pipeline.test.ts +29 -9
- package/src/components/generate-pr-waves-pipeline.test.ts +6 -3
|
@@ -18,20 +18,74 @@ jobs:
|
|
|
18
18
|
container: node:22
|
|
19
19
|
permissions:
|
|
20
20
|
contents: read
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
outputs:
|
|
22
|
+
report_0: '${{ steps.plan.outputs.report_0 }}'
|
|
23
23
|
steps:
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
- name: Check out the change
|
|
25
|
+
env:
|
|
26
|
+
CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
|
|
27
|
+
run: |
|
|
28
|
+
set -eu
|
|
29
|
+
git config --global --add safe.directory "$PWD" 2>/dev/null || true
|
|
30
|
+
[ -d .git ] || git init -q .
|
|
31
|
+
git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
32
|
+
if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
|
|
33
|
+
chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
34
|
+
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
|
|
35
|
+
unset chant_auth
|
|
36
|
+
fi
|
|
37
|
+
ref="$GITHUB_REF"
|
|
38
|
+
want="$GITHUB_SHA"
|
|
39
|
+
case "$ref" in
|
|
40
|
+
refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
|
|
41
|
+
refs/tags/*) target="$ref" ;;
|
|
42
|
+
refs/*) target="refs/remotes/${ref#refs/}" ;;
|
|
43
|
+
*) target="refs/remotes/origin/chant-head" ;;
|
|
44
|
+
esac
|
|
45
|
+
if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
|
|
46
|
+
git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
|
|
47
|
+
git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
|
|
48
|
+
case "$ref" in
|
|
49
|
+
refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
|
|
50
|
+
*) git checkout -q --force --detach "$target" ;;
|
|
51
|
+
esac
|
|
52
|
+
- run: |
|
|
53
|
+
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
|
|
54
|
+
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)
|
|
28
55
|
- name: Plan the members this pull request reaches
|
|
56
|
+
id: plan
|
|
29
57
|
env:
|
|
30
58
|
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
31
59
|
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
32
|
-
GITHUB_TOKEN: '${{ github.token }}'
|
|
33
60
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
34
|
-
run:
|
|
61
|
+
run: |
|
|
62
|
+
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
|
|
63
|
+
chant_report="$PWD/.chant/pr"
|
|
64
|
+
chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=450000,PARTS=1,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; }
|
|
65
|
+
trap chant_handover EXIT
|
|
66
|
+
chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
|
|
67
|
+
plan-note:
|
|
68
|
+
needs:
|
|
69
|
+
- plan
|
|
70
|
+
if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
container: node:22
|
|
73
|
+
permissions:
|
|
74
|
+
actions: read
|
|
75
|
+
contents: read
|
|
76
|
+
pull-requests: write
|
|
77
|
+
statuses: write
|
|
78
|
+
steps:
|
|
79
|
+
- name: Check out the target branch's commit
|
|
80
|
+
uses: actions/checkout@v7
|
|
81
|
+
with:
|
|
82
|
+
ref: '${{ github.event.pull_request.base.sha }}'
|
|
83
|
+
- name: Take the plan report from the plan job
|
|
84
|
+
run: |
|
|
85
|
+
set -u
|
|
86
|
+
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'
|
|
87
|
+
${{ toJSON(needs.plan.outputs.report_0) }}
|
|
88
|
+
END_OF_REPORT
|
|
35
89
|
- name: Keep the plan report
|
|
36
90
|
if: always()
|
|
37
91
|
uses: actions/upload-artifact@v4
|
|
@@ -40,6 +94,14 @@ jobs:
|
|
|
40
94
|
path: .chant/pr
|
|
41
95
|
if-no-files-found: ignore
|
|
42
96
|
include-hidden-files: true
|
|
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 github --result "$PLAN_RESULT"
|
|
43
105
|
record:
|
|
44
106
|
if: github.event_name == 'push'
|
|
45
107
|
runs-on: ubuntu-latest
|
|
@@ -24,20 +24,74 @@ jobs:
|
|
|
24
24
|
container: node:22
|
|
25
25
|
permissions:
|
|
26
26
|
contents: read
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
outputs:
|
|
28
|
+
report_0: '${{ steps.plan.outputs.report_0 }}'
|
|
29
29
|
steps:
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
- name: Check out the change
|
|
31
|
+
env:
|
|
32
|
+
CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
|
|
33
|
+
run: |
|
|
34
|
+
set -eu
|
|
35
|
+
git config --global --add safe.directory "$PWD" 2>/dev/null || true
|
|
36
|
+
[ -d .git ] || git init -q .
|
|
37
|
+
git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
38
|
+
if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
|
|
39
|
+
chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
40
|
+
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
|
|
41
|
+
unset chant_auth
|
|
42
|
+
fi
|
|
43
|
+
ref="$GITHUB_REF"
|
|
44
|
+
want="$GITHUB_SHA"
|
|
45
|
+
case "$ref" in
|
|
46
|
+
refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
|
|
47
|
+
refs/tags/*) target="$ref" ;;
|
|
48
|
+
refs/*) target="refs/remotes/${ref#refs/}" ;;
|
|
49
|
+
*) target="refs/remotes/origin/chant-head" ;;
|
|
50
|
+
esac
|
|
51
|
+
if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
|
|
52
|
+
git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
|
|
53
|
+
git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
|
|
54
|
+
case "$ref" in
|
|
55
|
+
refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
|
|
56
|
+
*) git checkout -q --force --detach "$target" ;;
|
|
57
|
+
esac
|
|
58
|
+
- run: |
|
|
59
|
+
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
|
|
60
|
+
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)
|
|
34
61
|
- name: Plan the members this pull request reaches
|
|
62
|
+
id: plan
|
|
35
63
|
env:
|
|
36
64
|
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
37
65
|
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
38
|
-
GITHUB_TOKEN: '${{ github.token }}'
|
|
39
66
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
40
|
-
run:
|
|
67
|
+
run: |
|
|
68
|
+
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
|
|
69
|
+
chant_report="$PWD/.chant/pr"
|
|
70
|
+
chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=450000,PARTS=1,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; }
|
|
71
|
+
trap chant_handover EXIT
|
|
72
|
+
chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github --wave-gate release
|
|
73
|
+
plan-note:
|
|
74
|
+
needs:
|
|
75
|
+
- plan
|
|
76
|
+
if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
container: node:22
|
|
79
|
+
permissions:
|
|
80
|
+
actions: read
|
|
81
|
+
contents: read
|
|
82
|
+
pull-requests: write
|
|
83
|
+
statuses: write
|
|
84
|
+
steps:
|
|
85
|
+
- name: Check out the target branch's commit
|
|
86
|
+
uses: 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
|
+
END_OF_REPORT
|
|
41
95
|
- name: Keep the plan report
|
|
42
96
|
if: always()
|
|
43
97
|
uses: actions/upload-artifact@v4
|
|
@@ -46,6 +100,14 @@ jobs:
|
|
|
46
100
|
path: .chant/pr
|
|
47
101
|
if-no-files-found: ignore
|
|
48
102
|
include-hidden-files: true
|
|
103
|
+
- name: Post the plan note and chant/plan
|
|
104
|
+
if: always()
|
|
105
|
+
env:
|
|
106
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
107
|
+
HEAD_SHA: '${{ github.sha }}'
|
|
108
|
+
PLAN_RESULT: '${{ needs.plan.result }}'
|
|
109
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
110
|
+
run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"
|
|
49
111
|
record:
|
|
50
112
|
if: github.event_name == 'push'
|
|
51
113
|
runs-on: ubuntu-latest
|
|
@@ -126,7 +188,7 @@ jobs:
|
|
|
126
188
|
if-no-files-found: ignore
|
|
127
189
|
include-hidden-files: true
|
|
128
190
|
apply-wave-2:
|
|
129
|
-
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
191
|
+
if: '!cancelled() && needs.apply-wave-1.result != ''skipped'' && (github.event_name == ''workflow_run'' || github.event_name == ''workflow_dispatch'')'
|
|
130
192
|
runs-on: ubuntu-latest
|
|
131
193
|
needs:
|
|
132
194
|
- apply-wave-1
|
|
@@ -176,7 +238,7 @@ jobs:
|
|
|
176
238
|
if-no-files-found: ignore
|
|
177
239
|
include-hidden-files: true
|
|
178
240
|
apply-wave-3:
|
|
179
|
-
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
|
241
|
+
if: '!cancelled() && needs.apply-wave-2.result != ''skipped'' && (github.event_name == ''workflow_run'' || github.event_name == ''workflow_dispatch'')'
|
|
180
242
|
runs-on: ubuntu-latest
|
|
181
243
|
needs:
|
|
182
244
|
- apply-wave-2
|
|
@@ -24,20 +24,74 @@ jobs:
|
|
|
24
24
|
container: node:22
|
|
25
25
|
permissions:
|
|
26
26
|
contents: read
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
outputs:
|
|
28
|
+
report_0: '${{ steps.plan.outputs.report_0 }}'
|
|
29
29
|
steps:
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
- name: Check out the change
|
|
31
|
+
env:
|
|
32
|
+
CHANT_CHECKOUT_TOKEN: '${{ github.token }}'
|
|
33
|
+
run: |
|
|
34
|
+
set -eu
|
|
35
|
+
git config --global --add safe.directory "$PWD" 2>/dev/null || true
|
|
36
|
+
[ -d .git ] || git init -q .
|
|
37
|
+
git remote get-url origin >/dev/null 2>&1 || git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
38
|
+
if [ -n "${CHANT_CHECKOUT_TOKEN:-}" ]; then
|
|
39
|
+
chant_auth="$(printf 'x-access-token:%s' "$CHANT_CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
40
|
+
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraheader GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $chant_auth"
|
|
41
|
+
unset chant_auth
|
|
42
|
+
fi
|
|
43
|
+
ref="$GITHUB_REF"
|
|
44
|
+
want="$GITHUB_SHA"
|
|
45
|
+
case "$ref" in
|
|
46
|
+
refs/heads/*) target="refs/remotes/origin/${ref#refs/heads/}" ;;
|
|
47
|
+
refs/tags/*) target="$ref" ;;
|
|
48
|
+
refs/*) target="refs/remotes/${ref#refs/}" ;;
|
|
49
|
+
*) target="refs/remotes/origin/chant-head" ;;
|
|
50
|
+
esac
|
|
51
|
+
if git rev-parse -q --verify HEAD >/dev/null; then git clean -ffdxq; fi
|
|
52
|
+
git -c protocol.version=2 fetch -q --prune --no-recurse-submodules origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' "+$want:$target"
|
|
53
|
+
git -c protocol.version=2 fetch -q --no-tags origin '+refs/heads/chant/lifecycle:refs/heads/chant/lifecycle' 2>/dev/null || true
|
|
54
|
+
case "$ref" in
|
|
55
|
+
refs/heads/*) git checkout -q --force -B "${ref#refs/heads/}" "$target" ;;
|
|
56
|
+
*) git checkout -q --force --detach "$target" ;;
|
|
57
|
+
esac
|
|
58
|
+
- run: |
|
|
59
|
+
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
|
|
60
|
+
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)
|
|
34
61
|
- name: Plan the members this pull request reaches
|
|
62
|
+
id: plan
|
|
35
63
|
env:
|
|
36
64
|
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
|
37
65
|
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
38
|
-
GITHUB_TOKEN: '${{ github.token }}'
|
|
39
66
|
CHANT_RUNNER_KEY: '${{ secrets.CHANT_RUNNER_KEY }}'
|
|
40
|
-
run:
|
|
67
|
+
run: |
|
|
68
|
+
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
|
|
69
|
+
chant_report="$PWD/.chant/pr"
|
|
70
|
+
chant_handover() { node -e 'const fs=require("fs"),p=require("path"),z=require("zlib");const [dir,out]=process.argv.slice(1);const CHUNK=450000,PARTS=1,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; }
|
|
71
|
+
trap chant_handover EXIT
|
|
72
|
+
chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
|
|
73
|
+
plan-note:
|
|
74
|
+
needs:
|
|
75
|
+
- plan
|
|
76
|
+
if: always() && github.event_name == 'pull_request' && (needs.plan.result == 'success' || needs.plan.result == 'failure')
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
container: node:22
|
|
79
|
+
permissions:
|
|
80
|
+
actions: read
|
|
81
|
+
contents: read
|
|
82
|
+
pull-requests: write
|
|
83
|
+
statuses: write
|
|
84
|
+
steps:
|
|
85
|
+
- name: Check out the target branch's commit
|
|
86
|
+
uses: 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
|
+
END_OF_REPORT
|
|
41
95
|
- name: Keep the plan report
|
|
42
96
|
if: always()
|
|
43
97
|
uses: actions/upload-artifact@v4
|
|
@@ -46,6 +100,14 @@ jobs:
|
|
|
46
100
|
path: .chant/pr
|
|
47
101
|
if-no-files-found: ignore
|
|
48
102
|
include-hidden-files: true
|
|
103
|
+
- name: Post the plan note and chant/plan
|
|
104
|
+
if: always()
|
|
105
|
+
env:
|
|
106
|
+
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
107
|
+
HEAD_SHA: '${{ github.sha }}'
|
|
108
|
+
PLAN_RESULT: '${{ needs.plan.result }}'
|
|
109
|
+
GITHUB_TOKEN: '${{ github.token }}'
|
|
110
|
+
run: chant components pr-post --pr "$PR_NUMBER" --head "$HEAD_SHA" --env prod --output .chant/pr --forge github --result "$PLAN_RESULT"
|
|
49
111
|
record:
|
|
50
112
|
if: github.event_name == 'push'
|
|
51
113
|
runs-on: ubuntu-latest
|
|
@@ -46,6 +46,19 @@ export function keepRecordStep(name: string, artifact: string, path: string): Re
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The `if:` of a wave job after the first (#4291): it runs when the wave
|
|
51
|
+
* before it failed, so a failed member blocks only the members that depend
|
|
52
|
+
* on it, and the next wave's job blocks those and runs the rest. It does not
|
|
53
|
+
* run when the run was cancelled, or when the job before it was skipped
|
|
54
|
+
* (its own condition was false). `condition` is the job's own condition,
|
|
55
|
+
* which still has to hold.
|
|
56
|
+
*/
|
|
57
|
+
export function afterEarlierWave(previous: string, condition?: string): string {
|
|
58
|
+
const runs = `!cancelled() && needs.${previous}.result != 'skipped'`;
|
|
59
|
+
return condition ? `${runs} && (${condition})` : runs;
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/** A `concurrency:` mapping: one run at a time per group. */
|
|
50
63
|
export function concurrency(group: string, cancelInProgress = false): Record<string, unknown> {
|
|
51
64
|
return { group, "cancel-in-progress": cancelInProgress };
|
|
@@ -434,6 +434,9 @@ describe("generateGithubPipeline: gated waves (#3049)", () => {
|
|
|
434
434
|
const parsed = parseYAML(result.yaml) as { jobs: Record<string, { needs?: string[]; steps: Array<Record<string, unknown>> }> };
|
|
435
435
|
expect(Object.keys(parsed.jobs)).toEqual(["wave-1", "wave-2", "wave-3"]);
|
|
436
436
|
expect(parsed.jobs["wave-2"].needs).toEqual(["wave-1"]);
|
|
437
|
+
// #4291: wave 2 runs after wave 1 failed, so a failure blocks only its dependents.
|
|
438
|
+
expect((parsed.jobs["wave-2"] as Record<string, unknown>).if).toBe("!cancelled() && needs.wave-1.result != 'skipped'");
|
|
439
|
+
expect((parsed.jobs["wave-1"] as Record<string, unknown>).if).toBeUndefined();
|
|
437
440
|
|
|
438
441
|
const steps = parsed.jobs["wave-2"].steps;
|
|
439
442
|
expect(steps[0].with).toEqual({ "fetch-depth": 0 });
|