@biffo/cli 0.296.12 → 0.296.14
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.
- package/package.json +1 -1
- package/scripts/branch-health.sh +50 -5
- package/scripts/wait-for-checks.sh +88 -2
package/package.json
CHANGED
package/scripts/branch-health.sh
CHANGED
|
@@ -54,6 +54,18 @@
|
|
|
54
54
|
# merges landing seconds apart cancel the first run by design. It is called out
|
|
55
55
|
# by name so nobody debugs a phantom.
|
|
56
56
|
#
|
|
57
|
+
# `plan-only` (#1582) is likewise reported but does not fail the branch — a
|
|
58
|
+
# `Deploy Infrastructure` dispatch left at the default `action: plan` really did
|
|
59
|
+
# succeed at planning, and that can be entirely deliberate (an operator running
|
|
60
|
+
# a dry run). What it must never do is share the plain `ok` label a real apply
|
|
61
|
+
# gets: `conclusion` is "success" either way, so the ONLY signal that tells them
|
|
62
|
+
# apart is the run's own title (`displayTitle`), which #1678 made carry a
|
|
63
|
+
# `PLAN ONLY ... (nothing applied)` marker for exactly this. This script fails
|
|
64
|
+
# CLOSED on that signal going missing — see the summary query below, where
|
|
65
|
+
# `displayTitle`'s absence collapses the whole `gh run list` call to empty
|
|
66
|
+
# output, which is already handled as exit 2 ("cannot tell"), not as a silent
|
|
67
|
+
# `ok`.
|
|
68
|
+
#
|
|
57
69
|
# ## Usage
|
|
58
70
|
#
|
|
59
71
|
# sh scripts/branch-health.sh [-R owner/repo] [--branch dev] [--quiet]
|
|
@@ -67,7 +79,7 @@ BRANCH=""
|
|
|
67
79
|
QUIET=""
|
|
68
80
|
|
|
69
81
|
usage() {
|
|
70
|
-
sed -n '2,
|
|
82
|
+
sed -n '2,73p' "$0" | sed 's/^# \{0,1\}//'
|
|
71
83
|
exit 2
|
|
72
84
|
}
|
|
73
85
|
|
|
@@ -144,13 +156,24 @@ label=${REPO:-$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")}
|
|
|
144
156
|
# list": the ordering is gh's to change, and a status tool that quietly reports
|
|
145
157
|
# an older run because an API changed its sort is the same class of defect as the
|
|
146
158
|
# truncated list this replaces. Ask for the newest explicitly.
|
|
159
|
+
#
|
|
160
|
+
# `displayTitle` is requested here for #1582's second half: a `workflow_dispatch`
|
|
161
|
+
# left at the default `action: plan` runs its plan step for real and concludes
|
|
162
|
+
# "success" — conclusion alone can never tell that apart from a real apply, by
|
|
163
|
+
# design (#1582's whole point). #1678 fixed the Actions-UI half by giving such a
|
|
164
|
+
# run a `run-name` that says "PLAN ONLY ... (nothing applied)"; this line is what
|
|
165
|
+
# lets branch-health.sh see that same marker instead of rendering the run as a
|
|
166
|
+
# plain, indistinguishable "ok". The per-workflow history query further below
|
|
167
|
+
# already requests this field for a different reason (naming who broke a
|
|
168
|
+
# failure) — same field, independent reason to want it here.
|
|
147
169
|
summary=$(gh_run list --branch "$BRANCH" --limit 200 \
|
|
148
|
-
--json workflowName,status,conclusion,headSha,createdAt,url,event \
|
|
170
|
+
--json workflowName,status,conclusion,headSha,createdAt,url,event,displayTitle \
|
|
149
171
|
--jq 'group_by(.workflowName)
|
|
150
172
|
| map(max_by(.createdAt))
|
|
151
173
|
| .[]
|
|
152
174
|
| [ (if .status == "completed" then (.conclusion // "unknown") else .status end),
|
|
153
|
-
.workflowName, .headSha[0:8], .createdAt[0:16], .url, .event
|
|
175
|
+
.workflowName, .headSha[0:8], .createdAt[0:16], .url, .event,
|
|
176
|
+
(.displayTitle // "") ]
|
|
154
177
|
| @tsv' 2>/dev/null)
|
|
155
178
|
|
|
156
179
|
if [ -z "$summary" ]; then
|
|
@@ -164,11 +187,24 @@ pending=""
|
|
|
164
187
|
cancelled=""
|
|
165
188
|
skipped=""
|
|
166
189
|
ok=""
|
|
190
|
+
planonly=""
|
|
167
191
|
|
|
168
|
-
while IFS="$TAB" read -r state name sha when url event; do
|
|
192
|
+
while IFS="$TAB" read -r state name sha when url event title; do
|
|
169
193
|
[ -n "$name" ] || continue
|
|
170
194
|
case "$state" in
|
|
171
|
-
success)
|
|
195
|
+
success)
|
|
196
|
+
# A literal, upper-case "PLAN ONLY" is the #1678 run-name marker
|
|
197
|
+
# (`format('PLAN ONLY {0} (nothing applied)', ...)`) — never something a
|
|
198
|
+
# commit-subject-derived title produces by coincidence (this repo's own
|
|
199
|
+
# history has "plan-only" and "plan-time" in commit subjects, always
|
|
200
|
+
# lower-case, and grep confirms zero for the exact upper-case phrase).
|
|
201
|
+
# A "success" run whose title carries it applied nothing and must not
|
|
202
|
+
# collapse into the same "ok" bucket as a real apply.
|
|
203
|
+
case "$title" in
|
|
204
|
+
*"PLAN ONLY"*) planonly="${planonly}${name}\n" ;;
|
|
205
|
+
*) ok="${ok}${name}\n" ;;
|
|
206
|
+
esac
|
|
207
|
+
;;
|
|
172
208
|
failure | timed_out | startup_failure)
|
|
173
209
|
failed="${failed}${state}\t${name}\t${sha}\t${when}\t${url}\t${event}\n"
|
|
174
210
|
;;
|
|
@@ -193,6 +229,10 @@ echo
|
|
|
193
229
|
[ -n "$ok" ] && printf '%b' "$ok" | sed "s/^/ ${GREEN}ok${OFF} /"
|
|
194
230
|
[ -n "$skipped" ] && printf '%b' "$skipped" | sed "s/^/ ${DIM}skipped${OFF} /"
|
|
195
231
|
[ -n "$cancelled" ] && printf '%b' "$cancelled" | sed "s/^/ ${YELLOW}cancelled${OFF} /"
|
|
232
|
+
# Own label, not "ok" — #1582's second half. A skim-reader tells rows apart by
|
|
233
|
+
# this left-hand column, not by reading every run's title, so the row itself
|
|
234
|
+
# has to say "nothing applied" rather than reusing the label a real apply gets.
|
|
235
|
+
[ -n "$planonly" ] && printf '%b' "$planonly" | sed "s/^/ ${YELLOW}plan-only${OFF} /" | sed "s/\$/ — nothing applied/"
|
|
196
236
|
|
|
197
237
|
if [ -n "$pending" ]; then
|
|
198
238
|
printf '%b' "$pending" | awk -F'\t' -v d="$YELLOW" -v o="$OFF" 'NF{printf " %srunning%s %s (%s)\n", d, o, $2, $1}'
|
|
@@ -204,6 +244,11 @@ if [ -z "$failed" ]; then
|
|
|
204
244
|
echo "${DIM}A cancelled run is usually spot reclamation or a superseded concurrency${OFF}"
|
|
205
245
|
echo "${DIM}group, not the code. Re-run it rather than debugging it.${OFF}"
|
|
206
246
|
fi
|
|
247
|
+
if [ -n "$planonly" ]; then
|
|
248
|
+
echo
|
|
249
|
+
echo "${DIM}A plan-only dispatch did not apply anything — Terraform state is${OFF}"
|
|
250
|
+
echo "${DIM}unchanged for that environment. Not a failure, but not a deploy either.${OFF}"
|
|
251
|
+
fi
|
|
207
252
|
echo
|
|
208
253
|
echo "${GREEN}Nothing on '$BRANCH' is failing.${OFF}"
|
|
209
254
|
exit 0
|
|
@@ -98,10 +98,37 @@
|
|
|
98
98
|
# anything keep waiting on the checks as before. An unreadable field
|
|
99
99
|
# else (old gh, missing scope) must never become a verdict.
|
|
100
100
|
#
|
|
101
|
+
# ## A wait that outlives its caller is worse than no wait
|
|
102
|
+
#
|
|
103
|
+
# This script's timeout says how long IT will wait. It said nothing about how
|
|
104
|
+
# long its CALLER has left, and that gap loses whole sessions.
|
|
105
|
+
#
|
|
106
|
+
# Measured 2026-08-22, biffo-fleet Foreman `7d362ba7`, which runs under
|
|
107
|
+
# `timeout 3300`. It pushed a commit at 05:37:03 and started this script at
|
|
108
|
+
# 05:37:04 with about five minutes of its 55-minute budget left. CI was
|
|
109
|
+
# genuinely in flight and would have concluded at 05:48:18 — a correct ~11
|
|
110
|
+
# minute wait. The session was SIGKILLed at 05:42:45, this script returned
|
|
111
|
+
# **137**, and the turn was lost mid-flight: no verdict, no cost record, the
|
|
112
|
+
# tick reporting `cost=UNKNOWN tokens=UNKNOWN`. Nothing was wrong with the
|
|
113
|
+
# checks or with the waiting; the wait could not fit in the time left and
|
|
114
|
+
# neither side knew it. It was the eleventh such kill in three days.
|
|
115
|
+
#
|
|
116
|
+
# So a caller that knows when it dies can say so:
|
|
117
|
+
#
|
|
118
|
+
# WAIT_FOR_CHECKS_DEADLINE absolute unix epoch the CALLER dies at
|
|
119
|
+
# WAIT_FOR_CHECKS_MARGIN seconds to leave it to react (default 60)
|
|
120
|
+
#
|
|
121
|
+
# The effective deadline becomes the EARLIER of its own timeout and that bound,
|
|
122
|
+
# and if not even one poll fits it exits 2 immediately — before any API call —
|
|
123
|
+
# rather than starting a wait it cannot finish. Both remain exit 2, "cannot
|
|
124
|
+
# tell", never a pass: "ran out of time" and "checks are green" must never be
|
|
125
|
+
# the same answer. Unset, nothing changes.
|
|
126
|
+
#
|
|
101
127
|
# ## Usage
|
|
102
128
|
#
|
|
103
129
|
# sh scripts/wait-for-checks.sh <pr-number> [-R owner/repo]
|
|
104
130
|
# [--timeout SECONDS] [--interval SECONDS]
|
|
131
|
+
# [--deadline EPOCH]
|
|
105
132
|
#
|
|
106
133
|
# Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary is needed.
|
|
107
134
|
|
|
@@ -111,6 +138,8 @@ PR=""
|
|
|
111
138
|
REPO=""
|
|
112
139
|
TIMEOUT="${WAIT_FOR_CHECKS_TIMEOUT:-1800}"
|
|
113
140
|
INTERVAL="${WAIT_FOR_CHECKS_INTERVAL:-30}"
|
|
141
|
+
SESSION_DEADLINE="${WAIT_FOR_CHECKS_DEADLINE:-}"
|
|
142
|
+
MARGIN="${WAIT_FOR_CHECKS_MARGIN:-60}"
|
|
114
143
|
|
|
115
144
|
usage() {
|
|
116
145
|
# Print the whole header block, however long it grows: from line 2 up to the
|
|
@@ -134,6 +163,10 @@ while [ $# -gt 0 ]; do
|
|
|
134
163
|
INTERVAL="${2:-}"
|
|
135
164
|
shift 2
|
|
136
165
|
;;
|
|
166
|
+
--deadline)
|
|
167
|
+
SESSION_DEADLINE="${2:-}"
|
|
168
|
+
shift 2
|
|
169
|
+
;;
|
|
137
170
|
-h | --help) usage ;;
|
|
138
171
|
*)
|
|
139
172
|
PR="$1"
|
|
@@ -152,6 +185,24 @@ GREEN=$(printf '\033[32m')
|
|
|
152
185
|
DIM=$(printf '\033[90m')
|
|
153
186
|
OFF=$(printf '\033[0m')
|
|
154
187
|
|
|
188
|
+
# --- Bound the wait by the CALLER's life, not only by our own timeout ----------
|
|
189
|
+
#
|
|
190
|
+
# A non-numeric or empty bound is IGNORED rather than read as zero: an unreadable
|
|
191
|
+
# value must never become "no time left", which would turn a caller's typo into a
|
|
192
|
+
# script that refuses to wait for anything.
|
|
193
|
+
deadline=$(($(date +%s) + TIMEOUT))
|
|
194
|
+
bounded_by_caller=0
|
|
195
|
+
case "$SESSION_DEADLINE" in
|
|
196
|
+
'' | *[!0-9]*) : ;;
|
|
197
|
+
*)
|
|
198
|
+
caller_limit=$((SESSION_DEADLINE - MARGIN))
|
|
199
|
+
if [ "$caller_limit" -lt "$deadline" ]; then
|
|
200
|
+
deadline=$caller_limit
|
|
201
|
+
bounded_by_caller=1
|
|
202
|
+
fi
|
|
203
|
+
;;
|
|
204
|
+
esac
|
|
205
|
+
|
|
155
206
|
gh_pr() {
|
|
156
207
|
if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi
|
|
157
208
|
}
|
|
@@ -176,6 +227,33 @@ case "$state" in
|
|
|
176
227
|
;;
|
|
177
228
|
esac
|
|
178
229
|
|
|
230
|
+
# Not even one poll fits in what the CALLER has left. Refuse BEFORE the polling
|
|
231
|
+
# starts: being killed mid-wait costs the caller its whole turn, while exiting now
|
|
232
|
+
# leaves it time to record what it already knows.
|
|
233
|
+
#
|
|
234
|
+
# TWO PLACEMENT RULES, both learned by getting them wrong (cli/src/lib/
|
|
235
|
+
# wait-for-checks.test.ts caught both):
|
|
236
|
+
#
|
|
237
|
+
# 1. ONLY when a caller bound is actually in force. Keyed on the effective
|
|
238
|
+
# deadline alone, `--timeout 0` -- a deliberate, tested "one pass then
|
|
239
|
+
# report" -- started refusing to run at all. A caller's own short timeout is
|
|
240
|
+
# its own business; this bound is about the caller's LIFE, not its patience.
|
|
241
|
+
#
|
|
242
|
+
# 2. AFTER the MERGED/CLOSED fast path. Placed before it, an already-merged PR
|
|
243
|
+
# with no time left returned "cannot tell" instead of the exit 0 it had
|
|
244
|
+
# already earned. There is nothing to wait for there, so there is nothing to
|
|
245
|
+
# refuse. One state read is a cost worth paying to answer correctly.
|
|
246
|
+
if [ "$bounded_by_caller" = "1" ] && [ "$deadline" -le "$(( $(date +%s) + INTERVAL ))" ]; then
|
|
247
|
+
left=$(( deadline - $(date +%s) ))
|
|
248
|
+
[ "$left" -lt 0 ] && left=0
|
|
249
|
+
echo "${RED}wait-for-checks: not enough time left to wait for PR $PR.${OFF}" >&2
|
|
250
|
+
echo "The caller dies in ${left}s (margin ${MARGIN}s) and one poll takes ${INTERVAL}s," >&2
|
|
251
|
+
echo "so this would be killed mid-wait, losing the turn without a verdict." >&2
|
|
252
|
+
echo "Re-run with more time, or raise the caller's budget." >&2
|
|
253
|
+
echo "Not a failure and not a pass: this is 'cannot tell'." >&2
|
|
254
|
+
exit 2
|
|
255
|
+
fi
|
|
256
|
+
|
|
179
257
|
# --- Signal 1: the checks branch protection says MUST report ------------------
|
|
180
258
|
|
|
181
259
|
owner_repo="$REPO"
|
|
@@ -198,7 +276,7 @@ fi
|
|
|
198
276
|
|
|
199
277
|
# --- Poll ---------------------------------------------------------------------
|
|
200
278
|
|
|
201
|
-
deadline
|
|
279
|
+
# `deadline` and `bounded_by_caller` were settled above, before any API call.
|
|
202
280
|
prev_count=-1
|
|
203
281
|
rollup=""
|
|
204
282
|
|
|
@@ -298,7 +376,15 @@ EOF
|
|
|
298
376
|
|
|
299
377
|
now=$(date +%s)
|
|
300
378
|
if [ "$now" -ge "$deadline" ]; then
|
|
301
|
-
|
|
379
|
+
if [ "$bounded_by_caller" = "1" ]; then
|
|
380
|
+
# Distinct wording on purpose: "the caller ran out" sends you to its budget,
|
|
381
|
+
# "we ran out" sends you to CI. Same exit code, different fix.
|
|
382
|
+
echo "${RED}wait-for-checks: stopped early — the caller's deadline arrived.${OFF}" >&2
|
|
383
|
+
echo "Checks had not concluded. Waiting longer would have been killed" >&2
|
|
384
|
+
echo "mid-wait instead of returning this. Raise the caller's budget." >&2
|
|
385
|
+
else
|
|
386
|
+
echo "${RED}wait-for-checks: timed out after ${TIMEOUT}s.${OFF}" >&2
|
|
387
|
+
fi
|
|
302
388
|
if [ "$count" = "0" ]; then
|
|
303
389
|
# The exact case the naive loop gets wrong, so name it explicitly.
|
|
304
390
|
echo "No checks ever appeared on PR $PR. That is 'cannot tell', not 'green'." >&2
|