Skip to content

ci: run the spawn-edge acceptance per leg before publish (#211) - #226

Merged
ronaldtse merged 3 commits into
mainfrom
acceptance/spawn-edge-gate
Sep 30, 2026
Merged

ronaldtse merged 3 commits into
mainfrom
acceptance/spawn-edge-gate

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

What

Wires the spawn-edge acceptance gate into _build-platform.yml — the factory half of #211. The argv[0] drop in the driver's spawn plan (the tebako#691 failure class, fixed in tebako v2.8.23) reached a published runtime because no per-leg check exercised payload-to-payload spawn before publish: the boot smoke proves the runtime boots, but never spawns a second payload through the driver's spawn surface. This gate closes that hole with two tiny payloads and one boot — no metanorma compile.

Companion harness PR: tamatebako/ruby#126 (the fixtures + runners under ci/spawn-edge/).

How CI exercises it

Per ruby line × platform, in _build-platform.yml, after the boot smoke and before the leg-complete marker — a red gate publishes nothing:

  1. Checkout the spawn-edge harness from tamatebako/ruby, riding the pre-existing inputs.harness_ref (default main), same as the windows dogfood.
  2. Three arms:
    • Spawn-edge acceptance (POSIX legs) — run.sh under bash;
    • Spawn-edge acceptance (musl legs, in alpine) — same script inside alpine:3.21, mirroring the musl boot smoke;
    • Spawn-edge acceptance (windows) — run-msys.sh under msys2.
  3. Each arm resolves the leg's pin-verified tfs CLI from .build/downloads/tfs (fail-closed if absent) and runs the harness against the leg's freshly built runtime package (RUNTIME_PKG_DIR=runtime-packages, RUBY_VERSION from the matrix, TEBAKO_VERSION from the chain gate).

The harness presses a provider payload (exposes spawn-edge-echo) and a consumer payload (requires it, array-spawns it via system() and IO.popen), stages a scratch store, and boots the consumer against the fresh runtime. Any argv-handling regression in the spawn plan turns the child's boot into a named error and fails the leg with the child's diagnostics printed.

The macos/linux-gnu/linux-musl trigger workflows gain the harness_ref workflow_dispatch input (default main), mirroring build-windows.yml, so probe rounds can pin a harness branch.

spec/build_workflow_spec.rb locks the ordering structurally: gate arms after every boot-smoke step and the harness checkout, before the leg-complete marker and the package upload, each fed RUBY_VERSION.

Evidence

Harness verified locally against published packages on macos-arm64 (details in tamatebako/ruby#126):

  • v0.16.32 (ruby 4.0.7, 3.3.12) → SPAWN-EDGE-ACCEPTANCE-OK, exit 0;
  • v0.16.29 (pre-fix) → gate fails with exactly the tebako#691 signature (argv[0] drop → the provider mount is lost at child boot), exit 1.

Repo suite: bundle exec rspec — 295 examples, 0 failures (38 pre-existing platform-gated pendings); bundle exec rubocop clean on the touched spec; actionlint on the four workflow files reports only the pre-existing findings also present on main.

Sequencing

tamatebako/ruby#126 should land first: the default harness_ref is main, and until the harness is on main the gate's checkout fails closed by design. The interim end-to-end proof is a probe dispatch of any build workflow with harness_ref: ci/spawn-edge-acceptance; the windows and musl arms are exercised by the PR's own legs under that pin.

The argv[0] drop in the driver's spawn plan (the tebako#691 failure
class, fixed in tebako v2.8.23) reached a published runtime because no
per-leg check exercised payload-to-payload spawn before publish. The
boot smoke only proves the runtime boots; it never spawns a second
payload through the driver's spawn surface.

This wires a minimal spawn-edge acceptance gate into
_build-platform.yml:

- checks out the harness from tamatebako/ruby (ci/spawn-edge), riding
  the pre-existing inputs.harness_ref like the windows dogfood;
- runs it per ruby line x platform, after the boot smoke and before
  the leg-complete marker — a red gate publishes nothing;
- three arms: POSIX legs run run.sh under bash, musl legs run it
  inside alpine:3.21 like the musl boot smoke, windows runs
  run-msys.sh under msys2;
- each arm resolves the leg's pin-verified tfs CLI from
  .build/downloads/tfs, fail-closed if absent.

The harness presses a provider payload (exposes spawn-edge-echo) and a
consumer payload (requires it, array-spawns it via system() and
IO.popen), stages a scratch store, and boots the consumer against the
freshly built runtime package. Any argv-handling regression in the
spawn plan turns the child's boot into a named error and fails the
leg.

The macos/linux-gnu/linux-musl trigger workflows gain the harness_ref
workflow_dispatch input (default main), mirroring build-windows.yml,
so probe rounds can pin a harness branch.

spec/build_workflow_spec.rb locks the ordering structurally: gate arms
after every boot-smoke step and the harness checkout, before the
leg-complete marker and the package upload, each fed RUBY_VERSION.
@ronaldtse
ronaldtse marked this pull request as ready for review September 30, 2026 07:29
The first gate round failed on every POSIX leg with the probe exiting
127: the workflow handed the harness a RELATIVE RUNTIME_PKG_DIR
(runtime-packages), and the harness cd's into its scratch dir for the
proof run — the boot then could not exec the leg's runtime exe
("run.sh: line 146: runtime-packages/tebako-runtime-…: No such file or
directory"). The harness contract is fine (it documents the dir, not
its spelling); the wiring owed it an absolute path.

- POSIX + windows arms: resolve RUNTIME_PKG_DIR to an absolute path
  (cd + pwd; declare/assign split so set -e still catches a missing
  dir) before invoking the harness. The msys pwd yields the /d/…
  spelling run-msys.sh's find/cp need.
- musl arm: pass -e RUNTIME_PKG_DIR=/mnt/w/runtime-packages — the
  in-container absolute spelling of the workspace mount.

spec/build_workflow_spec.rb now asserts each gate arm absolutizes the
package dir, so the relative-spelling regression fails the suite.

Verified locally against the published v0.16.32 macos-arm64 package in
a CI-mimicking layout: the relative spelling reproduces the CI failure
verbatim (line 146, exit 127), and the absolutize line turns the same
run green (SPAWN-EDGE-ACCEPTANCE-OK 4.0.7 (macos-arm64)).
@ronaldtse

Copy link
Copy Markdown
Contributor Author

First gate round failed on every POSIX leg (e.g. job 109784285613, macos arm64 Ruby 4.0.7): the probe exited 127 with run.sh: line 146: runtime-packages/tebako-runtime-0.16.32-4.0.7-macos-arm64: No such file or directory.

Root cause was on the wiring side, not the harness: the arms handed the harness a relative RUNTIME_PKG_DIR (runtime-packages), and the harness cds into its scratch dir for the proof run — the boot then could not exec the leg's runtime exe. Locally the harness had only ever been run with an absolute dir, so the relative spelling was untested.

Fix in 4221c31, harness untouched:

  • POSIX + windows arms resolve RUNTIME_PKG_DIR to an absolute path (cd + pwd, declare/assign split so set -e still catches a missing dir; msys pwd yields the /d/… spelling run-msys.sh needs) before invoking the harness.
  • musl arm passes -e RUNTIME_PKG_DIR=/mnt/w/runtime-packages — the in-container absolute spelling of the workspace mount.
  • spec/build_workflow_spec.rb now asserts each gate arm absolutizes the package dir, so this regression fails the suite.

Local verification against the published v0.16.32 macos-arm64 package in a CI-mimicking layout: the relative spelling reproduces the CI failure verbatim (same line, exit 127); with the absolutize line the same run is green (SPAWN-EDGE-ACCEPTANCE-OK 4.0.7 (macos-arm64)). Full repo suite 295 examples / 0 failures; actionlint clean on the touched steps.

)

