@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.
@@ -18,20 +18,74 @@ jobs:
18
18
  container: node:22
19
19
  permissions:
20
20
  contents: read
21
- pull-requests: write
22
- statuses: write
21
+ outputs:
22
+ report_0: '${{ steps.plan.outputs.report_0 }}'
23
23
  steps:
24
- - uses: actions/checkout@v7
25
- with:
26
- fetch-depth: 0
27
- - 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)
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: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
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
- pull-requests: write
28
- statuses: write
27
+ outputs:
28
+ report_0: '${{ steps.plan.outputs.report_0 }}'
29
29
  steps:
30
- - uses: actions/checkout@v7
31
- with:
32
- fetch-depth: 0
33
- - 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)
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: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github --wave-gate release
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
- pull-requests: write
28
- statuses: write
27
+ outputs:
28
+ report_0: '${{ steps.plan.outputs.report_0 }}'
29
29
  steps:
30
- - uses: actions/checkout@v7
31
- with:
32
- fetch-depth: 0
33
- - 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)
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: chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge github
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 });