Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions scripts/ci/auto-merge-sweep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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 <<INNER_EOF
${base_red_jobs}
INNER_EOF
if [ -n "$uncovered" ]; then
echo "[auto-merge] #${number} skip: ${BASE_BRANCH} is red on [${uncovered%; }] and this PR does not prove those green β€” ${title}"
continue
fi
echo "[auto-merge] #${number} is green on every job ${BASE_BRANCH} fails β€” merging it to repair the base: ${title}" >&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
Expand Down