@biffo/cli 0.234.2 → 0.236.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.
- package/_skeletons/plugin-template/AGENTS.md +7 -2
- package/_skeletons/plugin-template/scripts/biffo.sh +107 -0
- package/_skeletons/sibling-template/AGENTS.md +7 -2
- package/_skeletons/sibling-template/scripts/biffo.sh +107 -0
- package/dist/index.js +52 -4
- package/package.json +3 -2
- package/_skeletons/sibling-template/scripts/wait-for-checks.sh +0 -238
- /package/{_skeletons/plugin-template/scripts → scripts}/wait-for-checks.sh +0 -0
|
@@ -96,12 +96,17 @@ it deliberately and say so in a comment; never steal a fresh one.
|
|
|
96
96
|
|
|
97
97
|
- Get CI green and confirm it: `gh pr checks <N>`. A green local run is not
|
|
98
98
|
sufficient — verify the actual PR checks.
|
|
99
|
-
- **Wait with `
|
|
99
|
+
- **Wait with the CLI's `wait-for-checks`, not a hand-rolled loop:**
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
sh scripts/wait-for-checks
|
|
102
|
+
sh scripts/biffo.sh wait-for-checks <N> # 0 green · 1 failed · 2 cannot tell
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
This repo no longer carries its own copy of the script. `scripts/biffo.sh`
|
|
106
|
+
resolves the version-pinned Biffo CLI from `.biffo-shared-version` and runs
|
|
107
|
+
the canonical copy that ships inside the package, so there is one script
|
|
108
|
+
rather than one per repo (#1109).
|
|
109
|
+
|
|
105
110
|
Do not write your own `until … grep -c pending … done`. That polls for the
|
|
106
111
|
**absence** of pending checks, so the empty window right after
|
|
107
112
|
`gh pr update-branch` — superseded runs dropped, new ones not yet registered —
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
# Nothing above matched, so this must be the template — the only repo that
|
|
81
|
+
# carries `cli/`. A satellite that reaches here has no pin, which since #1109
|
|
82
|
+
# means it cannot run guards at all; before this branch it fell through and
|
|
83
|
+
# exec'd a `tsx` that does not exist, exiting 127 with no explanation.
|
|
84
|
+
#
|
|
85
|
+
# `biffo sibling create` now stamps the pin at birth, so this is reachable
|
|
86
|
+
# mainly by a repo scaffolded before that, or by a plugin repo (`biffo plugin
|
|
87
|
+
# create` scaffolds INTO an existing repo, so it has no standalone repo root to
|
|
88
|
+
# stamp). `shared-sync.sh` writes the pin on its first run, which is the fix.
|
|
89
|
+
if [ ! -d cli ]; then
|
|
90
|
+
echo "biffo.sh: no biffo.core.json, no .biffo-shared-version, and no cli/ here." >&2
|
|
91
|
+
echo " If this is a satellite, run shared-sync from the template to stamp it:" >&2
|
|
92
|
+
echo " sh scripts/shared-sync.sh --estate <path-to-your-repos>" >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# NOT `pnpm --filter @biffo/cli exec tsx ...`: `pnpm exec` normalises every
|
|
97
|
+
# non-zero exit to 1. Verified — the CLI exits 2 and pnpm reports 1.
|
|
98
|
+
#
|
|
99
|
+
# That silently flattened the exit code of every guard this bridge runs in the
|
|
100
|
+
# template, and it matters now because the estate's conventions are built on a
|
|
101
|
+
# THREE-valued contract: 0 green, 1 failed, **2 cannot tell**, where 2 is never
|
|
102
|
+
# a pass (`wait-for-checks.sh`, `branch-health.sh`, `claim.sh`). Collapsing 2
|
|
103
|
+
# into 1 turns "I could not tell" into "it failed", which is the safe direction
|
|
104
|
+
# by luck rather than design — and the reverse collapse would be a fail-open.
|
|
105
|
+
#
|
|
106
|
+
# `exec` on the binary directly keeps the child's status as this script's own.
|
|
107
|
+
exec "$root/cli/node_modules/.bin/tsx" "$root/cli/src/index.ts" "$@"
|
|
@@ -96,12 +96,17 @@ it deliberately and say so in a comment; never steal a fresh one.
|
|
|
96
96
|
|
|
97
97
|
- Get CI green and confirm it: `gh pr checks <N>`. A green local run is not
|
|
98
98
|
sufficient — verify the actual PR checks.
|
|
99
|
-
- **Wait with `
|
|
99
|
+
- **Wait with the CLI's `wait-for-checks`, not a hand-rolled loop:**
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
sh scripts/wait-for-checks
|
|
102
|
+
sh scripts/biffo.sh wait-for-checks <N> # 0 green · 1 failed · 2 cannot tell
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
This repo no longer carries its own copy of the script. `scripts/biffo.sh`
|
|
106
|
+
resolves the version-pinned Biffo CLI from `.biffo-shared-version` and runs
|
|
107
|
+
the canonical copy that ships inside the package, so there is one script
|
|
108
|
+
rather than one per repo (#1109).
|
|
109
|
+
|
|
105
110
|
Do not write your own `until … grep -c pending … done`. That polls for the
|
|
106
111
|
**absence** of pending checks, so the empty window right after
|
|
107
112
|
`gh pr update-branch` — superseded runs dropped, new ones not yet registered —
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
# Nothing above matched, so this must be the template — the only repo that
|
|
81
|
+
# carries `cli/`. A satellite that reaches here has no pin, which since #1109
|
|
82
|
+
# means it cannot run guards at all; before this branch it fell through and
|
|
83
|
+
# exec'd a `tsx` that does not exist, exiting 127 with no explanation.
|
|
84
|
+
#
|
|
85
|
+
# `biffo sibling create` now stamps the pin at birth, so this is reachable
|
|
86
|
+
# mainly by a repo scaffolded before that, or by a plugin repo (`biffo plugin
|
|
87
|
+
# create` scaffolds INTO an existing repo, so it has no standalone repo root to
|
|
88
|
+
# stamp). `shared-sync.sh` writes the pin on its first run, which is the fix.
|
|
89
|
+
if [ ! -d cli ]; then
|
|
90
|
+
echo "biffo.sh: no biffo.core.json, no .biffo-shared-version, and no cli/ here." >&2
|
|
91
|
+
echo " If this is a satellite, run shared-sync from the template to stamp it:" >&2
|
|
92
|
+
echo " sh scripts/shared-sync.sh --estate <path-to-your-repos>" >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# NOT `pnpm --filter @biffo/cli exec tsx ...`: `pnpm exec` normalises every
|
|
97
|
+
# non-zero exit to 1. Verified — the CLI exits 2 and pnpm reports 1.
|
|
98
|
+
#
|
|
99
|
+
# That silently flattened the exit code of every guard this bridge runs in the
|
|
100
|
+
# template, and it matters now because the estate's conventions are built on a
|
|
101
|
+
# THREE-valued contract: 0 green, 1 failed, **2 cannot tell**, where 2 is never
|
|
102
|
+
# a pass (`wait-for-checks.sh`, `branch-health.sh`, `claim.sh`). Collapsing 2
|
|
103
|
+
# into 1 turns "I could not tell" into "it failed", which is the safe direction
|
|
104
|
+
# by luck rather than design — and the reverse collapse would be a fail-open.
|
|
105
|
+
#
|
|
106
|
+
# `exec` on the binary directly keeps the child's status as this script's own.
|
|
107
|
+
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
|
|
4
|
+
import { Command as Command27 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/core.ts
|
|
7
7
|
import { Command as Command4 } from "commander";
|
|
@@ -6589,6 +6589,11 @@ function writeSiblingTemplate(templateRoot, targetDir, config, context) {
|
|
|
6589
6589
|
2
|
|
6590
6590
|
) + "\n"
|
|
6591
6591
|
);
|
|
6592
|
+
writeFileSync7(
|
|
6593
|
+
join21(targetDir, ".biffo-shared-version"),
|
|
6594
|
+
`core-v${context.templateVersion.replace(/^core-v/, "")}
|
|
6595
|
+
`
|
|
6596
|
+
);
|
|
6592
6597
|
const envPath = join21(targetDir, "apps", "frontend", ".env.example");
|
|
6593
6598
|
try {
|
|
6594
6599
|
const path = basePathFor(context.pathPrefix);
|
|
@@ -9684,8 +9689,8 @@ async function runOwnershipCheck(argv) {
|
|
|
9684
9689
|
const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
9685
9690
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
9686
9691
|
if (messageFile) {
|
|
9687
|
-
const { readFileSync: readFileSync28, existsSync:
|
|
9688
|
-
if (
|
|
9692
|
+
const { readFileSync: readFileSync28, existsSync: existsSync39 } = await import("fs");
|
|
9693
|
+
if (existsSync39(messageFile)) commitMessage = readFileSync28(messageFile, "utf8");
|
|
9689
9694
|
}
|
|
9690
9695
|
} else {
|
|
9691
9696
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -10765,8 +10770,50 @@ function resolveGithubToken4() {
|
|
|
10765
10770
|
);
|
|
10766
10771
|
}
|
|
10767
10772
|
|
|
10773
|
+
// src/commands/wait-for-checks.ts
|
|
10774
|
+
import { spawnSync } from "child_process";
|
|
10775
|
+
import { dirname as dirname11 } from "path";
|
|
10776
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
10777
|
+
import { Command as Command26 } from "commander";
|
|
10778
|
+
|
|
10779
|
+
// src/lib/packaged-scripts.ts
|
|
10780
|
+
import { existsSync as existsSync38 } from "fs";
|
|
10781
|
+
import { dirname as dirname10, join as join38 } from "path";
|
|
10782
|
+
function findPackagedScript(startDir, relativePath) {
|
|
10783
|
+
let dir = startDir;
|
|
10784
|
+
for (; ; ) {
|
|
10785
|
+
const candidate = join38(dir, relativePath);
|
|
10786
|
+
if (existsSync38(candidate)) return candidate;
|
|
10787
|
+
const parent = dirname10(dir);
|
|
10788
|
+
if (parent === dir) return null;
|
|
10789
|
+
dir = parent;
|
|
10790
|
+
}
|
|
10791
|
+
}
|
|
10792
|
+
function packagedScriptMissing(relativePath) {
|
|
10793
|
+
return `biffo: cannot find ${relativePath}.
|
|
10794
|
+
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.`;
|
|
10795
|
+
}
|
|
10796
|
+
|
|
10797
|
+
// src/commands/wait-for-checks.ts
|
|
10798
|
+
var SCRIPT = "scripts/wait-for-checks.sh";
|
|
10799
|
+
var waitForChecksCommand = new Command26("wait-for-checks").description(
|
|
10800
|
+
"Wait for a PR\u2019s required checks on a positive signal (0 green, 1 failed, 2 cannot tell)"
|
|
10801
|
+
).argument("<pr>", "Pull request number").allowExcessArguments(true).allowUnknownOption(true).action(() => {
|
|
10802
|
+
const here = dirname11(fileURLToPath6(import.meta.url));
|
|
10803
|
+
const script = findPackagedScript(here, SCRIPT);
|
|
10804
|
+
if (!script) {
|
|
10805
|
+
process.stderr.write(`${packagedScriptMissing(SCRIPT)}
|
|
10806
|
+
`);
|
|
10807
|
+
process.exit(2);
|
|
10808
|
+
}
|
|
10809
|
+
const at = process.argv.indexOf("wait-for-checks");
|
|
10810
|
+
const args = at === -1 ? [] : process.argv.slice(at + 1);
|
|
10811
|
+
const result = spawnSync("sh", [script, ...args], { stdio: "inherit" });
|
|
10812
|
+
process.exit(result.status === null ? 2 : result.status);
|
|
10813
|
+
});
|
|
10814
|
+
|
|
10768
10815
|
// src/index.ts
|
|
10769
|
-
var program = new
|
|
10816
|
+
var program = new Command27();
|
|
10770
10817
|
function cliVersion() {
|
|
10771
10818
|
try {
|
|
10772
10819
|
return getLatestCoreVersion();
|
|
@@ -10779,6 +10826,7 @@ program.addCommand(initCommand);
|
|
|
10779
10826
|
program.addCommand(deployCommand);
|
|
10780
10827
|
program.addCommand(destroyCommand);
|
|
10781
10828
|
program.addCommand(teardownCommand);
|
|
10829
|
+
program.addCommand(waitForChecksCommand);
|
|
10782
10830
|
program.addCommand(pluginCommand);
|
|
10783
10831
|
program.addCommand(dataCommand);
|
|
10784
10832
|
program.addCommand(coreCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biffo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.236.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",
|
|
@@ -1,238 +0,0 @@
|
|
|
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
|
|
File without changes
|