Skip to content

Auto-update PR branches after push to main #1202

Auto-update PR branches after push to main

Auto-update PR branches after push to main #1202

name: "Auto-update PR branches"
run-name: "Auto-update PR branches after push to ${{ github.ref_name }}"
# When `main` advances, update every open PR that has auto-merge enabled
# and is BEHIND. Without this, the strict required-status-checks policy
# (`strict_required_status_checks_policy: true` on the `main` ruleset)
# blocks the auto-merge button forever — auto-merge fires only when checks
# are green AND the head is up-to-date with the base, but it does not
# update the branch on its own. This workflow closes that gap so polish /
# impl PRs squash-merge themselves end-to-end.
#
# Why we don't use GitHub's native merge queue: this repository is
# user-owned and merge_queue rules are not available on the account's
# plan — the rulesets API rejects the rule with an empty validation
# error. The auto-update workflow gives us the same end-user behavior
# (no manual "Update branch" clicks) without the plan dependency.
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write # update-branch creates a merge commit on the head ref
pull-requests: write # required to call PUT /pulls/:num/update-branch
concurrency:
group: auto-update-pr-branches
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Wait for PR mergeable state to settle
# GitHub recomputes `mergeStateStatus` asynchronously after a
# push to main. If we list PRs immediately the field can still
# be "UNKNOWN" — and the cached head SHA on the PR can lag
# behind the actual ref, so update-branch returns "expected
# head sha didn't match current head ref." Give GitHub a
# moment to settle.
run: sleep 30
- name: Update PRs with auto-merge enabled
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -eo pipefail
# After a push to main, every open PR with auto-merge is by
# definition behind. Don't filter on `mergeStateStatus`:
# right after the push it can still be UNKNOWN, and we'd
# skip valid candidates. Just iterate every open
# auto-merge PR — update-branch is a no-op when the head
# is already up-to-date.
PRS=$(gh pr list \
--repo "$GH_REPO" \
--state open \
--base main \
--limit 200 \
--json number,title,headRefName,autoMergeRequest)
CANDIDATES=$(echo "$PRS" | jq -c '
[ .[] | select(.autoMergeRequest != null) ]
')
COUNT=$(echo "$CANDIDATES" | jq 'length')
echo "::notice::Found $COUNT open PR(s) with auto-merge enabled"
if [[ "$COUNT" -eq 0 ]]; then
exit 0
fi
echo "$CANDIDATES" | jq -c '.[]' | while read -r pr; do
NUM=$(echo "$pr" | jq -r '.number')
TITLE=$(echo "$pr" | jq -r '.title')
BRANCH=$(echo "$pr" | jq -r '.headRefName')
echo "::notice::Updating PR #${NUM} (${BRANCH}): ${TITLE}"
# Capture stderr so the actual GitHub error (conflict,
# SHA mismatch, etc.) lands in the workflow log instead
# of being swallowed. Failures here don't fail the job —
# one stuck PR shouldn't block the others.
if ! OUT=$(gh api -X PUT \
"repos/${GH_REPO}/pulls/${NUM}/update-branch" \
-H "Accept: application/vnd.github+json" 2>&1); then
echo "::warning::Could not update PR #${NUM}: ${OUT}"
fi
done