The second gate round failed on every linux-gnu and linux-musl leg:

- linux-gnu died SILENTLY 12ms in: the leg builds in a Docker
  container with a container-local prefix, so the pin-verified tfs CLI
  that packs the env image lives at /root/.build/downloads/tfs INSIDE
  the container and the host workspace has no .build/downloads/tfs.
  The gate's find then exited 1 and, under the step's pipefail, the
  assignment died before the ::error:: guard could fire.
- linux-musl hit the named guard (no pin-verified tfs CLI) — same root
  cause, surfaced because the in-alpine sh runs without pipefail.

Fixes:

1. The container build step's trailing copy chain (which already
   copies the smoke headers/tools out to /mnt/w) now also copies
   /root/.build/downloads/tfs/. out to /mnt/w/.build/downloads/tfs/,
   preserving the <sha>/tfs-* layout the gate's find expects.
   Fail-closed: a miss fails the leg right there — the gate has
   nothing to press with otherwise.

2. The tfs resolution in all three gate arms carries || true INSIDE
   the command substitution (find … | head -1 || true), so a missing
   cache dir reaches the named ::error:: guard instead of dying
   silently at the assignment under pipefail.

spec/build_workflow_spec.rb now asserts both: every gate arm's find is
|| true-guarded, and the container build step copies the tfs cache
host-visibly.

