fix(relay-deploy): arm64 published binary cannot load, and the deploy runs twice - #1813
Merged
Merged
Conversation
…top deploying twice
Reported symptom: the published-binary deploy built its image, started the
container, failed the health check, and then ran the entire published-binary
attempt a second time before reaching the source build.
## arm64 relay could never load in the runtime image
The release matrix builds x86_64 on ubuntu-22.04 but arm64 on ubuntu-24.04-arm.
Verified against the actual published 0.2.14 artifacts:
x86_64 relay requires up to GLIBC_2.35
aarch64 relay requires GLIBC_2.38
debian:bookworm-slim ships 2.36
So x86_64 worked and arm64 could not start at all, which is why this went
unnoticed. Reproduced on an aarch64 daemon with the real binary:
/app/bitfun-relay-server: /lib/aarch64-linux-gnu/libc.so.6:
version `GLIBC_2.38' not found (required by /app/bitfun-relay-server)
Base image is now debian:trixie-slim (glibc 2.41), verified to load and serve
/health on the first probe with the real arm64 binary.
The image build now greps `ldd` output and fails on "not found", so a future
runner bump surfaces in seconds instead of as an opaque health-check timeout.
Note `ldd` exits 0 even while reporting an unsatisfied symbol version, so its
output is the gate rather than its status; the relay binary itself cannot serve
as the probe because it has no --version flag and simply starts serving.
## The failure was invisible
`docker logs --tail 40 bitfun-relay 2>/dev/null` discarded the container's
stderr — which is where the relay's tracing output, and the loader error above,
both go. That is why the log showed a failed health check with no explanation.
Keep both streams and also report container running/exit/OOM state, so a crash
is distinguishable from a container that is up but not answering.
## The deploy ran twice
`bitfun_run_deploy_sh` is reached only after `bitfun_try_release_deploy` failed,
but invoked `deploy.sh` with no arguments — and `deploy.sh`'s own first step is
that same release-binary path. It re-downloaded, rebuilt and restarted the
binary that had just failed. Pass `--build-from-source`, the flag that already
existed for exactly this.
Tests: 31 pass. Adds coverage for the runtime base and its ldd gate, the
preserved diagnostics, and one --build-from-source per docker mode.
Completes the arm64 fix from the other side. The release matrix built x86_64 on ubuntu-22.04 (glibc 2.35) but arm64 on ubuntu-24.04-arm (2.39), so the arm64 relay and CLI required GLIBC_2.38 — unable to run on Debian 12 / Ubuntu 22.04 arm64 at all. Changing the relay's runtime image fixed the Docker deploy, but not anyone following the README's "Manual Run (without Docker)" path, who never touches Docker. Pin arm64 to ubuntu-22.04-arm so both arches share a 2.35 floor, and assert it with scripts/ci/check-glibc-floor.sh over every published binary (bitfun-relay-server, relay-admin, bitfun, bitfun-cli). Nothing previously declared the supported floor, which is why a runner image difference silently became a user-visible breakage. The checker reads `.gnu.version_r` via `readelf -V` — the authoritative list of versioned symbols a binary needs. Verified against the real published 0.2.14 archives: x86_64 passes at 2.35, arm64 fails at 2.35 reporting 2.38, arm64 passes at 2.38, and a mixed run reports each binary before failing. The relay runtime base deliberately stays on trixie-slim. Desktop pins BITFUN_RELEASE_TAG to its own version, so a v0.2.14 client installs the v0.2.14 archive forever and published archives keep the floor they were built with — the base must cover the highest floor still in the wild (2.38), not the floor CI produces from now on. Both files now say so, so neither gets "optimized" back.
…R byte `stage_scripts_command` used `sed 's/<CR>$//'` with a literal CR in the command string. That CR then travelled through `to_unix_script` on its way to the host — the CR remover passing through the CR remover — and any text-mode hop that rewrote line endings would silently degrade the strip to a no-op. Found while testing on a real arm64 host: a Python text-mode read in the test harness turned the CR into LF, and the command reached the host as an unterminated `sed` expression. `tr -d '\r'` expands the escape itself, so the command is pure ASCII and cannot be damaged in transit. Removing every CR rather than only trailing ones is safe for generated bash, which never contains an intentional CR (`embedded_scripts_are_lf_only` enforces that). The rewrite now also removes its scratch file when it fails, instead of leaving a partial `.lf` behind. Verified end to end on Ubuntu 24.04 arm64: scripts uploaded with all 3879 lines CRLF-corrupted came out with 0 CRs, mode 700, no scratch file, and both parsed.
Review of the new checker found two ways it could report success without actually checking anything. `grep` exits non-zero when a binary has no versioned glibc symbols at all, and under `set -euo pipefail` that aborted the whole run at the command substitution — before the branch that handles the empty case. Verified: the pre-fix script exits 1 with no output on a static binary; it now reports "no versioned glibc requirement" and continues. Worse, without `readelf` every binary looked requirement-free, so the check passed everything. Verified in debian:trixie-slim, which ships no binutils: the script now refuses with exit 2 instead of green-lighting the release.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1801, which fixed the CRLF and Docker-config half. With those in place a real arm64 host got further and exposed two more defects: the published binary still could not start, and the deploy visibly ran twice.
Verified end to end on Ubuntu 24.04 arm64 hardware, not just reasoned about.
1. The arm64 published binary could never load
The release matrix builds x86_64 on
ubuntu-22.04but arm64 onubuntu-24.04-arm. Measured against the real published 0.2.14 artifacts:debian:bookworm-slimprovidesReproduced on the reporter's own machine, against the image their failed run had left behind:
The container exited instantly, so the deploy surfaced only as a failed health check followed by a ~20 minute source rebuild. x86_64 was unaffected, which is why it went unnoticed.
Fixed on both sides, because they have different blast radii:
debian:trixie-slim(glibc 2.41). This is the only fix that helps clients already in the wild.ubuntu-22.04-arm, giving both arches one 2.35 floor. This is the only fix that helps anyone following the README's "Manual Run (without Docker)" path, who never touches Docker and is broken today.debian:trixie-slimmust stay even though CI now emits 2.35 binaries: Desktop pinsBITFUN_RELEASE_TAGto its own version, so a v0.2.14 client installs the 2.38 archive forever. The base has to cover the highest floor still in the wild, not the floor CI produces from now on. Both files say so, to stop it being "optimised" back.Two new guards, since nothing previously declared this contract:
scripts/ci/check-glibc-floor.shreads.gnu.version_rviareadelf -Vand fails the build above the declared floor. Verified against the real 0.2.14 archives: x86_64 passes at 2.35, arm64 fails at 2.35 reporting 2.38, arm64 passes at 2.38, and a mixed run reports each binary before failing.lddoutput and fails to build on an unloadable binary. Notelddexits 0 while reporting an unsatisfied symbol version, so its output is the gate, not its status — my first attempt usedRUN lddand would never have fired. The relay binary is also useless as a probe: it has no--versionand just starts serving.2. The failure was invisible
docker logs --tail 40 bitfun-relay 2>/dev/nulldiscarded the container's stderr — where both the relay's tracing output and the loader error above go. That is why the report showed a failed health check with no explanation at all. Both streams are kept now, plus container running/exit/OOM state so a crash is distinguishable from a container that is up but not answering.3. The deploy ran twice
bitfun_run_deploy_shis reached only afterbitfun_try_release_deployhas failed, but it invokeddeploy.shwith no arguments — anddeploy.sh's own first step is that same release-binary path. It re-downloaded, rebuilt and restarted the binary that had just failed. It now passes--build-from-source, the flag that already existed for exactly this.4. The CR strip no longer depends on a raw CR byte
#1801 shipped
sed 's/<CR>$//'with a literal CR in the command string — which then travelled throughto_unix_scripton its way to the host, the CR remover passing through the CR remover. It worked only because the CR happened to be followed by$rather than a newline.tr -d '\r'expands the escape itself, so the command is pure ASCII and cannot be damaged in transit. The rewrite also cleans up its scratch file when it fails.Verified on real arm64 hardware
Generated scripts uploaded deliberately CRLF-corrupted (harsher than the original report: it dies at line 2 on
set -euo pipefail\r, not line 37):Up (healthy)— docker's own HEALTHCHECK passes/health{"status":"healthy","account_features":true,...}relay-adminNo accounts found.--build-from-sourcedeploy.shindirectandsgmodesForcing the fixed Dockerfile back to
bookworm-slimfails the build with the exact loader error, confirming the gate fires.An honest correction
#1801 also quoted arguments for
sg docker -c. I implied that was related to theunknown shorthand flag: 't' in -tfailure. Tested against the realsgbinary, it was not — the old unquoted form handles that particular command fine. It does genuinely break on multi-word arguments:So it is worth having — the new container-state diagnostic in this PR uses exactly such a template and would break in
sgmode without it — but it was not the cause of the reported bug. The root cause there was the root-ownedconfig.json, already fixed in #1801 and confirmed by the reporter's follow-up run.Reviewer note
ubuntu-22.04-armis documented as available for public repositories, but the matrix change has not been executed — this workflow only runs viaworkflow_callfrom a release. If the label were unavailable the job would fail loudly rather than silently, and the fallback is to keepubuntu-24.04-armand build inside adebian:bookwormcontainer instead.