@biffo/cli 0.234.2 → 0.235.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.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Run the Biffo CLI, from whichever copy is correct for this repo.
4
+ #
5
+ # - **Template** (no `biffo.core.json`): the local `cli/` workspace, through
6
+ # tsx. The template develops the CLI, so its own CI has to exercise the code
7
+ # in the working tree — running the published package here would test the
8
+ # last release instead of the change under review, and a broken guard would
9
+ # go green and only fail after publish.
10
+ #
11
+ # - **Instance** (`biffo.core.json` present): the published package, pinned to
12
+ # the core version this instance is actually on. Instances no longer carry
13
+ # `cli/` at all: it is 31k lines of a scaffolding tool they never develop
14
+ # and never deploy, and shipping it meant the template's own test suite ran
15
+ # in every tenant's CI — where a failure could not be fixed by the repo it
16
+ # failed in.
17
+ #
18
+ # Pinning to `biffo.core.json` rather than `@latest` keeps the guards in step
19
+ # with the core the instance runs. An instance mid-upgrade is checked by the
20
+ # version it is upgrading from, which is the one whose rules its tree follows.
21
+ #
22
+ # That last sentence is why the version is read from `HEAD` and not the working
23
+ # tree (#667). `biffo core upgrade --apply` rewrites `biffo.core.json` to the
24
+ # TARGET version before committing, so by the time the `commit-msg` hook runs
25
+ # the working tree already names a version that, by definition, may not be
26
+ # published yet:
27
+ #
28
+ # npm error notarget No matching version found for @biffo/cli@0.133.1
29
+ # husky - commit-msg script failed (code 1)
30
+ #
31
+ # The upgrade then cannot be committed at all, and `--no-verify` does not help
32
+ # because CI runs the same check. Reading HEAD judges the commit by the rules
33
+ # the tree currently follows, and decouples committing an upgrade from whether
34
+ # the target has reached npm yet.
35
+ set -eu
36
+
37
+ root=$(git rev-parse --show-toplevel)
38
+ cd "$root"
39
+
40
+ # Satellites (sibling apps, plugin repos, runner fleets) carry neither
41
+ # `biffo.core.json` nor `cli/`. They DO carry `.biffo-shared-version`, written
42
+ # by `scripts/shared-sync.sh` and naming the template core version their shared
43
+ # files came from — the same pin, under a different name.
44
+ #
45
+ # Without this branch a satellite fell through to the template's `pnpm --filter`
46
+ # line and failed, so every guard a satellite ran had to be a COPIED shell
47
+ # script kept in step by hand. That is the drift surface `shared-files.json`
48
+ # exists to police: 16 files across 15 repos, roughly 240 copies, and about ten
49
+ # of the estate's guards written to police them. Giving satellites the same
50
+ # versioned CLI instances already use is what makes those copies unnecessary
51
+ # (#1109).
52
+ #
53
+ # The tag form is `core-v0.231.2`; npm wants `0.231.2`.
54
+ if [ ! -f biffo.core.json ] && [ -f .biffo-shared-version ]; then
55
+ shared=$(tr -d ' \t\n\r' < .biffo-shared-version)
56
+ version=${shared#core-v}
57
+ if [ -z "$version" ]; then
58
+ echo "biffo.sh: .biffo-shared-version is present but carries no readable version." >&2
59
+ exit 2
60
+ fi
61
+ exec npx --yes "@biffo/cli@$version" "$@"
62
+ fi
63
+
64
+ if [ -f biffo.core.json ]; then
65
+ # The committed record first. Falls through to the working tree when there is
66
+ # no HEAD yet (a fresh `biffo init`, before the first commit) or when the file
67
+ # is newly added and not yet in any commit.
68
+ version=$(git show HEAD:biffo.core.json 2>/dev/null |
69
+ node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo '')
70
+ if [ -z "$version" ] || [ "$version" = 'undefined' ]; then
71
+ version=$(node -p "require('./biffo.core.json').version" 2>/dev/null || echo '')
72
+ fi
73
+ if [ -z "$version" ] || [ "$version" = 'undefined' ]; then
74
+ echo "biffo.sh: biffo.core.json is present but carries no readable version." >&2
75
+ exit 2
76
+ fi
77
+ exec npx --yes "@biffo/cli@$version" "$@"
78
+ fi
79
+
80
+ # NOT `pnpm --filter @biffo/cli exec tsx ...`: `pnpm exec` normalises every
81
+ # non-zero exit to 1. Verified — the CLI exits 2 and pnpm reports 1.
82
+ #
83
+ # That silently flattened the exit code of every guard this bridge runs in the
84
+ # template, and it matters now because the estate's conventions are built on a
85
+ # THREE-valued contract: 0 green, 1 failed, **2 cannot tell**, where 2 is never
86
+ # a pass (`wait-for-checks.sh`, `branch-health.sh`, `claim.sh`). Collapsing 2
87
+ # into 1 turns "I could not tell" into "it failed", which is the safe direction
88
+ # by luck rather than design — and the reverse collapse would be a fail-open.
89
+ #
90
+ # `exec` on the binary directly keeps the child's status as this script's own.
91
+ exec "$root/cli/node_modules/.bin/tsx" "$root/cli/src/index.ts" "$@"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Run the Biffo CLI, from whichever copy is correct for this repo.
4
+ #
5
+ # - **Template** (no `biffo.core.json`): the local `cli/` workspace, through
6
+ # tsx. The template develops the CLI, so its own CI has to exercise the code
7
+ # in the working tree — running the published package here would test the
8
+ # last release instead of the change under review, and a broken guard would
9
+ # go green and only fail after publish.
10
+ #
11
+ # - **Instance** (`biffo.core.json` present): the published package, pinned to
12
+ # the core version this instance is actually on. Instances no longer carry
13
+ # `cli/` at all: it is 31k lines of a scaffolding tool they never develop
14
+ # and never deploy, and shipping it meant the template's own test suite ran
15
+ # in every tenant's CI — where a failure could not be fixed by the repo it
16
+ # failed in.
17
+ #
18
+ # Pinning to `biffo.core.json` rather than `@latest` keeps the guards in step
19
+ # with the core the instance runs. An instance mid-upgrade is checked by the
20
+ # version it is upgrading from, which is the one whose rules its tree follows.
21
+ #
22
+ # That last sentence is why the version is read from `HEAD` and not the working
23
+ # tree (#667). `biffo core upgrade --apply` rewrites `biffo.core.json` to the
24
+ # TARGET version before committing, so by the time the `commit-msg` hook runs
25
+ # the working tree already names a version that, by definition, may not be
26
+ # published yet:
27
+ #
28
+ # npm error notarget No matching version found for @biffo/cli@0.133.1
29
+ # husky - commit-msg script failed (code 1)
30
+ #
31
+ # The upgrade then cannot be committed at all, and `--no-verify` does not help
32
+ # because CI runs the same check. Reading HEAD judges the commit by the rules
33
+ # the tree currently follows, and decouples committing an upgrade from whether
34
+ # the target has reached npm yet.
35
+ set -eu
36
+
37
+ root=$(git rev-parse --show-toplevel)
38
+ cd "$root"
39
+
40
+ # Satellites (sibling apps, plugin repos, runner fleets) carry neither
41
+ # `biffo.core.json` nor `cli/`. They DO carry `.biffo-shared-version`, written
42
+ # by `scripts/shared-sync.sh` and naming the template core version their shared
43
+ # files came from — the same pin, under a different name.
44
+ #
45
+ # Without this branch a satellite fell through to the template's `pnpm --filter`
46
+ # line and failed, so every guard a satellite ran had to be a COPIED shell
47
+ # script kept in step by hand. That is the drift surface `shared-files.json`
48
+ # exists to police: 16 files across 15 repos, roughly 240 copies, and about ten
49
+ # of the estate's guards written to police them. Giving satellites the same
50
+ # versioned CLI instances already use is what makes those copies unnecessary
51
+ # (#1109).
52
+ #
53
+ # The tag form is `core-v0.231.2`; npm wants `0.231.2`.
54
+ if [ ! -f biffo.core.json ] && [ -f .biffo-shared-version ]; then
55
+ shared=$(tr -d ' \t\n\r' < .biffo-shared-version)
56
+ version=${shared#core-v}
57
+ if [ -z "$version" ]; then
58
+ echo "biffo.sh: .biffo-shared-version is present but carries no readable version." >&2
59
+ exit 2
60
+ fi
61
+ exec npx --yes "@biffo/cli@$version" "$@"
62
+ fi
63
+
64
+ if [ -f biffo.core.json ]; then
65
+ # The committed record first. Falls through to the working tree when there is
66
+ # no HEAD yet (a fresh `biffo init`, before the first commit) or when the file
67
+ # is newly added and not yet in any commit.
68
+ version=$(git show HEAD:biffo.core.json 2>/dev/null |
69
+ node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo '')
70
+ if [ -z "$version" ] || [ "$version" = 'undefined' ]; then
71
+ version=$(node -p "require('./biffo.core.json').version" 2>/dev/null || echo '')
72
+ fi
73
+ if [ -z "$version" ] || [ "$version" = 'undefined' ]; then
74
+ echo "biffo.sh: biffo.core.json is present but carries no readable version." >&2
75
+ exit 2
76
+ fi
77
+ exec npx --yes "@biffo/cli@$version" "$@"
78
+ fi
79
+
80
+ # NOT `pnpm --filter @biffo/cli exec tsx ...`: `pnpm exec` normalises every
81
+ # non-zero exit to 1. Verified — the CLI exits 2 and pnpm reports 1.
82
+ #
83
+ # That silently flattened the exit code of every guard this bridge runs in the
84
+ # template, and it matters now because the estate's conventions are built on a
85
+ # THREE-valued contract: 0 green, 1 failed, **2 cannot tell**, where 2 is never
86
+ # a pass (`wait-for-checks.sh`, `branch-health.sh`, `claim.sh`). Collapsing 2
87
+ # into 1 turns "I could not tell" into "it failed", which is the safe direction
88
+ # by luck rather than design — and the reverse collapse would be a fail-open.
89
+ #
90
+ # `exec` on the binary directly keeps the child's status as this script's own.
91
+ exec "$root/cli/node_modules/.bin/tsx" "$root/cli/src/index.ts" "$@"
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command26 } from "commander";
4
+ import { Command as Command27 } from "commander";
5
5
 
6
6
  // src/commands/core.ts
7
7
  import { Command as Command4 } from "commander";
@@ -9684,8 +9684,8 @@ async function runOwnershipCheck(argv) {
9684
9684
  const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
9685
9685
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9686
9686
  if (messageFile) {
9687
- const { readFileSync: readFileSync28, existsSync: existsSync38 } = await import("fs");
9688
- if (existsSync38(messageFile)) commitMessage = readFileSync28(messageFile, "utf8");
9687
+ const { readFileSync: readFileSync28, existsSync: existsSync39 } = await import("fs");
9688
+ if (existsSync39(messageFile)) commitMessage = readFileSync28(messageFile, "utf8");
9689
9689
  }
9690
9690
  } else {
9691
9691
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -10765,8 +10765,50 @@ function resolveGithubToken4() {
10765
10765
  );
10766
10766
  }
10767
10767
 
10768
+ // src/commands/wait-for-checks.ts
10769
+ import { spawnSync } from "child_process";
10770
+ import { dirname as dirname11 } from "path";
10771
+ import { fileURLToPath as fileURLToPath6 } from "url";
10772
+ import { Command as Command26 } from "commander";
10773
+
10774
+ // src/lib/packaged-scripts.ts
10775
+ import { existsSync as existsSync38 } from "fs";
10776
+ import { dirname as dirname10, join as join38 } from "path";
10777
+ function findPackagedScript(startDir, relativePath) {
10778
+ let dir = startDir;
10779
+ for (; ; ) {
10780
+ const candidate = join38(dir, relativePath);
10781
+ if (existsSync38(candidate)) return candidate;
10782
+ const parent = dirname10(dir);
10783
+ if (parent === dir) return null;
10784
+ dir = parent;
10785
+ }
10786
+ }
10787
+ function packagedScriptMissing(relativePath) {
10788
+ return `biffo: cannot find ${relativePath}.
10789
+ It ships with this package via cli/scripts/packaged-root-assets.mjs; if you are running from a checkout, run from inside the template repo. If you are running the published package, this is a packaging bug \u2014 the asset was not copied at prepack.`;
10790
+ }
10791
+
10792
+ // src/commands/wait-for-checks.ts
10793
+ var SCRIPT = "scripts/wait-for-checks.sh";
10794
+ var waitForChecksCommand = new Command26("wait-for-checks").description(
10795
+ "Wait for a PR\u2019s required checks on a positive signal (0 green, 1 failed, 2 cannot tell)"
10796
+ ).argument("<pr>", "Pull request number").allowExcessArguments(true).allowUnknownOption(true).action(() => {
10797
+ const here = dirname11(fileURLToPath6(import.meta.url));
10798
+ const script = findPackagedScript(here, SCRIPT);
10799
+ if (!script) {
10800
+ process.stderr.write(`${packagedScriptMissing(SCRIPT)}
10801
+ `);
10802
+ process.exit(2);
10803
+ }
10804
+ const at = process.argv.indexOf("wait-for-checks");
10805
+ const args = at === -1 ? [] : process.argv.slice(at + 1);
10806
+ const result = spawnSync("sh", [script, ...args], { stdio: "inherit" });
10807
+ process.exit(result.status === null ? 2 : result.status);
10808
+ });
10809
+
10768
10810
  // src/index.ts
10769
- var program = new Command26();
10811
+ var program = new Command27();
10770
10812
  function cliVersion() {
10771
10813
  try {
10772
10814
  return getLatestCoreVersion();
@@ -10779,6 +10821,7 @@ program.addCommand(initCommand);
10779
10821
  program.addCommand(deployCommand);
10780
10822
  program.addCommand(destroyCommand);
10781
10823
  program.addCommand(teardownCommand);
10824
+ program.addCommand(waitForChecksCommand);
10782
10825
  program.addCommand(pluginCommand);
10783
10826
  program.addCommand(dataCommand);
10784
10827
  program.addCommand(coreCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.234.2",
3
+ "version": "0.235.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,8 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "schemas",
26
- "_skeletons"
26
+ "_skeletons",
27
+ "scripts/wait-for-checks.sh"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "tsup src/index.ts --format esm --dts --clean",
@@ -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