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