Skip to content

Commit c0ad42d

Browse files
committed
ci: add auto-update-pr flywheel workflow
1 parent d19b53b commit c0ad42d

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Flywheel: after a merge to the base branch, find one open PR that is
2+
# out-of-date (behind base) but otherwise passing + conflict-free, and press
3+
# its "Update branch" button. Updating retriggers the PR's CI; if it goes green
4+
# and the PR has auto-merge enabled, it merges -> another push to base -> this
5+
# workflow runs again -> the next stale PR gets updated, and so on.
6+
#
7+
# Only ONE PR is updated per run, on purpose: updating every stale PR at once
8+
# makes them all up-to-date simultaneously, the first merges, the rest are
9+
# instantly stale again, and a full CI run is burned on each for nothing.
10+
#
11+
# Uses PROSOPONATOR_PAT (not the default GITHUB_TOKEN) so the branch update is
12+
# attributed to a real user and therefore retriggers the PR's CI workflows.
13+
14+
name: auto-update-pr
15+
16+
on:
17+
push:
18+
branches: [main]
19+
# backstop: catches the case where a stale PR's CI failed and stalled the
20+
# flywheel, then the base later moved for some other reason.
21+
schedule:
22+
- cron: "*/30 * * * *"
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: read
27+
pull-requests: read
28+
29+
# Single-flight: never let two runs race to update PRs and double-spend CI.
30+
concurrency:
31+
group: auto-update-pr
32+
cancel-in-progress: false
33+
34+
defaults:
35+
run:
36+
shell: bash
37+
38+
jobs:
39+
update-stale-pr:
40+
runs-on: ubuntu-latest
41+
env:
42+
GH_TOKEN: ${{ secrets.PROSOPONATOR_PAT }}
43+
REPO: ${{ github.repository }}
44+
BASE: main
45+
# how long to wait for GitHub to finish computing mergeability per PR
46+
POLL_ATTEMPTS: "6"
47+
POLL_SLEEP_SECONDS: "10"
48+
steps:
49+
- name: Update one stale-but-passing PR
50+
run: |
51+
set -euo pipefail
52+
53+
# Open, non-draft PRs targeting BASE that have auto-merge enabled,
54+
# oldest first (lowest number) for FIFO fairness.
55+
# Drop the `.autoMergeRequest != null` filter to also flywheel PRs
56+
# that don't yet have auto-merge enabled.
57+
mapfile -t prs < <(
58+
gh pr list --repo "$REPO" --state open --base "$BASE" --limit 100 \
59+
--json number,isDraft,autoMergeRequest \
60+
--jq '[ .[]
61+
| select(.isDraft == false)
62+
| select(.autoMergeRequest != null)
63+
| .number ]
64+
| sort
65+
| .[]'
66+
)
67+
68+
if [ "${#prs[@]}" -eq 0 ]; then
69+
echo "No open auto-merge PRs targeting $BASE. Nothing to do."
70+
exit 0
71+
fi
72+
73+
echo "Candidate PRs (auto-merge, targeting $BASE): ${prs[*]}"
74+
75+
for n in "${prs[@]}"; do
76+
echo "::group::PR #$n"
77+
78+
mergeable=""
79+
state=""
80+
# Poll until GitHub finishes computing the merge state (it is lazy
81+
# and returns UNKNOWN right after a base change).
82+
for attempt in $(seq 1 "$POLL_ATTEMPTS"); do
83+
read -r mergeable state < <(
84+
gh pr view "$n" --repo "$REPO" \
85+
--json mergeable,mergeStateStatus \
86+
--jq '"\(.mergeable) \(.mergeStateStatus)"'
87+
)
88+
echo "attempt $attempt: mergeable=$mergeable mergeStateStatus=$state"
89+
if [ "$mergeable" != "UNKNOWN" ] && [ "$state" != "UNKNOWN" ]; then
90+
break
91+
fi
92+
sleep "$POLL_SLEEP_SECONDS"
93+
done
94+
95+
if [ "$mergeable" = "UNKNOWN" ] || [ "$state" = "UNKNOWN" ]; then
96+
echo "Merge state still computing after polling; skipping PR #$n."
97+
echo "::endgroup::"
98+
continue
99+
fi
100+
101+
# CONFLICTING => real conflicts, author must resolve. Skip.
102+
if [ "$mergeable" != "MERGEABLE" ]; then
103+
echo "PR #$n is not mergeable ($mergeable); skipping."
104+
echo "::endgroup::"
105+
continue
106+
fi
107+
108+
# mergeStateStatus meanings we care about:
109+
# BEHIND -> conflict-free, checks/reviews otherwise satisfied,
110+
# only blocker is being out of date. THIS is the target.
111+
# CLEAN -> already up to date and ready (auto-merge handles it).
112+
# BLOCKED/UNSTABLE/DIRTY -> failing/pending checks, missing review,
113+
# or conflicts: not something a branch update fixes.
114+
if [ "$state" != "BEHIND" ]; then
115+
echo "PR #$n mergeStateStatus=$state (not BEHIND); skipping."
116+
echo "::endgroup::"
117+
continue
118+
fi
119+
120+
echo "PR #$n is behind but passing. Updating its branch..."
121+
gh api \
122+
--method PUT \
123+
-H "Accept: application/vnd.github+json" \
124+
"/repos/$REPO/pulls/$n/update-branch"
125+
126+
echo "Updated PR #$n. Done (one per run)."
127+
echo "::endgroup::"
128+
exit 0
129+
done
130+
131+
echo "No behind-but-passing PR found this run."

0 commit comments

Comments
 (0)