ci: share the C library check and build musl protect-ffi in Alpine - #1027
Conversation
|
freshtonic
left a comment
There was a problem hiding this comment.
I approve this change. One script now holds the C library rules, and every build and preflight runs it. This also closes the gap in ffi-preflight.yml, which accepted a static musl binary. The Alpine build follows the pattern that #1018 uses for @cipherstash/auth. On this head commit (e2820c20), all three Linux legs of the FFI preflight passed, and the linux-x64-musl leg was one of them. Thus, the container build, the neon dist placement, the load test inside the container and the new C library step all run correctly.
Before you merge, do these checks:
- The
smokejob of that preflight has not started yet, because it waits for the Windows leg. It contains the new Alpine smoke step. That step is the only new step that has not run yet. Make sure that it passes. Run Tests (Node 22)andRun Tests (Node 24)fail inlanguages/typescript/packages/cli/src/__tests__/release-train.test.ts. That test compares thestashversion pin inskills/stash-cli/SKILL.md. This pull request does not change either file, so I think the failure is not from this change. Rebase onmain, or confirm thatmainhas the same failure.
Small items. They do not block the merge:
- The new checkout step in
ffi-preflight.ymlusesactions/checkout@v6, but the same step inauth-preflight.ymluses a pinned SHA. This agrees with the other actions inffi-preflight.yml, so you can keep it. But a SHA pin is better for a workflow that downloads release artifacts. - In
check-c-library.test.mjs, the comment in the executable-bit test says "this reads the committed mode". ButstatSyncreads the mode of the file on disk, not the mode in git. If a localchmod +xis not committed, the test passes locally, and the CI checkout then gets a file that it cannot run. To read the committed mode, usegit ls-files -s scripts/check-c-library.shand look for100755. The other choice is to change the comment.
The three decisions in the description:
- No changeset: I agree. The packages keep the same files and the same runtime behaviour. Also, no skill mentions musl.cc or the musl toolchain, so no
SKILL.mdneeds a change. integration-protect-ffi.ymland its mise version with no pin: I agree that it is a separate change. Please open an issue for it, because mise 2026.10.0 will probably stop that job the next time the path filter starts it.- The musl leg uses the runner's Rust: this is correct. It keeps all six protect-ffi legs on one Rust version.
The rules for which C library a Linux release binary must link were copied into _build-auth-artifacts.yml, auth-preflight.yml and ffi-preflight.yml, and nothing kept the copies the same. The copy in ffi-preflight.yml had already drifted: it refused glibc for the musl binary but did not require musl, so it accepted a static binary. scripts/check-c-library.sh <platform> <binary> now holds the rules, and each of the three workflows calls it. The preflights check out the built commit's scripts/ before they download the tarballs. scripts/__tests__/check-c-library.test.mjs runs the script with the real readelf on a glibc, a musl and two static ELF files, which the test writes itself. No musl compiler is on the runners, and a checked-in binary is a file no reviewer can read. It also checks that every Linux platform package has a rule, that an unknown platform fails, and that no workflow keeps a copy of the rules. lint-release.yml now runs shellcheck on the script. Refs: CIP-4284 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
…packing Only ffi-preflight.yml checked which C library the Linux protect-ffi binaries link, and nothing runs that dry run automatically. release.yml builds through _build-ffi-artifacts.yml, so a release could publish a musl binary that links glibc. The suite published a glibc-linked @cipherstash/auth-linux-x64-musl 0.44.0 that way. Each Linux leg now runs scripts/check-c-library.sh on its binary after placing it and before packing it, as _build-auth-artifacts.yml does since #1018. scripts/__tests__/ffi-build-artifacts.test.mjs pins the step's place, its Linux condition and the binary it reads. Refs: CIP-4282 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
_build-ffi-artifacts.yml downloaded its musl toolchain from musl.cc. On 2 October 2026 that download timed out from GitHub's runners on six tries, so protect-ffi's next release would fail at that step. Its digest also pinned the file without authenticating where it came from. The linux-x64-musl leg now builds inside the node:22-alpine image, at the digest that _build-auth-artifacts.yml uses since #1018. Alpine is a musl system, so its own compiler links musl. The container runs the matrix's build script and log with -crt-static, places the binary as the host legs do, and loads it with Node.js on musl. The host build steps skip that leg, and the Rust version is the runner's, as on the other five legs. ffi-preflight.yml now also installs the wrapper and the musl tarball inside the same Alpine image and loads the binding, as auth-preflight does. A test checks that every workflow uses one pinned Alpine image. Refs: CIP-4282 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
The script reads readelf's output into a variable because a pipe into `grep -q` fails open: grep exits at its match, readelf takes SIGPIPE, and under pipefail the match reads as a miss. The short fixtures did not catch a regression to the pipe, because readelf finished writing before grep exited. Two fixtures now name the C library first and then 3000 other libraries, so readelf is still writing when grep exits. With the pipe, the gnu and musl platforms reject them; with the variable, they accept. Refs: CIP-4284 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
e2820c2 to
309d787
Compare
Both mise steps in _build-ffi-artifacts.yml ran without a mise version, so the action installed the newest mise. mise 2026.10.0, released on 2 October 2026, refuses to install a cargo: tool until the Rust that mise is configured with is installed: tool 'cargo:cargo-zigbuild@0.23.0' requires configured install dependency 'rust@1.94.1', but its selected version is not installed The root mise.toml pins rust 1.94.1, and the zig step installs only `zig cargo:cargo-zigbuild`, so that Rust is never installed there. Main fails this way without this branch. ffi-preflight run 37079039403, dispatched on main at cb58a7b, failed both gnu legs at that step (jobs 111075318561 and 111075318565), with mise 2026.10.0. Main's run 36969039169 passed with the same mise at 05:27Z, because it built 966be05, which has no root mise.toml. The stack crates import added it later that day. integration-protect-ffi.yml still passes on main because its bare `mise install` installs rust 1.94.1 first. Both steps now pin mise 2026.4.0, the version that _build-auth-artifacts.yml and the EQL release builds pin. With the pin, ffi-preflight run 37078751162 passed every leg. A test checks that every mise step in the file carries the auth build's version. Refs: CIP-4282 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
309d787 to
b816cf9
Compare
One script now checks the C library of every Linux release binary
A Linux binary links one C library. Most Linux systems, such as Ubuntu, use glibc, the GNU C library. Alpine Linux uses musl, a smaller C library, and a glibc binary does not load there. So each Linux platform package must contain a binary that links the C library its name claims.
The release workflows check this by reading each binary's NEEDED entries. A NEEDED entry is a line in the binary's dynamic section that names a shared library the system must load with it. A gnu binary must need
libc.so.6. A musl binary must not needlibc.so.6, and must needlibc.musl-x86_64.so.1.This pull request fixes Linear issue CIP-4284 and Linear issue CIP-4282. It must merge before #1014 releases protect-ffi.
The C library rules now live in one script
Until now, three workflows each had their own copy of the rules:
_build-auth-artifacts.yml,auth-preflight.ymlandffi-preflight.yml. A preflight is a workflow that builds the real release packages and tests them, but cannot publish them. Nothing kept the three copies the same, and one had already drifted. The copy inffi-preflight.ymlrefused glibc for the musl binary, but it did not require musl, so it accepted a statically linked binary.scripts/check-c-library.sh <platform> <binary>now holds the rules, and every workflow that checks a Linux binary calls it. It fails for a platform it has no rule for, so a new Linux platform cannot ship unchecked. The two preflights now check out the built commit'sscripts/folder before they download the packages.scripts/__tests__/check-c-library.test.mjsruns the script with the realreadelfon six ELF files. They are a gnu binary, a musl binary, two binaries with long dynamic sections, and two statically linked binaries. The test also checks that every Linux platform package has a rule, and that no workflow keeps its own copy of the rules.The test writes these ELF files itself, from about 120 lines of JavaScript. I chose this over compiling them because no musl compiler is on the CI runners or on a developer's Mac. I chose it over checked-in binaries because a reviewer cannot read a binary file. The script reads only the NEEDED entries, through the real
readelf, so a small ELF file with the same entries is the same input.I also ran the script by hand on real glibc and musl libraries, compiled in Debian and Alpine. It gave the same answers as it gives for the test's files.
readelfis part of GNU binutils, which every Linux runner has and macOS does not. The script tests run in CI and on Linux, and they are skipped only outside CI whenreadelfis missing.Every protect-ffi build now checks its Linux binaries
Before this change, only
ffi-preflight.ymlchecked protect-ffi's C library, and nothing starts that preflight automatically.release.ymlbuilds through_build-ffi-artifacts.yml, so a release could publish a musl binary that links glibc. cipherstash-suite published a glibc-linked@cipherstash/auth-linux-x64-musl0.44.0 in that way.Each Linux leg of
_build-ffi-artifacts.ymlnow runs the script after it places the binary and before it packs it, as #1018 did for@cipherstash/auth.scripts/__tests__/ffi-build-artifacts.test.mjschecks the step's position, its Linux condition and the binary it reads.protect-ffi's musl binary now builds inside Alpine Linux
_build-ffi-artifacts.ymldownloaded its musl toolchain from musl.cc. On 2 October 2026, that download timed out from GitHub's runners on six tries in a row. The pinned digest also checked the file without checking who made it.The
linux-x64-muslleg now builds insidenode:22-alpine, at the digest that #1018 pinned for@cipherstash/auth. Alpine is a musl system, so its own compiler links musl. The container runs the build script and log that the matrix chooses, with-crt-staticso the binary stays dynamically linked to musl. It then places the binary as the other legs do, and loads it with Node.js on musl. The host build steps skip that leg.The container installs the Rust version that its runner reports, as the other five legs use their runners' Rust.
ffi-preflight.ymlnow also installs the wrapper and the musl package inside the same Alpine image, and loads the native binding. A test checks that all four workflows use one pinned Alpine image.mise is now pinned in the protect-ffi build, because main fails without it
The first FFI preflight on this branch failed on both gnu legs, at the step that installs zig and cargo-zigbuild. That step did not pin a mise version, so it installed mise 2026.10.0, released on 2 October 2026. That release refuses to install cargo-zigbuild until the root
mise.toml's Rust 1.94.1 is installed, and this step never installs it.Main fails in the same way. FFI preflight run 37079039403 on main, at
cb58a7b9, failed both gnu legs at that step with the same mise error. Main's earlier FFI preflight run 36969039169 passed with the same mise, because it built966be055. That commit has no rootmise.toml, which the stack crates import added later that day.integration-protect-ffi.ymlstill passes, because its baremise installinstalls Rust 1.94.1 first.Both mise steps in
_build-ffi-artifacts.ymlnow pin mise 2026.4.0, the version that the auth and EQL release builds pin. A test checks the pin. Without this commit, a protect-ffi release from #1014 would fail at the same step.Each new test fails when the code it covers is broken
I broke the code on purpose 31 times and ran the tests each time. This is a mutation check. Each time, at least one test failed, and I then put the code back.
The mutations were these:
readelfstraight intogrep -q, which can fail open. Only the long dynamic sections catch this, which is why the test has them.-crt-static, the load test, the file ownership fix and the build log, and brought back musl.cc.Both preflights passed on this branch
e2820c20, passed every leg and both smoke tests. That commit has the same files as the merged head.6e823f63, passed every leg and both smoke tests. The later commits change no auth workflow and do not change the script.Both smoke jobs printed "links the expected C library" for all three Linux packages, then "smoke OK" and "musl smoke OK".
After the rebase onto
c4ab67ac, I ran both preflights again on the headb816cf92. In auth preflight run 37081875739, only the Windows leg failed. Its "Add the Rust target" step timed out after 20 seconds while mise fetched wasm-pack from the GitHub API. This pull request does not change that step. FFI preflight run 37081873328 was still running when this pull request merged.pnpm test:scripts,pnpm run code:check, actionlint and shellcheck all pass locally.lint-release.ymlnow also runs shellcheck on the new script.Three decisions need a reviewer
integration-protect-ffi.ymlalso runs mise-action without a mise version. It passes today, because it installs Rust before cargo-zigbuild, so I left it alone.mise.toml's Rust instead.Linked issues
This PR fixed #1041 and #1043.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a