fix(ci): block merge when a PR still has needs-validation (in ci.yml validate gate) (#4666)

* feat(ci): block merge when a PR still has the needs-validation label

A required check on merge_group (and pull_request) that fails if any PR in scope still
carries 'needs-validation'. On the merge queue this ejects the group; on the PR it shows
red so an unvalidated PR can't be queued. Respects the skip-validation override implicitly
(that override means needs-validation is never added).

Enforcement requires adding this job as a required status check for main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): fold needs-validation gate into ci.yml validate job (fail closed)

Per review (PerishCode):
- Drop the standalone block-unvalidated-merge.yml; per .github/AGENTS.md, ci.yml is the
  low-privilege PR / merge-queue / validation gate, so the check belongs there.
- Add it as a step in the existing 'validate' job (already a required check, so it now
  gates the merge queue with no extra branch-protection wiring).
- Fail closed: drop the '|| true' suppressions; on pull_request read labels from the event
  payload (no API call), on merge_group resolve the group's PRs and let any lookup failure
  block the merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): make merge_group label lookup fail closed (out of the if condition)

Per review (PerishCode): a gh call inside an if condition has its nonzero exit swallowed
(read as 'no label'), so a failed lookup waved the merge through. Capture each gh result into
a plain assignment so set -e / pipefail blocks on failure; the if only tests the captured value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lefarcen
2026-06-23 06:40:45 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent e707e017ce
commit 7229584ae4
+41
View File
@@ -889,6 +889,47 @@ jobs:
exit 1
fi
- name: Block merge while the needs-validation label is present
# Hard gate: a PR that still carries `needs-validation` must not merge. Fails closed —
# any label-lookup error blocks rather than silently waving the merge through. Respects
# the skip-validation override implicitly (that override means needs-validation is never
# added). On pull_request the labels come straight from the event payload (no API call);
# on merge_group we resolve every PR in the queued group.
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
BASE_SHA: ${{ github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.merge_group.head_sha }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
if printf '%s' "$PR_LABELS" | jq -e 'index("needs-validation")' >/dev/null; then
echo "::error::PR still has 'needs-validation' — validate (/explore Pass or manual QA) and remove the label before merging."
exit 1
fi
elif [ "$EVENT_NAME" = "merge_group" ]; then
# Fail closed: capture each gh result into a plain assignment so a failed lookup
# trips set -e / pipefail and blocks the merge. Do NOT put the gh call in an `if`
# condition — a nonzero exit there is swallowed (read as "no label") and waves the
# merge through.
shas="$(gh api "repos/$REPO/compare/$BASE_SHA...$HEAD_SHA" --jq '.commits[].sha')"
prs=""
for s in $shas; do
pulls="$(gh api "repos/$REPO/commits/$s/pulls" --jq '.[].number')"
prs="$(printf '%s\n%s\n' "$prs" "$pulls")"
done
for pr in $(printf '%s\n' "$prs" | sort -u | grep -E '^[0-9]+$'); do
labels="$(gh pr view "$pr" --repo "$REPO" --json labels --jq '.labels[].name')"
if printf '%s\n' "$labels" | grep -qx 'needs-validation'; then
echo "::error::PR #$pr still has 'needs-validation' — blocking merge."
exit 1
fi
done
fi
echo "No 'needs-validation' label in scope — clear to merge."
runtime_summary:
name: Runtime summary
needs: