Real bug behind 0.27.0-fork.5 and fork.7 shipping stale linux binaries.
Docker compose interpolates \${VAR} (and \$VAR) at YAML parse time
against the host shell — including inside `command:` blocks. So:
PID1=\$! ← compose sees \$! → host has no `!` var → ""
wait \$PID1 ... ← compose sees \$PID1 → "" → becomes `wait `
SRC="...\$TARGET..." ← \$TARGET still works (set in `environment:`)
cp "\$SRC" "..." ← \$SRC eaten → empty → cp errors silently
Result: the per-PID error check I added in dbf272c never fired
because both lines were `wait` (no args) — which waits for ALL
children and exits with the LAST one's status, not each individually.
A failing arm64 build couldn't fail the script.
Fix: escape every script-local \$ as \$\$. Docker compose translates
\$\$ → literal \$ when materializing the command for the container,
and the in-container shell then expands \$VAR correctly.
Verified by `docker compose config` showing the resolved command
contains \$\$PID1 / \$\$SRC etc (which becomes \$PID1 / \$SRC in the
container's bash).
Two latent bugs in the release pipeline that conspired to ship a stale
linux-x64 binary in 0.27.0-fork.5 (only caught by manually grepping
the embedded version string):
1. build-linux ran x64 and arm64 in parallel and used a single
`wait $PID1 $PID2` to join them. That command waits for both, but
its exit code is the LAST waited pid only — so if x64 silently
broke and arm64 succeeded, the outer script exited 0 and shipped
whatever was already in /output from the previous release. Now we
wait on each pid individually and exit 1 on either failure.
2. build-single's cp used `agent-browser*` which globs to BOTH the
binary and its `.d` dependency file. When two sources are passed,
cp requires the destination to be a directory. We weren't, so cp
exited non-zero with "Not a directory" and the build script
shrugged it off because the next line was `chmod ... || true`.
Now we resolve a single explicit source path.