@biffo/cli 0.219.0 → 0.221.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -60,6 +60,19 @@ This file is distributed by the template and kept in step by
|
|
|
60
60
|
|
|
61
61
|
- Get CI green and confirm it: `gh pr checks <N>`. A green local run is not
|
|
62
62
|
sufficient — verify the actual PR checks.
|
|
63
|
+
- **Wait with `scripts/wait-for-checks.sh`, not a hand-rolled loop:**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sh scripts/wait-for-checks.sh <N> # 0 green · 1 failed · 2 cannot tell
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Do not write your own `until … grep -c pending … done`. That polls for the
|
|
70
|
+
**absence** of pending checks, so the empty window right after
|
|
71
|
+
`gh pr update-branch` — superseded runs dropped, new ones not yet registered —
|
|
72
|
+
reads as "all green" and merges a PR whose CI has not started. The script
|
|
73
|
+
waits on a **positive** signal instead, and its **exit 2 means "cannot tell",
|
|
74
|
+
which is never a pass**.
|
|
75
|
+
|
|
63
76
|
- **Squash-merge, delete the branch, remove the worktree:**
|
|
64
77
|
`gh pr merge <N> --squash --delete-branch`.
|
|
65
78
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Wait for a pull request's checks to finish, without mistaking "not started"
|
|
4
|
+
# for "all green".
|
|
5
|
+
#
|
|
6
|
+
# ## Why this exists
|
|
7
|
+
#
|
|
8
|
+
# Every session hand-rolls this loop, and the natural formulation is wrong in
|
|
9
|
+
# the dangerous direction:
|
|
10
|
+
#
|
|
11
|
+
# until [ "$(gh pr checks "$N" | grep -c pending)" = 0 ]; do sleep 30; done
|
|
12
|
+
#
|
|
13
|
+
# That polls for the **absence** of pending work, so a transient empty set reads
|
|
14
|
+
# as completion. Immediately after `gh pr update-branch` GitHub drops the
|
|
15
|
+
# superseded check runs before registering the new ones — for a few seconds
|
|
16
|
+
# there are **zero** checks, `pending` is 0, and the loop exits on a PR whose CI
|
|
17
|
+
# has not started. The caller then merges. Observed on 2026-08-02 while clearing
|
|
18
|
+
# a 13-repo queue; the same session also wrote an `until` whose `|| &&`
|
|
19
|
+
# precedence never terminated and burned a full 10-minute timeout.
|
|
20
|
+
#
|
|
21
|
+
# That is the estate's dominant failure shape — a gate passing because it cannot
|
|
22
|
+
# run — reproduced inside the agent's own tooling, where no CI guard can see it.
|
|
23
|
+
#
|
|
24
|
+
# ## The rule this encodes
|
|
25
|
+
#
|
|
26
|
+
# **Wait on a positive signal, never on the absence of a negative.** Two ways to
|
|
27
|
+
# get one, strongest first:
|
|
28
|
+
#
|
|
29
|
+
# 1. **Branch protection's required contexts.** If the base branch is protected,
|
|
30
|
+
# those names are exactly the checks that MUST report, so "every required
|
|
31
|
+
# context has concluded" is a direct answer rather than an inference. This is
|
|
32
|
+
# the only condition that cannot be satisfied by an empty or half-registered
|
|
33
|
+
# set.
|
|
34
|
+
# 2. **Stability, when protection is unreadable.** Some repos are unprotected
|
|
35
|
+
# (both plugin repos were until 2026-07-27) and a token may lack the scope to
|
|
36
|
+
# read protection. Then: at least one check present, all concluded, and the
|
|
37
|
+
# same count seen on two consecutive polls — so a fast check concluding while
|
|
38
|
+
# slower ones are still registering does not end the wait early.
|
|
39
|
+
#
|
|
40
|
+
# ## Exit codes, and why 2 exists
|
|
41
|
+
#
|
|
42
|
+
# 0 every required/observed check concluded, none failed
|
|
43
|
+
# 1 a check failed — the names are printed
|
|
44
|
+
# 2 cannot determine: timed out, no checks ever appeared, PR unreadable
|
|
45
|
+
#
|
|
46
|
+
# 2 is distinct from 1 on purpose, and neither is 0. A timeout is not a pass,
|
|
47
|
+
# and a caller that treats "cannot tell" as "green" has rebuilt the defect this
|
|
48
|
+
# script exists to prevent. `ci-wiring-audit.sh` uses the same 2-means-cannot-run
|
|
49
|
+
# convention.
|
|
50
|
+
#
|
|
51
|
+
# `cancelled` is reported separately rather than as a failure: on this estate's
|
|
52
|
+
# self-hosted runners a cancelled job is usually spot reclamation or a
|
|
53
|
+
# `cancel-in-progress` concurrency group, not the code. It still exits 1 —
|
|
54
|
+
# something must be re-run — but the message says which, so nobody debugs a
|
|
55
|
+
# phantom.
|
|
56
|
+
#
|
|
57
|
+
# ## Usage
|
|
58
|
+
#
|
|
59
|
+
# sh scripts/wait-for-checks.sh <pr-number> [-R owner/repo]
|
|
60
|
+
# [--timeout SECONDS] [--interval SECONDS]
|
|
61
|
+
#
|
|
62
|
+
# Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary is needed.
|
|
63
|
+
|
|
64
|
+
set -uo pipefail
|
|
65
|
+
|
|
66
|
+
PR=""
|
|
67
|
+
REPO=""
|
|
68
|
+
TIMEOUT="${WAIT_FOR_CHECKS_TIMEOUT:-1800}"
|
|
69
|
+
INTERVAL="${WAIT_FOR_CHECKS_INTERVAL:-30}"
|
|
70
|
+
|
|
71
|
+
usage() {
|
|
72
|
+
sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//'
|
|
73
|
+
exit 2
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while [ $# -gt 0 ]; do
|
|
77
|
+
case "$1" in
|
|
78
|
+
-R | --repo)
|
|
79
|
+
REPO="${2:-}"
|
|
80
|
+
shift 2
|
|
81
|
+
;;
|
|
82
|
+
--timeout)
|
|
83
|
+
TIMEOUT="${2:-}"
|
|
84
|
+
shift 2
|
|
85
|
+
;;
|
|
86
|
+
--interval)
|
|
87
|
+
INTERVAL="${2:-}"
|
|
88
|
+
shift 2
|
|
89
|
+
;;
|
|
90
|
+
-h | --help) usage ;;
|
|
91
|
+
*)
|
|
92
|
+
PR="$1"
|
|
93
|
+
shift
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
[ -n "$PR" ] || {
|
|
99
|
+
echo "wait-for-checks: no PR number given" >&2
|
|
100
|
+
usage
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
RED=$(printf '\033[31m')
|
|
104
|
+
GREEN=$(printf '\033[32m')
|
|
105
|
+
DIM=$(printf '\033[90m')
|
|
106
|
+
OFF=$(printf '\033[0m')
|
|
107
|
+
|
|
108
|
+
gh_pr() {
|
|
109
|
+
if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
gh_api() {
|
|
113
|
+
gh api "$@" 2>/dev/null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# --- What is this PR, and is there anything to wait for? ----------------------
|
|
117
|
+
|
|
118
|
+
meta=$(gh_pr view "$PR" --json state,baseRefName --jq '"\(.state)\t\(.baseRefName)"') || {
|
|
119
|
+
echo "${RED}wait-for-checks: cannot read PR $PR${OFF}" >&2
|
|
120
|
+
exit 2
|
|
121
|
+
}
|
|
122
|
+
state=${meta%% *}
|
|
123
|
+
base=${meta##* }
|
|
124
|
+
|
|
125
|
+
case "$state" in
|
|
126
|
+
MERGED | CLOSED)
|
|
127
|
+
echo "${DIM}PR $PR is $state — nothing to wait for.${OFF}"
|
|
128
|
+
exit 0
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
# --- Signal 1: the checks branch protection says MUST report ------------------
|
|
133
|
+
|
|
134
|
+
owner_repo="$REPO"
|
|
135
|
+
[ -n "$owner_repo" ] || owner_repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null)
|
|
136
|
+
|
|
137
|
+
required=""
|
|
138
|
+
if [ -n "$owner_repo" ]; then
|
|
139
|
+
required=$(gh_api "repos/$owner_repo/branches/$base/protection" \
|
|
140
|
+
--jq '.required_status_checks.contexts[]?' | sort -u)
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [ -n "$required" ]; then
|
|
144
|
+
echo "${DIM}Waiting on $(echo "$required" | wc -l | tr -d ' ') required check(s) on $base.${OFF}"
|
|
145
|
+
else
|
|
146
|
+
# Not an error. An unprotected branch is a real configuration, and a token
|
|
147
|
+
# without the scope to read protection is common. Say which mode is in use, so
|
|
148
|
+
# a weaker guarantee is never mistaken for the strong one.
|
|
149
|
+
echo "${DIM}No readable branch protection on $base — falling back to stability.${OFF}"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# --- Poll ---------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
deadline=$(($(date +%s) + TIMEOUT))
|
|
155
|
+
prev_count=-1
|
|
156
|
+
rollup=""
|
|
157
|
+
|
|
158
|
+
while :; do
|
|
159
|
+
rollup=$(gh_pr view "$PR" --json statusCheckRollup --jq '
|
|
160
|
+
[ .statusCheckRollup[]?
|
|
161
|
+
| { name: (.name // .context),
|
|
162
|
+
state: (.conclusion // .state // (if .status == "COMPLETED" then "" else null end))
|
|
163
|
+
}
|
|
164
|
+
] | .[] | "\(.name)\t\(.state // "")"') || rollup=""
|
|
165
|
+
|
|
166
|
+
count=0
|
|
167
|
+
[ -n "$rollup" ] && count=$(printf '%s\n' "$rollup" | grep -c .)
|
|
168
|
+
|
|
169
|
+
# Every check that has reported a terminal state.
|
|
170
|
+
concluded=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && $2 != "" && $2 != "PENDING" && $2 != "IN_PROGRESS" && $2 != "QUEUED" && $2 != "WAITING" { print $1 }')
|
|
171
|
+
|
|
172
|
+
done_waiting=0
|
|
173
|
+
|
|
174
|
+
if [ -n "$required" ]; then
|
|
175
|
+
# Strong condition: every required context is present AND concluded.
|
|
176
|
+
missing=""
|
|
177
|
+
while IFS= read -r ctx; do
|
|
178
|
+
[ -n "$ctx" ] || continue
|
|
179
|
+
printf '%s\n' "$concluded" | grep -Fxq "$ctx" || missing="$missing $ctx"
|
|
180
|
+
done <<EOF
|
|
181
|
+
$required
|
|
182
|
+
EOF
|
|
183
|
+
[ -z "$missing" ] && done_waiting=1
|
|
184
|
+
else
|
|
185
|
+
# Fallback: at least one check, all concluded, and the set has stopped
|
|
186
|
+
# growing. The count check is what stops a fast Secret Scan concluding alone
|
|
187
|
+
# while five slower jobs are still being registered.
|
|
188
|
+
if [ "$count" -gt 0 ]; then
|
|
189
|
+
n_concluded=$(printf '%s\n' "$concluded" | grep -c .)
|
|
190
|
+
if [ "$n_concluded" = "$count" ] && [ "$count" = "$prev_count" ]; then
|
|
191
|
+
done_waiting=1
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
[ "$done_waiting" = "1" ] && break
|
|
197
|
+
|
|
198
|
+
prev_count=$count
|
|
199
|
+
|
|
200
|
+
now=$(date +%s)
|
|
201
|
+
if [ "$now" -ge "$deadline" ]; then
|
|
202
|
+
echo "${RED}wait-for-checks: timed out after ${TIMEOUT}s.${OFF}" >&2
|
|
203
|
+
if [ "$count" = "0" ]; then
|
|
204
|
+
# The exact case the naive loop gets wrong, so name it explicitly.
|
|
205
|
+
echo "No checks ever appeared on PR $PR. That is 'cannot tell', not 'green'." >&2
|
|
206
|
+
else
|
|
207
|
+
echo "Still unfinished:" >&2
|
|
208
|
+
printf '%s\n' "$rollup" | awk -F'\t' 'NF && ($2 == "" || $2 == "PENDING" || $2 == "IN_PROGRESS" || $2 == "QUEUED" || $2 == "WAITING") { print " " $1 }' >&2
|
|
209
|
+
fi
|
|
210
|
+
exit 2
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
sleep "$INTERVAL"
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
# --- Report -------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
failed=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && ($2 == "FAILURE" || $2 == "TIMED_OUT" || $2 == "ACTION_REQUIRED" || $2 == "STARTUP_FAILURE" || $2 == "ERROR") { print " " $1 " (" $2 ")" }')
|
|
219
|
+
cancelled=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && $2 == "CANCELLED" { print " " $1 }')
|
|
220
|
+
|
|
221
|
+
if [ -n "$cancelled" ]; then
|
|
222
|
+
echo "${RED}Cancelled:${OFF}"
|
|
223
|
+
printf '%s\n' "$cancelled"
|
|
224
|
+
echo "${DIM}A cancelled check is usually infrastructure (spot reclamation, or a" >&2
|
|
225
|
+
echo "cancel-in-progress concurrency group), not your code. Re-run it rather" >&2
|
|
226
|
+
echo "than debugging a phantom.${OFF}" >&2
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
if [ -n "$failed" ]; then
|
|
230
|
+
echo "${RED}Failed:${OFF}"
|
|
231
|
+
printf '%s\n' "$failed"
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
[ -n "$cancelled" ] && exit 1
|
|
236
|
+
|
|
237
|
+
echo "${GREEN}All checks concluded, none failed.${OFF}"
|
|
238
|
+
exit 0
|
|
@@ -60,6 +60,19 @@ This file is distributed by the template and kept in step by
|
|
|
60
60
|
|
|
61
61
|
- Get CI green and confirm it: `gh pr checks <N>`. A green local run is not
|
|
62
62
|
sufficient — verify the actual PR checks.
|
|
63
|
+
- **Wait with `scripts/wait-for-checks.sh`, not a hand-rolled loop:**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sh scripts/wait-for-checks.sh <N> # 0 green · 1 failed · 2 cannot tell
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Do not write your own `until … grep -c pending … done`. That polls for the
|
|
70
|
+
**absence** of pending checks, so the empty window right after
|
|
71
|
+
`gh pr update-branch` — superseded runs dropped, new ones not yet registered —
|
|
72
|
+
reads as "all green" and merges a PR whose CI has not started. The script
|
|
73
|
+
waits on a **positive** signal instead, and its **exit 2 means "cannot tell",
|
|
74
|
+
which is never a pass**.
|
|
75
|
+
|
|
63
76
|
- **Squash-merge, delete the branch, remove the worktree:**
|
|
64
77
|
`gh pr merge <N> --squash --delete-branch`.
|
|
65
78
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Wait for a pull request's checks to finish, without mistaking "not started"
|
|
4
|
+
# for "all green".
|
|
5
|
+
#
|
|
6
|
+
# ## Why this exists
|
|
7
|
+
#
|
|
8
|
+
# Every session hand-rolls this loop, and the natural formulation is wrong in
|
|
9
|
+
# the dangerous direction:
|
|
10
|
+
#
|
|
11
|
+
# until [ "$(gh pr checks "$N" | grep -c pending)" = 0 ]; do sleep 30; done
|
|
12
|
+
#
|
|
13
|
+
# That polls for the **absence** of pending work, so a transient empty set reads
|
|
14
|
+
# as completion. Immediately after `gh pr update-branch` GitHub drops the
|
|
15
|
+
# superseded check runs before registering the new ones — for a few seconds
|
|
16
|
+
# there are **zero** checks, `pending` is 0, and the loop exits on a PR whose CI
|
|
17
|
+
# has not started. The caller then merges. Observed on 2026-08-02 while clearing
|
|
18
|
+
# a 13-repo queue; the same session also wrote an `until` whose `|| &&`
|
|
19
|
+
# precedence never terminated and burned a full 10-minute timeout.
|
|
20
|
+
#
|
|
21
|
+
# That is the estate's dominant failure shape — a gate passing because it cannot
|
|
22
|
+
# run — reproduced inside the agent's own tooling, where no CI guard can see it.
|
|
23
|
+
#
|
|
24
|
+
# ## The rule this encodes
|
|
25
|
+
#
|
|
26
|
+
# **Wait on a positive signal, never on the absence of a negative.** Two ways to
|
|
27
|
+
# get one, strongest first:
|
|
28
|
+
#
|
|
29
|
+
# 1. **Branch protection's required contexts.** If the base branch is protected,
|
|
30
|
+
# those names are exactly the checks that MUST report, so "every required
|
|
31
|
+
# context has concluded" is a direct answer rather than an inference. This is
|
|
32
|
+
# the only condition that cannot be satisfied by an empty or half-registered
|
|
33
|
+
# set.
|
|
34
|
+
# 2. **Stability, when protection is unreadable.** Some repos are unprotected
|
|
35
|
+
# (both plugin repos were until 2026-07-27) and a token may lack the scope to
|
|
36
|
+
# read protection. Then: at least one check present, all concluded, and the
|
|
37
|
+
# same count seen on two consecutive polls — so a fast check concluding while
|
|
38
|
+
# slower ones are still registering does not end the wait early.
|
|
39
|
+
#
|
|
40
|
+
# ## Exit codes, and why 2 exists
|
|
41
|
+
#
|
|
42
|
+
# 0 every required/observed check concluded, none failed
|
|
43
|
+
# 1 a check failed — the names are printed
|
|
44
|
+
# 2 cannot determine: timed out, no checks ever appeared, PR unreadable
|
|
45
|
+
#
|
|
46
|
+
# 2 is distinct from 1 on purpose, and neither is 0. A timeout is not a pass,
|
|
47
|
+
# and a caller that treats "cannot tell" as "green" has rebuilt the defect this
|
|
48
|
+
# script exists to prevent. `ci-wiring-audit.sh` uses the same 2-means-cannot-run
|
|
49
|
+
# convention.
|
|
50
|
+
#
|
|
51
|
+
# `cancelled` is reported separately rather than as a failure: on this estate's
|
|
52
|
+
# self-hosted runners a cancelled job is usually spot reclamation or a
|
|
53
|
+
# `cancel-in-progress` concurrency group, not the code. It still exits 1 —
|
|
54
|
+
# something must be re-run — but the message says which, so nobody debugs a
|
|
55
|
+
# phantom.
|
|
56
|
+
#
|
|
57
|
+
# ## Usage
|
|
58
|
+
#
|
|
59
|
+
# sh scripts/wait-for-checks.sh <pr-number> [-R owner/repo]
|
|
60
|
+
# [--timeout SECONDS] [--interval SECONDS]
|
|
61
|
+
#
|
|
62
|
+
# Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary is needed.
|
|
63
|
+
|
|
64
|
+
set -uo pipefail
|
|
65
|
+
|
|
66
|
+
PR=""
|
|
67
|
+
REPO=""
|
|
68
|
+
TIMEOUT="${WAIT_FOR_CHECKS_TIMEOUT:-1800}"
|
|
69
|
+
INTERVAL="${WAIT_FOR_CHECKS_INTERVAL:-30}"
|
|
70
|
+
|
|
71
|
+
usage() {
|
|
72
|
+
sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//'
|
|
73
|
+
exit 2
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while [ $# -gt 0 ]; do
|
|
77
|
+
case "$1" in
|
|
78
|
+
-R | --repo)
|
|
79
|
+
REPO="${2:-}"
|
|
80
|
+
shift 2
|
|
81
|
+
;;
|
|
82
|
+
--timeout)
|
|
83
|
+
TIMEOUT="${2:-}"
|
|
84
|
+
shift 2
|
|
85
|
+
;;
|
|
86
|
+
--interval)
|
|
87
|
+
INTERVAL="${2:-}"
|
|
88
|
+
shift 2
|
|
89
|
+
;;
|
|
90
|
+
-h | --help) usage ;;
|
|
91
|
+
*)
|
|
92
|
+
PR="$1"
|
|
93
|
+
shift
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
[ -n "$PR" ] || {
|
|
99
|
+
echo "wait-for-checks: no PR number given" >&2
|
|
100
|
+
usage
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
RED=$(printf '\033[31m')
|
|
104
|
+
GREEN=$(printf '\033[32m')
|
|
105
|
+
DIM=$(printf '\033[90m')
|
|
106
|
+
OFF=$(printf '\033[0m')
|
|
107
|
+
|
|
108
|
+
gh_pr() {
|
|
109
|
+
if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
gh_api() {
|
|
113
|
+
gh api "$@" 2>/dev/null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# --- What is this PR, and is there anything to wait for? ----------------------
|
|
117
|
+
|
|
118
|
+
meta=$(gh_pr view "$PR" --json state,baseRefName --jq '"\(.state)\t\(.baseRefName)"') || {
|
|
119
|
+
echo "${RED}wait-for-checks: cannot read PR $PR${OFF}" >&2
|
|
120
|
+
exit 2
|
|
121
|
+
}
|
|
122
|
+
state=${meta%% *}
|
|
123
|
+
base=${meta##* }
|
|
124
|
+
|
|
125
|
+
case "$state" in
|
|
126
|
+
MERGED | CLOSED)
|
|
127
|
+
echo "${DIM}PR $PR is $state — nothing to wait for.${OFF}"
|
|
128
|
+
exit 0
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
# --- Signal 1: the checks branch protection says MUST report ------------------
|
|
133
|
+
|
|
134
|
+
owner_repo="$REPO"
|
|
135
|
+
[ -n "$owner_repo" ] || owner_repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null)
|
|
136
|
+
|
|
137
|
+
required=""
|
|
138
|
+
if [ -n "$owner_repo" ]; then
|
|
139
|
+
required=$(gh_api "repos/$owner_repo/branches/$base/protection" \
|
|
140
|
+
--jq '.required_status_checks.contexts[]?' | sort -u)
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [ -n "$required" ]; then
|
|
144
|
+
echo "${DIM}Waiting on $(echo "$required" | wc -l | tr -d ' ') required check(s) on $base.${OFF}"
|
|
145
|
+
else
|
|
146
|
+
# Not an error. An unprotected branch is a real configuration, and a token
|
|
147
|
+
# without the scope to read protection is common. Say which mode is in use, so
|
|
148
|
+
# a weaker guarantee is never mistaken for the strong one.
|
|
149
|
+
echo "${DIM}No readable branch protection on $base — falling back to stability.${OFF}"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# --- Poll ---------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
deadline=$(($(date +%s) + TIMEOUT))
|
|
155
|
+
prev_count=-1
|
|
156
|
+
rollup=""
|
|
157
|
+
|
|
158
|
+
while :; do
|
|
159
|
+
rollup=$(gh_pr view "$PR" --json statusCheckRollup --jq '
|
|
160
|
+
[ .statusCheckRollup[]?
|
|
161
|
+
| { name: (.name // .context),
|
|
162
|
+
state: (.conclusion // .state // (if .status == "COMPLETED" then "" else null end))
|
|
163
|
+
}
|
|
164
|
+
] | .[] | "\(.name)\t\(.state // "")"') || rollup=""
|
|
165
|
+
|
|
166
|
+
count=0
|
|
167
|
+
[ -n "$rollup" ] && count=$(printf '%s\n' "$rollup" | grep -c .)
|
|
168
|
+
|
|
169
|
+
# Every check that has reported a terminal state.
|
|
170
|
+
concluded=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && $2 != "" && $2 != "PENDING" && $2 != "IN_PROGRESS" && $2 != "QUEUED" && $2 != "WAITING" { print $1 }')
|
|
171
|
+
|
|
172
|
+
done_waiting=0
|
|
173
|
+
|
|
174
|
+
if [ -n "$required" ]; then
|
|
175
|
+
# Strong condition: every required context is present AND concluded.
|
|
176
|
+
missing=""
|
|
177
|
+
while IFS= read -r ctx; do
|
|
178
|
+
[ -n "$ctx" ] || continue
|
|
179
|
+
printf '%s\n' "$concluded" | grep -Fxq "$ctx" || missing="$missing $ctx"
|
|
180
|
+
done <<EOF
|
|
181
|
+
$required
|
|
182
|
+
EOF
|
|
183
|
+
[ -z "$missing" ] && done_waiting=1
|
|
184
|
+
else
|
|
185
|
+
# Fallback: at least one check, all concluded, and the set has stopped
|
|
186
|
+
# growing. The count check is what stops a fast Secret Scan concluding alone
|
|
187
|
+
# while five slower jobs are still being registered.
|
|
188
|
+
if [ "$count" -gt 0 ]; then
|
|
189
|
+
n_concluded=$(printf '%s\n' "$concluded" | grep -c .)
|
|
190
|
+
if [ "$n_concluded" = "$count" ] && [ "$count" = "$prev_count" ]; then
|
|
191
|
+
done_waiting=1
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
[ "$done_waiting" = "1" ] && break
|
|
197
|
+
|
|
198
|
+
prev_count=$count
|
|
199
|
+
|
|
200
|
+
now=$(date +%s)
|
|
201
|
+
if [ "$now" -ge "$deadline" ]; then
|
|
202
|
+
echo "${RED}wait-for-checks: timed out after ${TIMEOUT}s.${OFF}" >&2
|
|
203
|
+
if [ "$count" = "0" ]; then
|
|
204
|
+
# The exact case the naive loop gets wrong, so name it explicitly.
|
|
205
|
+
echo "No checks ever appeared on PR $PR. That is 'cannot tell', not 'green'." >&2
|
|
206
|
+
else
|
|
207
|
+
echo "Still unfinished:" >&2
|
|
208
|
+
printf '%s\n' "$rollup" | awk -F'\t' 'NF && ($2 == "" || $2 == "PENDING" || $2 == "IN_PROGRESS" || $2 == "QUEUED" || $2 == "WAITING") { print " " $1 }' >&2
|
|
209
|
+
fi
|
|
210
|
+
exit 2
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
sleep "$INTERVAL"
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
# --- Report -------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
failed=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && ($2 == "FAILURE" || $2 == "TIMED_OUT" || $2 == "ACTION_REQUIRED" || $2 == "STARTUP_FAILURE" || $2 == "ERROR") { print " " $1 " (" $2 ")" }')
|
|
219
|
+
cancelled=$(printf '%s\n' "$rollup" | awk -F'\t' 'NF && $2 == "CANCELLED" { print " " $1 }')
|
|
220
|
+
|
|
221
|
+
if [ -n "$cancelled" ]; then
|
|
222
|
+
echo "${RED}Cancelled:${OFF}"
|
|
223
|
+
printf '%s\n' "$cancelled"
|
|
224
|
+
echo "${DIM}A cancelled check is usually infrastructure (spot reclamation, or a" >&2
|
|
225
|
+
echo "cancel-in-progress concurrency group), not your code. Re-run it rather" >&2
|
|
226
|
+
echo "than debugging a phantom.${OFF}" >&2
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
if [ -n "$failed" ]; then
|
|
230
|
+
echo "${RED}Failed:${OFF}"
|
|
231
|
+
printf '%s\n' "$failed"
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
[ -n "$cancelled" ] && exit 1
|
|
236
|
+
|
|
237
|
+
echo "${GREEN}All checks concluded, none failed.${OFF}"
|
|
238
|
+
exit 0
|