Local verification: shell-level repro under bash -e -o pipefail — the
old spelling exits 1 with zero output (the CI signature), the new
spelling reaches the named ::error:: and still picks the first match
when the dir exists; yaml parse, full rspec suite (295/0), rubocop and
actionlint clean (only the 4 pre-existing findings).
@ronaldtse

Copy link
Copy Markdown
Contributor Author

Second gate round: macOS and windows legs now pass the gate; linux-gnu died silently 12ms in and linux-musl hit the named guard (no pin-verified tfs CLI under .build/downloads/tfs) — same root cause on both.

Root cause: container legs build with a container-local prefix (--prefix /root/.build), so the pin-verified tfs CLI that packs the env image lives at /root/.build/downloads/tfs/<sha>/tfs-… inside the container and the host workspace has no .build/downloads/tfs. On linux-gnu (bash + pipefail) the gate's find exited 1 and the assignment died before the ::error:: guard — hence the silent 12ms exit. On linux-musl the in-alpine sh has no pipefail, so the same miss surfaced as the named error.

Fix in b8630bf (same branch, no new PR):

  1. Host-visible tfs CLI: the container build step's trailing copy chain (the one that already copies smoke headers/tools to /mnt/w) now also copies /root/.build/downloads/tfs/. → /mnt/w/.build/downloads/tfs/, preserving the <sha>/tfs-* layout the gate's find -mindepth 2 -maxdepth 2 expects. Fail-closed: a miss fails the leg right there.
  2. Hardened resolution: all three gate arms now carry || true inside the command substitution (find … | head -1 || true), so a missing cache dir reaches the named ::error:: guard instead of dying silently at the assignment under pipefail.
  3. spec/build_workflow_spec.rb locks both: every gate arm's find must be || true-guarded, and the container build step must copy the tfs cache host-visibly.

Local verification: shell-level repro under bash -e -o pipefail — old spelling exits 1 with zero output (the CI signature), new spelling reaches the named ::error::, and still resolves the first match when the dir exists; yaml parse clean; full suite 295 examples / 0 failures; rubocop clean; actionlint shows only the 4 pre-existing findings.

Also confirmed from this round's logs: the windows arm passed end-to-end (its tfs-*.exe resolution and run-msys.sh path were correct all along — windows builds host-side, so the cache was never container-local there).

@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:33 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:33 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:33 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:33 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:34 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:37 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:37 — with GitHub Actions Active
@ronaldtse
ronaldtse deployed to windows-signing September 30, 2026 09:37 — with GitHub Actions Active
@ronaldtse
ronaldtse merged commit 2a8d501 into main Sep 30, 2026
56 checks passed
@ronaldtse
ronaldtse deleted the acceptance/spawn-edge-gate branch September 30, 2026 10:11

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants