@@ -3,11 +3,10 @@ name: CI
33on :
44 pull_request :
55 branches : [master]
6- paths :
7- - ' **/Dockerfile'
8- - ' **/*.sh'
9- - ' .github/workflows/**'
10- - ' .hadolint.yaml'
6+ # Deliberately unfiltered: "CI complete" below is the required status check
7+ # for master, and a required check that never runs blocks a PR forever -- a
8+ # docs-only PR would deadlock. Image builds are still skipped per-image via
9+ # detect-changes; the always-on jobs are lint-only and take seconds.
1110
1211concurrency :
1312 group : ci-${{ github.event.pull_request.number || github.ref }}
@@ -194,3 +193,52 @@ jobs:
194193 IMAGE_TAG : ${{ matrix.image }}:test
195194 VERIFY_CMD : ${{ matrix.verify }}
196195 run : docker run --rm --entrypoint sh "$IMAGE_TAG" -c "$VERIFY_CMD"
196+
197+ # -- Single required status check for branch protection ------------------
198+ # Aggregates every job above so master needs exactly one required check.
199+ # Passes when each job succeeded or was legitimately skipped (path-filtered
200+ # builds), and fails if any job failed or was cancelled.
201+ #
202+ # Any job added to this workflow must also be added to `needs` below: this is
203+ # the only required check on master and Renovate auto-merges on it, so an
204+ # unwatched job would go red while the gate stayed green. The first step
205+ # enforces that rather than trusting anyone to remember.
206+ ci-complete :
207+ name : CI complete
208+ # always(), not !cancelled(): GitHub counts a *skipped* required check as
209+ # passing, so !cancelled() would let a cancelled run report a green gate.
210+ if : always()
211+ needs : [lint-dockerfiles, lint-workflows, detect-changes, build-and-verify]
212+ runs-on : ubuntu-latest
213+ steps :
214+ - name : Checkout repository
215+ uses : actions/checkout@v6
216+
217+ - name : Verify every job is watched
218+ env :
219+ WATCHED : ${{ toJSON(needs) }}
220+ run : |
221+ missing=""
222+ for job in $(yq '.jobs | keys | .[]' .github/workflows/ci.yml); do
223+ [ "$job" = "ci-complete" ] && continue
224+ jq -e --arg j "$job" 'has($j)' <<< "$WATCHED" > /dev/null || missing="$missing $job"
225+ done
226+ if [ -n "$missing" ]; then
227+ echo "::error::Jobs missing from ci-complete.needs:$missing"
228+ exit 1
229+ fi
230+
231+ - name : Check results of all CI jobs
232+ env :
233+ WATCHED : ${{ toJSON(needs) }}
234+ run : |
235+ jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$WATCHED"
236+ bad=$(jq -r '
237+ to_entries[]
238+ | select(.value.result != "success" and .value.result != "skipped")
239+ | "\(.key) (\(.value.result))"' <<< "$WATCHED")
240+ if [ -n "$bad" ]; then
241+ echo "::error::CI jobs did not pass: $(echo "$bad" | tr '\n' ' ')"
242+ exit 1
243+ fi
244+ echo "All CI jobs passed or were skipped."
0 commit comments