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
5 changed files with 47 additions and 40 deletions
Showing only changes of commit 1fb712c76d - Show all commits
+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 document, its error code — leaving the corpus's pairing, uniqueness, source positions and
byte-level equality to the Node suite that owns them. 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` Every leg announces its name, and the image where it runs in one, before it runs and its elapsed
alongside `ci.sh`, so a long run reads as progress rather than as a hang — which is what a silent time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as a hang.
one cost the `0.1.0` release. A leg added later owes the same marker, and a leg that buffers its A leg added later owes the same marker, and a function a leg runs chains its statements with `&&`,
output to read something out of it streams and keeps the copy in a file: `tee /dev/stderr` reopens because the `||` that captures the leg's status suspends `set -e` for everything it calls. A leg
the stream, so under the `> log 2>&1` a reader runs locally the two offsets advance independently whose output is both streamed and grepped keeps the copy in a `mktemp` file: `tee /dev/stderr`
and punch NUL holes through each other's lines (4d). 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 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 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")" cd "$(dirname "$0")"
source ./docker-runner.sh 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() { node_tests() {
in_image "$node_image" npm test 2>&1 | tee "$test_log" 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 leg "tests ($node_image)" node_tests
if grep -q 'ℹ tests 0' "$test_log"; then if grep -q 'ℹ tests 0' "$test_log"; then
echo 'the gate ran zero tests — failing instead of a vacuous green' 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 bun_image=oven/bun:1.4.0-alpine
deno_image=denoland/deno:2.9.6 deno_image=denoland/deno:2.9.6
firefox_image=selenium/standalone-firefox:153.0.4 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" "$@" 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 # Markers on stderr so a captured leg's value stays clean; leg_* because bash scopes local into the leg's own call.
# function's own name because bash scopes them dynamically into whatever the leg runs.
leg() { leg() {
local leg_name=$1 leg_elapsed leg_started leg_status=0 local leg_name=$1 leg_elapsed leg_started leg_status=0
shift shift
@@ -26,7 +27,7 @@ leg() {
with_firefox() { with_firefox() {
local container in_image_network status=0 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. # 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 "docker rm -f $container >/dev/null 2>&1" EXIT
trap 'exit 130' INT trap 'exit 130' INT
+12 -13
View File
@@ -3,23 +3,22 @@ set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
source ./docker-runner.sh 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() { published_version() {
in_image "$node_image" npm view "$1@$2" version 2>/dev/null || true in_image "$node_image" npm view "$1@$2" version 2>/dev/null || true
} }
push_tag() { push_tag() {
git tag "v$version" git tag "v$version" && git push origin "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 leg "read package.json ($node_image)" read_package_fields
@@ -28,7 +27,7 @@ if [ "$private" = 'true' ]; then
exit 0 exit 0
fi 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") 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. # 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 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 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. 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 **Measured** (2026-09-20): ten legs, not the nine counted above, each naming the image it runs
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, in where it runs in one, on a 28.4 s warm gate — install 1.5 s, typecheck 1.2 s, Node tests
build 1.0 s, pack and install 1.6 s, consumer typecheck 1.0 s, engines floor 0.5 s, browser 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,
5.7 s. The browser leg lands in the 5.4–7.9 s the item quotes, and the markers cost nothing engines floor 0.4 s, browser 5.6 s. The browser leg lands in the 5.4–7.9 s the item quotes,
measurable: 28.9 s before against 27.9 s after. `publish.sh` reads its fields in 2.5 s and the and the markers cost nothing measurable: 28.9 s before against 28.4 s after. `publish.sh`
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 reads its fields in 2.6 s and the registry in 1.4 s; its `npm ci` and rebuild are the gate's
§9 accepts for rebuilding rather than promoting the gate's `dist` are about 2.5. own 1.5 s and 1.0 s, so the seconds §9 accepts for rebuilding rather than promoting the gate's
Three things the writing turned up. The markers print to stderr, so a leg whose value is read — `dist` are about 2.5.
`publish.sh` asking npmjs — stays capturable. `leg`'s locals carry its own name because bash Four things the writing turned up, three of them bash scoping a rule differently than it
scopes them into whatever the leg runs: unprefixed, `name` was swallowed by the leg reading reads. The markers print to stderr, so a leg whose value is read — `publish.sh` asking npmjs —
`package.json`. And `leg` returns its command's status the way `with_firefox` already did, stays capturable. `leg`'s locals carry its own name because bash scopes them into whatever the
because the bare call swallowed a non-zero one wherever `set -e` is suspended, which also gets leg runs: unprefixed, `name` was swallowed by the leg reading `package.json`. `leg` returns
the elapsed time printed for the leg that failed. 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 - [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 the published identity: `package.json` `name` and `repository`, the Gitea repo and its