From 3356bce7160d60b66532893675c4d358a2590579 Mon Sep 17 00:00:00 2001 From: G <41178744+maonakamoto@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:38:23 +0200 Subject: [PATCH] fix(ci): a red base must not trap the PR that repairs it --- scripts/ci/auto-merge-sweep.sh | 52 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/scripts/ci/auto-merge-sweep.sh b/scripts/ci/auto-merge-sweep.sh index b6f0b76..d30869d 100644 --- a/scripts/ci/auto-merge-sweep.sh +++ b/scripts/ci/auto-merge-sweep.sh @@ -64,7 +64,7 @@ echo "[auto-merge] sweeping open PRs against ${BASE_BRANCH} in ${REPO}" # batching this script exists to prevent. base_sha=$(gh api "repos/${REPO}/commits/${BASE_BRANCH}" --jq '.sha') base_ci=$(gh run list --repo "$REPO" --workflow "$CI_WORKFLOW" --branch "$BASE_BRANCH" --limit 1 \ - --json status,conclusion,headSha --jq '.[0] // empty') + --json databaseId,status,conclusion,headSha --jq '.[0] // empty') if [ -z "$base_ci" ]; then echo "[auto-merge] no CI history for ${BASE_BRANCH} — proceeding" @@ -81,9 +81,32 @@ else echo "[auto-merge] ${BASE_BRANCH} CI is still running — deferring to the next sweep" exit 0 fi + # A red base must not become a trap for the PR that repairs it. + # + # "Never merge onto red" is right for an unrelated change: it stops a broken + # base quietly collecting more of them and getting harder to diagnose. But + # when the PR *is* the repair, the same rule deadlocks the repo — the fix + # cannot travel the path its own redness blocks, and only a human can move + # it. Seen in maonakamoto/aoz-housing on 2026-08-07: E2E red on the base, the + # fix sitting green in a PR, every sweep refusing politely. + # + # So identify WHICH jobs are red and let a PR through only if its own checks + # pass every one of them. Not a weakening: a PR's checks run on the MERGE + # result (refs/pull/N/merge), so green-on-those-jobs is direct evidence the + # post-merge base is better than the pre-merge base. Still refused: a PR that + # does not cover the failing jobs, one that covers only some of them, and a + # base failure whose jobs cannot be identified at all. + base_red_jobs="" if [ "$base_conclusion" != "success" ]; then - echo "[auto-merge] ${BASE_BRANCH} CI is ${base_conclusion} — refusing to merge onto a broken base" >&2 - exit 0 + base_run_id=$(printf '%s' "${base_ci}" | jq -r '.databaseId') + base_red_jobs=$(gh run view "${base_run_id}" --repo "$REPO" --json jobs \ + --jq '[.jobs[] | select(.conclusion == "failure") | .name] | .[]' 2>/dev/null || true) + if [ -z "${base_red_jobs}" ]; then + echo "[auto-merge] ${BASE_BRANCH} CI is ${base_conclusion} and no failing job could be identified — refusing to merge onto a broken base" >&2 + exit 0 + fi + echo "[auto-merge] ${BASE_BRANCH} CI is ${base_conclusion} — failing: $(printf '%s' "${base_red_jobs}" | tr '\n' ' ')" >&2 + echo "[auto-merge] only a PR that is green on those exact jobs may merge (its checks run on the merge result)" fi fi @@ -222,6 +245,29 @@ for number in $(printf '%s' "$prs_json" | jq -r 'sort_by(.number) | .[].number') break fi + # Red base: this PR merges only if it proves every failing job green. + if [ -n "${base_red_jobs}" ]; then + pr_green=$(printf '%s' "$pr" | jq -r ' + [ .statusCheckRollup[]? + | select(((.conclusion // .state // "") | test("^(SUCCESS|NEUTRAL|SKIPPED)$"))) + | (.name // .context) ] | .[]') + uncovered="" + while IFS= read -r job; do + [ -z "$job" ] && continue + printf '%s\n' "$pr_green" | grep -Fxq "$job" || uncovered="${uncovered}${job}; " + done <&2 + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + echo "- 🔧 #${number} merged onto a red \`${BASE_BRANCH}\` because it passes every failing job — ${title}" >> "$GITHUB_STEP_SUMMARY" + fi + fi + echo "[auto-merge] #${number} green and ready — merging: ${title}" if gh pr merge "$number" --repo "$REPO" --squash --delete-branch; then merged_any=1