4d - every gate leg names itself, its image and its seconds #106

Merged
lilleman merged 4 commits from gate-legs into main 2026-09-20 00:12:56 +02:00
6 changed files with 102 additions and 46 deletions
+8
View File
@@ -238,6 +238,14 @@ 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, where a container is in play, the 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. A leg added later owes the same marker, and a function a leg reaches 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
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
+19 -17
View File
@@ -3,28 +3,30 @@ set -euo pipefail
cd "$(dirname "$0")"
source ./docker-runner.sh
in_image "$node_image" npm ci
in_image "$node_image" npm run typecheck
leg "install ($node_image)" in_image "$node_image" npm ci
leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
if ! test_output=$(in_image "$node_image" npm test 2>&1); then
printf '%s\n' "$test_output"
exit 1
fi
printf '%s\n' "$test_output"
if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then
test_log=$(mktemp)
node_tests() {
in_image "$node_image" npm test 2>&1 | tee "$test_log"
}
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'
exit 1
fi
rm -f "$test_log"
in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
in_image "$bun_image" bun test src/
leg "tests ($deno_image)" in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
leg "tests ($bun_image)" in_image "$bun_image" bun test src/
in_image "$node_image" npm run build
in_image "$node_image" sh -c 'set -e
leg "build ($node_image)" in_image "$node_image" npm run build
leg "pack and install the tarball ($node_image)" in_image "$node_image" sh -c 'set -e
rm -rf package-tests/node_modules
npm pack --pack-destination /tmp >/dev/null
npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz >/dev/null'
in_image "$node_image" npx tsc -p package-tests
in_image "$floor_image" node package-tests/node-floor.js
npm pack --pack-destination /tmp
npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz'
leg "typecheck the consumer ($node_image)" in_image "$node_image" npx tsc -p package-tests
leg "round-trip on the engines floor ($floor_image)" in_image "$floor_image" node package-tests/node-floor.js
with_firefox in_image "$node_image" node browser-tests/run.js
leg "browser ($firefox_image)" with_firefox in_image "$node_image" node browser-tests/run.js
+16 -1
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,9 +12,22 @@ 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 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
printf '\n\033[1;34m==> %s\033[0m\n' "$leg_name" >&2
# EPOCHREALTIME carries the locale's radix character, so keep the digits and read microseconds.
leg_started=${EPOCHREALTIME//[^0-9]/}
"$@" || leg_status=$?
leg_elapsed=$((${EPOCHREALTIME//[^0-9]/} - leg_started))
printf '\033[1;34m<== %s: %d.%ds\033[0m\n' "$leg_name" "$((leg_elapsed / 1000000))" "$((leg_elapsed % 1000000 / 100000))" >&2
return $leg_status
}
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
+21 -13
View File
@@ -3,35 +3,43 @@ set -euo pipefail
cd "$(dirname "$0")"
source ./docker-runner.sh
read_field() {
in_image "$node_image" npm pkg get "$1" | tr -d '"\r'
}
published_version() {
in_image "$node_image" npm view "$1@$2" version 2>/dev/null || true
}
private=$(read_field private)
push_tag() {
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
if [ "$private" = 'true' ]; then
echo 'package.json is private — the maintainer removes that in the bump that first publishes'
exit 0
fi
name=$(read_field name)
version=$(read_field version)
published=$(published_version "$name" "$version")
tagged=$(git ls-remote --tags origin "v$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.
if [ -z "$published" ]; then
: "${NPM_TOKEN:?the publish needs NPM_TOKEN}"
in_image "$node_image" npm ci
in_image "$node_image" npm run build
leg "install ($node_image)" in_image "$node_image" npm ci
leg "build ($node_image)" in_image "$node_image" npm run build
leg "publish $name@$version ($node_image)" \
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e NPM_TOKEN -v "$PWD:/app" -w /app --entrypoint sh "$node_image" -c \
'printf "//registry.npmjs.org/:_authToken=%s\n" "$NPM_TOKEN" > "$HOME/.npmrc" && npm publish --access public'
fi
if [ -z "$tagged" ]; then
git tag "v$version"
git push origin "v$version"
leg "tag v$version" push_tag
fi
+36
View File
@@ -546,6 +546,42 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
lines an indented code block held (200k of them at 200 kB), and `emitRun` joining a mark
run's segments (200k nodes under one mark). Both fixed here with the same loop and a test
each, and §11 gained the rule so the spelling cannot walk back in.
- [x] **4d — What the gate says while it runs (`0.2.0`).** `ci.sh` runs nine legs and announces
none of them, so five minutes of a Gitea run read as silence and a hang cannot be told from
a slow pull — the maintainer hit exactly this on the `0.1.0` release. Three causes, each its
own fix. The legs need markers: `plainpages`' `ci.sh` prints a `step()` header per leg and
this one prints nothing, so name the leg and the image before each. The longest leg is the
quietest: `test_output=$(… npm test 2>&1)` buffers the whole Node run to replay it after,
because the zero-test guard greps the count — stream it and grep a copy (`tee`), rather than
trading the output for the guard. And two legs are silenced outright, `npm pack` and the
tarball install, whose `>/dev/null` predates the offline install that made them quick and
quiet. `publish.sh` owes the same: today it says nothing between reading `private` and the
registry answering, which is where its `npm ci` and rebuild sit — the seconds §9 accepts
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-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
remote, the README title, §6's published-as line, the checkout directory.
+1 -14
View File
@@ -69,20 +69,7 @@ bundle size and the tagline.
- [x] **4.4 — The real payloads.**
- [x] **4b — The block walk's retry (`0.2.0`).**
- [x] **4c — The scanning rule's remaining sites (`0.2.0`).**
- [ ] **4d — What the gate says while it runs (`0.2.0`).** `ci.sh` runs nine legs and announces
none of them, so five minutes of a Gitea run read as silence and a hang cannot be told from
a slow pull — the maintainer hit exactly this on the `0.1.0` release. Three causes, each its
own fix. The legs need markers: `plainpages`' `ci.sh` prints a `step()` header per leg and
this one prints nothing, so name the leg and the image before each. The longest leg is the
quietest: `test_output=$(… npm test 2>&1)` buffers the whole Node run to replay it after,
because the zero-test guard greps the count — stream it and grep a copy (`tee`), rather than
trading the output for the guard. And two legs are silenced outright, `npm pack` and the
tarball install, whose `>/dev/null` predates the offline install that made them quick and
quiet. `publish.sh` owes the same: today it says nothing between reading `private` and the
registry answering, which is where its `npm ci` and rebuild sit — the seconds §9 accepts
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.
- [x] **4d — What the gate says while it runs (`0.2.0`).**
- [x] **5 — Ship `0.1.0`.**
- [ ] **5e — The publish token's deadline.** `0.1.0` published only once the npm
token carried **Bypass 2FA**: the account requiring no 2FA on writes was not enough, and npm