A legged function chains with && : the || that captures the status suspends set -e
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-20 00:07:06 +02:00
parent be25a7a3f5
commit 1fb712c76d
5 changed files with 47 additions and 40 deletions
+7 -6
View File
@@ -238,12 +238,13 @@ The leg re-checks the conversions and nothing else — each fixture's emitted ma
document, its error code — leaving the corpus's pairing, uniqueness, source positions and
byte-level equality to the Node suite that owns them.
Every leg announces its name and its image before it runs and its elapsed time after, `publish.sh`
alongside `ci.sh`, so a long run reads as progress rather than as a hang — which is what a silent
one cost the `0.1.0` release. A leg added later owes the same marker, and a leg that buffers its
output to read something out of it streams and keeps the copy in a file: `tee /dev/stderr` reopens
the stream, so under the `> log 2>&1` a reader runs locally the two offsets advance independently
and punch NUL holes through each other's lines (4d).
Every leg announces its name, and the image where it runs in one, before it runs and its elapsed
time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as a hang.
A leg added later owes the same marker, and a function a leg runs chains its statements with `&&`,
because the `||` that captures the leg's status suspends `set -e` for everything it calls. A leg
whose output is both streamed and grepped keeps the copy in a `mktemp` file: `tee /dev/stderr`
reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL holes through each other's
lines (4d).
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
+3 -5
View File
@@ -3,16 +3,14 @@ set -euo pipefail
cd "$(dirname "$0")"
source ./docker-runner.sh
test_log=$(mktemp)
leg "install ($node_image)" in_image "$node_image" npm ci
leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
# Streamed, and copied so the zero-test guard reads the count without trading the output for it.
test_log=$(mktemp)
node_tests() {
in_image "$node_image" npm test 2>&1 | tee "$test_log"
}
leg "install ($node_image)" in_image "$node_image" npm ci
leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
leg "tests ($node_image)" node_tests
if grep -q 'ℹ tests 0' "$test_log"; then
echo 'the gate ran zero tests — failing instead of a vacuous green'
+4 -3
View File
@@ -1,3 +1,5 @@
: "${EPOCHREALTIME:?the gate times its legs with EPOCHREALTIME — bash 5 or newer}"
bun_image=oven/bun:1.4.0-alpine
deno_image=denoland/deno:2.9.6
firefox_image=selenium/standalone-firefox:153.0.4
@@ -10,8 +12,7 @@ in_image() {
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp ${PROPERTY_RUNS+-e PROPERTY_RUNS} ${in_image_network:+--network "$in_image_network"} -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@"
}
# Markers go to stderr so a leg reporting a value stays capturable, and the locals carry the
# function's own name because bash scopes them dynamically into whatever the leg runs.
# Markers on stderr so a captured leg's value stays clean; leg_* because bash scopes local into the leg's own call.
leg() {
local leg_name=$1 leg_elapsed leg_started leg_status=0
shift
@@ -26,7 +27,7 @@ leg() {
with_firefox() {
local container in_image_network status=0
container=$(docker run -d --rm "$firefox_image")
container=$(docker run -d --rm "$firefox_image") || return $?
# The id is baked in: the trap fires after this function's locals are gone.
trap "docker rm -f $container >/dev/null 2>&1" EXIT
trap 'exit 130' INT
+12 -13
View File
@@ -3,23 +3,22 @@ set -euo pipefail
cd "$(dirname "$0")"
source ./docker-runner.sh
read_field() {
in_image "$node_image" npm pkg get "$1" | tr -d '"\r'
}
read_package_fields() {
private=$(read_field private)
name=$(read_field name)
version=$(read_field version)
}
published_version() {
in_image "$node_image" npm view "$1@$2" version 2>/dev/null || true
}
push_tag() {
git tag "v$version"
git push origin "v$version"
git tag "v$version" && git push origin "v$version"
}
read_field() {
in_image "$node_image" npm pkg get "$1" | tr -d '"\r'
}
read_package_fields() {
private=$(read_field private) &&
name=$(read_field name) &&
version=$(read_field version)
}
leg "read package.json ($node_image)" read_package_fields
@@ -28,7 +27,7 @@ if [ "$private" = 'true' ]; then
exit 0
fi
published=$(leg "ask npmjs for $name@$version" published_version "$name" "$version")
published=$(leg "ask npmjs for $name@$version ($node_image)" published_version "$name" "$version")
tagged=$(leg "ask origin for v$version" git ls-remote --tags origin "v$version")
# Both steps observe their own end state, so a partial run converges on the next push to main.
+21 -13
View File
@@ -560,19 +560,27 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
rather than promoting the gate's `dist`, and unmeasured until the log shows them. Per-leg
timing is what turns "slow or hung" from a guess into a reading; the browser leg's own
5.4–7.9s against a 17s warm gate is the number that made it obviously cheap.
**Measured** (2026-09-19): ten legs, not the nine counted above, each naming its image, on a
27.9 s warm gate — install 1.5 s, typecheck 1.1 s, Node tests 4.6 s, Deno 6.3 s, Bun 3.9 s,
build 1.0 s, pack and install 1.6 s, consumer typecheck 1.0 s, engines floor 0.5 s, browser
5.7 s. The browser leg lands in the 5.4–7.9 s the item quotes, and the markers cost nothing
measurable: 28.9 s before against 27.9 s after. `publish.sh` reads its fields in 2.5 s and the
registry in 1.4 s; its `npm ci` and rebuild are the gate's own 1.5 s and 1.0 s, so the seconds
§9 accepts for rebuilding rather than promoting the gate's `dist` are about 2.5.
Three things the writing turned up. The markers print to stderr, so a leg whose value is read —
`publish.sh` asking npmjs — stays capturable. `leg`'s locals carry its own name because bash
scopes them into whatever the leg runs: unprefixed, `name` was swallowed by the leg reading
`package.json`. And `leg` returns its command's status the way `with_firefox` already did,
because the bare call swallowed a non-zero one wherever `set -e` is suspended, which also gets
the elapsed time printed for the leg that failed.
**Measured** (2026-09-20): ten legs, not the nine counted above, each naming the image it runs
in where it runs in one, on a 28.4 s warm gate — install 1.5 s, typecheck 1.2 s, Node tests
4.9 s, Deno 6.0 s, Bun 4.5 s, build 1.0 s, pack and install 1.6 s, consumer typecheck 1.0 s,
engines floor 0.4 s, browser 5.6 s. The browser leg lands in the 5.4–7.9 s the item quotes,
and the markers cost nothing measurable: 28.9 s before against 28.4 s after. `publish.sh`
reads its fields in 2.6 s and the registry in 1.4 s; its `npm ci` and rebuild are the gate's
own 1.5 s and 1.0 s, so the seconds §9 accepts for rebuilding rather than promoting the gate's
`dist` are about 2.5.
Four things the writing turned up, three of them bash scoping a rule differently than it
reads. The markers print to stderr, so a leg whose value is read — `publish.sh` asking npmjs —
stays capturable. `leg`'s locals carry its own name because bash scopes them into whatever the
leg runs: unprefixed, `name` was swallowed by the leg reading `package.json`. `leg` returns
its command's status the way `with_firefox` already did, because the bare call dropped a
non-zero one wherever `set -e` is suspended, which also gets the elapsed time printed for the
leg that failed. And the `||` that captures that status suspends `set -e` for everything the
leg calls, so a function a leg runs chains its statements with `&&` or every statement but the
last runs unchecked: `read_package_fields` read on past a failed read, and `push_tag` pushed a
tag the tag step had refused to write, both of which aborted before this chunk (the
stability-reviewer, 2026-09-20). §10 carries the rule so the next leg cannot reintroduce it,
and `EPOCHREALTIME` is guarded at `source` so an older bash names itself rather than dying as
an unbound variable on the first leg.
- [x] **5a — Rename to `@larvit/adf-codec` (`0.1.0`).** Before the first publish, the name being
the published identity: `package.json` `name` and `repository`, the Gitea repo and its