@@ -33,6 +33,12 @@ name: Merge into upstream
3333# until the last of them lands. Whichever run finds them all green performs
3434# the merge. That avoids a job sitting in a poll loop holding a runner.
3535#
36+ # This workflow reports the most severe state of the gating workflows, so a
37+ # run that merged nothing is never green. Failure outranks cancelled, which
38+ # outranks a clean pass. A leg that is still running is not a state yet, so
39+ # nothing is reported until every leg has finished, except for a failure,
40+ # which nothing can outrank and so reports straight away.
41+ #
3642# Note that workflow_run only ever uses the copy of this file on the default
3743# branch, and runs in the context of the default branch rather than the branch
3844# being merged - hence head_branch / head_sha / actor throughout.
@@ -72,10 +78,11 @@ jobs:
7278 timeout-minutes : 15
7379
7480 # Listing any permission drops the unlisted ones. The pushes below use the
75- # app token, not GITHUB_TOKEN, so read access is all this needs.
81+ # app token, not GITHUB_TOKEN. actions: write is for this run cancelling
82+ # itself to mirror a cancelled gating workflow, nothing else needs write.
7683 permissions :
7784 contents : read
78- actions : read
85+ actions : write
7986
8087 env :
8188 BRANCH : ${{ github.event.workflow_run.head_branch }}
@@ -95,51 +102,109 @@ jobs:
95102 set -euo pipefail
96103
97104 ready=yes
105+ failed=""
106+ cancelled=""
107+ pending=no
108+
109+ {
110+ echo "### Gating workflows for \`${HEAD_SHA}\`"
111+ echo
112+ echo "| Workflow | Status | Conclusion |"
113+ echo "| --- | --- | --- |"
114+ } >> "$GITHUB_STEP_SUMMARY"
98115
99116 for wf in ${REQUIRED_WORKFLOWS}; do
100117 # Match the commit and the push event, so a nightly schedule run of
101118 # the same workflow is never mistaken for this one.
102119 info=$(gh api \
103120 "repos/${GITHUB_REPOSITORY}/actions/workflows/${wf}/runs?head_sha=${HEAD_SHA}&event=push&per_page=1" \
104- --jq '.workflow_runs[0] // empty | "\(.status) \(.conclusion // "none")"') || info=""
121+ --jq '.workflow_runs[0] // empty | "\(.status) \(.conclusion // "none") \(.html_url) "') || info=""
105122
106123 if [ -z "${info}" ]; then
107124 echo " ${wf}: no run for this commit"
125+ echo "| ${wf} | no run | |" >> "$GITHUB_STEP_SUMMARY"
108126 ready=no
127+ pending=yes
109128 continue
110129 fi
111130
112- status="${info%% *}"
113- conclusion="${info##* }"
131+ read -r status conclusion url <<< "${info}"
114132
115133 if [ "${status}" != "completed" ]; then
116134 echo " ${wf}: ${status}"
135+ echo "| [${wf}](${url}) | ${status} | |" >> "$GITHUB_STEP_SUMMARY"
117136 ready=no
137+ pending=yes
118138 continue
119139 fi
120140
121- # A cancelled leg is not a pass. Concurrency cancels the older run
122- # when a new commit lands, and that result says nothing about this
123- # commit.
141+ echo " ${wf}: ${conclusion}"
142+ echo "| [${wf}](${url}) | ${status} | ${conclusion} |" >> "$GITHUB_STEP_SUMMARY"
143+
124144 case "${conclusion}" in
125145 success|skipped|neutral)
126- echo " ${wf}: ${conclusion}"
127146 ;;
128147
148+ # Concurrency cancels the older run when a newer commit lands,
149+ # so a cancelled leg is not a pass and not a failure either.
150+ cancelled)
151+ ready=no
152+ cancelled="${cancelled} ${wf}"
153+ ;;
154+
155+ # Anything outside the pass set fails, so a conclusion this
156+ # workflow has not seen before gets reported rather than
157+ # silently ignored.
129158 *)
130- echo " ${wf}: ${conclusion}"
131159 ready=no
160+ failed="${failed} ${wf}"
132161 ;;
133162 esac
134163 done
135164
136- # Exiting 0 either way: a leg that is still running or has failed is
137- # not this workflow's problem to report, and failing here would put a
138- # red run in the Actions tab for every commit that is merely still
139- # building.
140165 echo "ready=${ready}" >> "$GITHUB_OUTPUT"
141- if [ "${ready}" != "yes" ]; then
166+
167+ # Nothing outranks a failure, so it reports without waiting for the
168+ # legs that are still running.
169+ if [ -n "${failed}" ]; then
170+ {
171+ echo
172+ echo "### ${BRANCH} was not merged into master"
173+ echo
174+ echo "Gating workflows that did not pass:${failed}"
175+ } >> "$GITHUB_STEP_SUMMARY"
176+
177+ echo "::error::${BRANCH} was not merged into master, gating workflows that did not pass:${failed}"
178+ exit 1
179+ fi
180+
181+ # A leg that is still running has no state to report yet, and it can
182+ # still fail, which would outrank the cancelled below.
183+ if [ "${pending}" = "yes" ]; then
142184 echo "Not merging ${BRANCH} yet"
185+ exit 0
186+ fi
187+
188+ if [ -n "${cancelled}" ]; then
189+ {
190+ echo
191+ echo "### ${BRANCH} was not merged into master"
192+ echo
193+ echo "Gating workflows that were cancelled:${cancelled}"
194+ } >> "$GITHUB_STEP_SUMMARY"
195+
196+ echo "::warning::${BRANCH} was not merged into master, gating workflows that were cancelled:${cancelled}"
197+
198+ # An exit code only ever picks success or failure, so the run
199+ # cancels itself to mirror the cancelled leg. Parking on a fifo
200+ # with no writer afterwards blocks in the kernel until the runner
201+ # kills the job, so the script never reaches an exit code that
202+ # would overwrite the cancellation. timeout-minutes on the job
203+ # bounds the wait if the cancellation never arrives.
204+ gh run cancel "${GITHUB_RUN_ID}"
205+
206+ mkfifo "${RUNNER_TEMP}/await-cancel"
207+ cat "${RUNNER_TEMP}/await-cancel"
143208 fi
144209
145210 - uses : actions/checkout@v6
0 commit comments