Skip to content

Publish prebuilt Linux (x86_64, aarch64) and macOS arm64 binaries as release assets - #369

Merged
fcostaoliveira merged 9 commits into
masterfrom
release-binaries-366
Oct 29, 2026
Merged

fcostaoliveira merged 9 commits into
masterfrom
release-binaries-366

Conversation

@fcostaoliveira

@fcostaoliveira fcostaoliveira commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #366

Each GitHub release now gets prebuilt archives and a checksum file:

  • vector-db-benchmark-<tag>-linux-x86_64.tar.gz
  • vector-db-benchmark-<tag>-linux-aarch64.tar.gz
  • vector-db-benchmark-<tag>-macos-aarch64.tar.gz (the nice-to-have)
  • SHA256SUMS

They're built by the new .github/workflows/release-binaries.yml. It runs on release: published, or on workflow_dispatch with a tag input and an optional clobber. Each archive is flat, so curl -L <asset> | tar xz && ./vector-db-benchmark --help works in the directory you extract it into. Every archive contains:

  • all six binaries: vector-db-benchmark, prepare-msmarco, generate-dataset, bench_npy, bench_jsonl, and bench_hdf5, which cargo also builds;
  • datasets/datasets.json, experiments/configurations/ and the random-100 smoke corpus, which the CLI reads from the working directory, the same set the Docker image ships;
  • LICENSE, README.md;
  • BUILD-INFO.txt: the tag, commit, target, features, profile, rustc, and the OpenSSL and HDF5 versions the built binary reports (its --describe build block), plus rustc -vV.

Target and libc: glibc built on Ubuntu 22.04, with HDF5, zlib and OpenSSL statically linked

The real portability problem was HDF5, not TLS. Today every binary links the system libhdf5_serial.so.103, which a fresh host doesn't have. I picked the simplest option that holds up:

  • Targets: x86_64-unknown-linux-gnu and aarch64-unknown-linux-gnu, each built natively on ubuntu-22.04 / ubuntu-22.04-arm. Native arm runners are free for this public repo, so there's no emulation and no cross toolchain. macOS is built on macos-14 with MACOSX_DEPLOYMENT_TARGET=11.0.
  • Static dependencies: a new opt-in cargo feature, release-static, links HDF5, zlib and OpenSSL statically.
    • hdf5-sys/static and zlib compile the HDF5 that hdf5-src bundles, with DEFLATE.
    • openssl/vendored covers OpenSSL, on Linux only.
    • The lock gains hdf5-src, openssl-src, libz-sys and cmake, and no existing version changes.
  • Default builds don't change. The Docker image, cargo build and CI don't enable the feature.
  • Result: every Linux binary needs only libc.so.6, libm.so.6 and libgcc_s.so.1. The newest glibc symbol is GLIBC_2.34, and check_portable_linkage now enforces that ceiling (MAX_GLIBC=2.34). The binaries run on Amazon Linux 2023, RHEL 9, Ubuntu 22.04+ and Debian 12+. CI runs them on bare ubuntu:22.04 and on amazonlinux:2023, the default AWS client image.
  • Why not musl:
    • HDF5 would have to be cross-built with cmake against musl.
    • musl's allocator is known to be much slower under multi-threaded allocation. That's the wrong trade for a load generator whose job is to measure latency.

TLS findings

cargo tree -i openssl-sys shows OpenSSL comes in through native-tls, from three sources:

  • our own reqwest (default-tls);
  • turbopuffer-client;
  • the default features of elasticsearch and opensearch.

qdrant-client, mongodb and tonic already use rustls. Switching everything to rustls would change runtime behaviour for every HTTP engine (ES/OpenSearch/Milvus/Weaviate/Chroma/Turbopuffer/Vertex): the backend, the trust store and the cipher set. So I kept native-tls/OpenSSL and vendored it in the release build only.

The binaries still use the host's CA store: native-tls finds it at runtime through openssl-probe, not at OpenSSL's compiled-in /usr/local/ssl. This is now checked in CI (see below), and I also checked it by hand with the static binary:

Host Certificate-validating HTTPS request
ubuntu:22.04 + ca-certificates verified; got the server's HTTP answer
Debian 12 verified; got the server's HTTP answer
bare ubuntu:22.04, no CA bundle fails before any HTTP answer (fails closed)
amazonlinux:2023 (CA bundle under /etc/pki/tls) verified (CI, both architectures)

The vendored OpenSSL ignores the host's crypto-policies, such as /etc/crypto-policies or a FIPS provider. The README says so.

ES and OpenSearch run with CertificateValidation::None here, as they did before, which is why the probe uses the Chroma client, which validates certificates.

--version and params.build

  • build.rs records the commit the binary was built from. The logic is in src/build_info/resolve.rs, which build.rs compiles via #[path] and which is unit-tested as build_info::resolve.
  • vector-db-benchmark, prepare-msmarco and generate-dataset now print --version as vector-db-benchmark 0.1.0 (git <full sha>). A build with uncommitted changes adds , dirty tree. The run banner prints the same line.
  • Every search and upload result file gets a params.build block, and vector-db-benchmark --describe build prints the same block. It has:
    • the source: crate_version, git_sha (null when unknown), git_tree, git_sha_source;
    • how it was compiled: target, profile, features, rustc;
    • the C libraries loaded at run time: openssl (null on macOS) and hdf5.
  • What that buys: a release archive (["release-static"], OpenSSL 3.6.3, HDF5 1.10.7) and a Docker or cargo build of the same commit ([], Debian's OpenSSL 3.0 and HDF5 1.10.8) are now distinguishable. TLS cost sits in the timed path of the managed engines, so that difference can matter.
  • How the fields are produced:
    • features is derived in build.rs from cargo's CARGO_FEATURE_* variables, not from a hand-kept list.
    • openssl is read from OpenSSL_version(). That's why openssl is now a direct Linux dependency; it was already in the lock through native-tls.
    • Rendering is a pure provenance_of(&BuildFacts).
  • The dirty flag covers exactly what the binaries are compiled from: build.rs, src/, Cargo.toml and Cargo.lock, untracked files included. That's the same set cargo watches, so the flag can't go stale, and a README or test edit doesn't mark the build dirty.
    • package.sh refuses that same scope with the same flags (uncommitted_changes in lib.sh), plus any tracked file, because the archive also ships README, LICENSE, datasets and configs.
  • build.rs never writes to or watches .git/index. It runs git with --no-optional-locks and watches HEAD, logs/HEAD, packed-refs, the branch ref, and the nearest existing ancestor of the ref's directory.
    • That last one covers the case reviewers found: with reflogs off, after git pack-refs --all --prune, a branch feat/x has neither a ref file nor refs/heads/feat.
    • A commit then changes none of the watched files, and the binary kept the previous SHA. Reproduced in a clone: stale with the old watch, correct now.
    • The helper is unit-tested, and the test goes red if it returns the directory unconditionally.
    • At 33caa04, every cargo build recompiled the crate.
    • At 1800bfa, a host-side git status (IDE, prompt) still caused one recompile.
    • Now there are none. Measured 0 recompiles, against 1 with the index watch restored.
  • With no .git of its own, as in the Docker build context, the commit is reported as unknown:
    • The build doesn't fail.
    • It never borrows an enclosing repository's HEAD. git's top level must be the crate directory.
    • VDB_BUILD_GIT_SHA can assert the commit. It must be a full 40-character SHA, or the build fails.
  • docker-publish-{master,release}.yml now pass --build-arg VDB_BUILD_GIT_SHA=${{ github.sha }}. That's the only change to them, and the Dockerfile ARG sits after the dependency-cache layer.
  • docker-build-pr.yml passes VDB_BUILD_GIT_SHA=${{ github.sha }} too, and requires the image's --version to end with (git <that sha>).
    • For a pull_request run, github.sha is the merge commit that actions/checkout builds the context from. head.sha would name a commit the image wasn't built from.

Reproducibility and re-runs

  • Build inputs:
    • Builds use --locked, from the tag's commit. The plan job resolves the tag to a commit, and each build job checks that its checkout is that commit.
    • The toolchain is pinned with RELEASE_RUST_TOOLCHAIN (1.99.1).
    • Release builds use no cache.
    • package.sh refuses a dirty tree.
  • Tag must match the crate version: a release tag must name the crate version: 0.2.0, v0.2.0, or a pre-release such as 0.2.0-rc.1. No + is allowed, because GitHub may rename assets named after it, which would leave SHA256SUMS stale.
    • The plan job runs check_release_tag against the tag's own Cargo.toml before anything is built, so a mismatch fails in seconds. Before this, it failed only after all three builds, which left a public "latest" release with no assets.
    • So the next release needs a Cargo.toml version bump first. README and AGENTS.md now have a "Cutting a release" section.
  • Upload: scripts/release/publish.sh handles it:
    • archives go up first and SHA256SUMS last;
    • an existing asset from a different build, or a stale archive of that tag, makes it refuse, unless the run was dispatched with clobber: true;
    • clobber deletes SHA256SUMS first, deletes stale archives, replaces the archives, then uploads the new SHA256SUMS;
    • a re-run after a partial upload of the same build (identical sha256 digests) just finishes the upload. In practice that means Re-run failed jobs, which reuses the run's artifacts. Re-run all jobs rebuilds, the digests differ, and the run refuses. README, AGENTS.md and the workflow header say so;
    • an asset whose upload never finished (state not uploaded) isn't served, so it's deleted and uploaded again without clobber;
    • the refusal tells "different digest" apart from "no digest reported";
    • a failed asset listing aborts, so it can never read as an empty release.
  • Data files from the commit: package.sh takes datasets/, experiments/configurations/, LICENSE and README from git archive HEAD, so a local build can't ship untracked or ignored files. On macOS it defaults MACOSX_DEPLOYMENT_TARGET to 11.0, so HDF5's C code isn't compiled for the build host's macOS.
  • Final check: after uploading, the job downloads what the release now serves, checks it against SHA256SUMS, and installs it with install-release.sh.

How CI exercises the archives (on every PR touching their inputs)

The same workflow runs on pull_request with a path filter: Cargo files, build.rs, src/**, Makefile, scripts/release/**, the installer, the datasets and configs that get packaged, LICENSE/README.md, and the workflow itself. It does everything except the upload:

  1. Build on all three targets through make release-archive, the same command a release uses:
    • runs the release-static unit tests first: a DEFLATE-compressed HDF5 round trip through the bundled HDF5, plus the build identity;
    • check_portable_linkage rejects any NEEDED library besides glibc and libgcc, and any glibc symbol newer than 2.34;
    • on macOS it rejects non-system dylibs and minos above 11.0. That catches a wrong deployment target for the link, but not a C object compiled for a newer macOS, which the linker only warns about. MACOSX_DEPLOYMENT_TARGET is exported for the cmake/cc builds for that reason;
    • then runs verify.sh on the build host.
  2. Fresh host: on native x86_64 and arm64 runners, a bare ubuntu:22.04 container with no compiler and no libhdf5, and with libssl3 removed:
    • verify.sh lists the members before extracting, refusing path traversal and links;
    • checks ldd resolves no libssl, libcrypto, libhdf5 or libz;
    • checks each clap binary prints exactly <name> <version> (git <sha>) for the expected commit, and that --help works;
    • checks that --describe build names the commit, a clean tree and release-static, and that on this host it loads the same OpenSSL and HDF5 that BUILD-INFO.txt recorded at build time. In CI that was OpenSSL 3.6.3 and 1.10.7 on both architectures, and null/1.10.7 on macOS;
    • runs real work: --describe datasets from the bundled json, generate-dataset → bench_npy, bench_jsonl on random-100, and HDF5 initialisation;
    • then the TLS probe: it must fail there with no CA bundle, and succeed in a second container with ca-certificates installed. A third container, amazonlinux:2023 (glibc 2.34), runs verify.sh and the positive probe. Before the negative probe, the same container must resolve the host and open TCP to port 443 (bash /dev/tcp), so the only thing left to fail is TLS, not DNS or the network. The probe's unauthenticated DELETE/POST to api.github.com is intentional and documented (GitHub returns 404; nothing is created).
  3. Checksums:
    • make release-selftest runs 95 checks of the shell logic:
      • every publish.sh branch against a fake gh, including unfinished uploads;
      • check_release_tag, including + and version mismatch;
      • the checksums.sh set checks;
      • verify.sh's unsafe-archive refusals, plus a wrong --expect-tag, a wrong BUILD-INFO sha, a --version mismatch and a missing release-static. These run against a stand-in archive that otherwise passes, as a positive control;
      • package.sh's tag gate and dirty-tree refusals (modified source, untracked src/ file, modified README). Each must fail before a fake cargo runs, and a clean tree must reach it;
      • check_portable_linkage refusing /usr/bin/curl;
    • make release-checksums writes and checks SHA256SUMS, and requires it to name exactly the archives present;
    • users' own sha256sum -c is run too;
    • scripts/release/install-test.sh runs the installer against a local stand-in for GitHub releases (11 cases):
      • the no-tag path the README one-liner uses, following /releases/latest;
      • an explicit tag;
      • no release at all;
      • a latest release with no archives;
      • SHA256SUMS not listing the platform;
      • a checksum mismatch;
      • an archive member outside DEST, and a link member (members are now checked before extracting);
      • a non-empty DEST;
      • glibc older than 2.34, via a fake getconf, refused before downloading.

harness_invariants.rs (INV-A4) fails if any of the following happens:

  • the workflow stops going through these make targets and scripts;
  • it uploads inline;
  • it builds without --locked;
  • its PR filter drops an archive input;
  • ci.yml's two path lists stop covering what INV-A4 reads: build.rs, release-binaries.yml, docker-build-pr.yml and scripts/release/**, which ci.yml now lists;
  • docker-build-pr.yml stops listing build.rs or passing the SHA, or docker-publish-{master,release}.yml stop passing VDB_BUILD_GIT_SHA (ci.yml lists both, since the test reads them);
  • lib.sh's COMPILED_INPUTS drifts from build.rs's.

Test of the release workflow

workflow_dispatch only works once a workflow file is on the default branch, so I tested the release: published path instead. A release event runs the workflow from the tag's commit.

Setup:

  • I created a throwaway pre-release, 0.1.0-ci.366, marked not-latest.
  • Its tag pointed at a temporary commit on top of this branch, which removed only the release trigger from docker-publish-release.yml. That stopped the test from pushing a public Docker Hub image I couldn't delete.
  • The first attempt found a real bug: on macOS, rustc -vV | grep -q took SIGPIPE under pipefail (exit 141). The same pattern made verify.sh's link check fail open on long listings. Both are fixed in 33caa04, with a regression test.

Run 40831018822 on the fixed head:

  1. Attempt 1, every job green:
    • built all three archives;
    • passed the bare ubuntu:22.04 checks on x86_64 and arm64, including both TLS probes;
    • publish.sh uploaded the three archives, then SHA256SUMS;
    • the job downloaded what the release served, checksums.sh check passed, cmp matched the built files, and install-release.sh installed from github.com and printed vector-db-benchmark 0.1.0 (git 05e6c9a…).
  2. Attempt 2, publish re-run: it reported already on the release with identical digests: SHA256SUMS … and uploaded nothing. The assets' updated_at didn't change.
  3. Attempt 3: I first replaced linux-x86_64.tar.gz on the release with a 15-byte foreign file, then re-ran publish. It refused with different digest: vector-db-benchmark-0.1.0-ci.366-linux-x86_64.tar.gz … Refusing to mix builds, and the release was left as it was.

Against the published assets, by hand:

  • In a stock ubuntu:22.04 (aarch64, glibc 2.35) with only curl and ca-certificates added, and no compiler or libhdf5: curl -L <asset> | tar xz && ./vector-db-benchmark --help worked, --version printed the tag's commit, and the README installer worked.
  • On macOS 27 (arm64): install-release.sh installed and ran the macOS archive, which links only /usr/lib and /System libraries.

Cleanup: I deleted the release with --cleanup-tag, which removed its assets and tag, and both temporary branches. No release, tag or branch from the test remains; only the Actions run logs do.

Not exercised live: clobber: true. It needs workflow_dispatch (so this workflow on master) and an existing release. make release-selftest covers it against the fake gh:

  • deletes SHA256SUMS first;
  • deletes stale archives;
  • replaces the archives;
  • uploads SHA256SUMS last.

After merge, a quick check would be: dispatch on a throwaway pre-release twice, the second time with clobber: true.

Install

The README has a new "Prebuilt binaries" section:

curl -fsSL https://raw.githubusercontent.com/redis/vector-db-benchmark/master/scripts/install-release.sh | bash   # latest, host OS/arch, checksum-verified
make install-release TAG=0.2.0 DEST=/opt/vdb

There's also a by-hand one-liner, labelled Linux, that picks linux-$(uname -m). Before extracting, the installer checks archive members, refuses a non-empty destination, and dies clearly when no sha256 tool is present. On Linux it refuses glibc older than 2.34, via getconf, before downloading anything.

Verification

  • make agent-check exits 0, unpiped, on the head commit.
  • actionlint shows no findings on the three changed workflows.
  • shellcheck -x (v0.10.0) is clean on scripts/install-release.sh and every scripts/release/*.sh.
  • make release-selftest on stock macOS without GNU tar now fails at once with "GNU tar is required … brew install gnu-tar", instead of dying inside bsdtar.
  • A local release-static aarch64 archive passed both fresh-host steps in ubuntu:22.04 (glibc 2.35). CI runs the same checks plus amazonlinux:2023 on every PR.
  • Mutation-checked (each mutation turned the suite red):
    • the build_info::resolve guards: own-checkout, override validation, HEAD validation, dirty flag;
    • both params.build writes;
    • params.build.features forced to release-static or dropped, and target hardcoded or dropped: each fails build_info tests;
    • INV-A4: dropping 'build.rs' from the release workflow's PR paths, dropping scripts/release/** from one ci.yml list, removing the Docker PR build's SHA, or letting lib.sh's COMPILED_INPUTS drift: each turns it red;
    • resolve() ignoring the dirty probe (dirty: Some(false)), features_from_env finding nothing, and build.rs hardcoding release-static: each fails a build_info test;
    • watching .git/index again: a host git status causes a recompile (1 against 0);
    • watching the ref directory only if it exists: a commit on a packed feat/x keeps the stale SHA, reproduced end to end in a clone;
    • publish.sh not recognising unfinished uploads; the installer without its link-member or glibc checks: each is caught by make release-selftest or install-test.sh;
    • check_release_tag accepting +, uncommitted_changes dropping either scope, verify.sh ignoring features: each is caught by make release-selftest. Removing the installer's member check is caught by install-test.sh;
    • clap --version;
    • the PR filter and the --locked invariant;
    • static HDF5 without zlib (fails the DEFLATE test);
    • 12 mutations of publish.sh, verify.sh, checksums.sh and the tag policy, all caught by make release-selftest;
    • the old pipe-based link check, caught by the 3000-member archive case;
    • the linkage check refusing a non-static build.
  • A real bug the linkage guard caught:
    • With the Makefile's exported HDF5_DIR, hdf5-sys links the system HDF5 even under release-static.
    • CI had passed only because a cached bundled build was reused.
    • package.sh now unsets it.

Left open

🤖 Generated with Claude Code

fcostaoliveira and others added 2 commits October 29, 2026 06:13
Adds release-binaries.yml: on release published (or workflow_dispatch with a
tag) it builds linux-x86_64, linux-aarch64 and macos-aarch64 archives natively,
with --locked from the tag's commit, writes SHA256SUMS and uploads them with
gh release upload. It refuses to overwrite existing assets unless dispatched
with clobber: true, which then replaces the whole set. The same jobs, without
the upload, run on PRs that touch anything the archives are built from. They
also run each archive in a bare ubuntu:22.04 container on both architectures.

Portability: glibc target built on Ubuntu 22.04, with HDF5, zlib and OpenSSL
compiled in through an opt-in release-static feature. Default builds (Docker,
CI, cargo build) are unchanged and keep the same native-tls/OpenSSL TLS stack.

--version (all clap binaries) and params.build in every result file now name
the git commit the binary was built from (build.rs, with logic in
src/build_info/resolve.rs). A tree without its own .git reports it as unknown
rather than guessing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base image ships libssl3, so asserting its absence failed on a genuinely
fresh host. The archive never links it (the build job rejects any binary that
does), so only libhdf5's absence is asserted. The platform also goes through an
env var, not an expression spliced into the shell text. Scripts are executable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🤖 Automated first-pass review. A human maintainer still needs to review this before merge.

What I checked:

  • build.rs and src/build_info/resolve.rs: the own-checkout guard, override validation, the dirty scope, and the rerun watches.
  • params.build in both result writers (experiment.rs:3040, :3265), plus --describe build.
  • The release-static feature wiring in Cargo.toml.
  • publish.sh's refuse/resume/clobber branches.
  • verify.sh, install-release.sh and tls-probe.sh.
  • The INV-A4 additions to tests/harness_invariants.rs.

What I found: nothing worth fixing before merge. The taxonomy's main risk is a value that still runs and still reports but is wrong. Here that would be a binary naming a commit it wasn't built from, and the PR treats that as the failure to prevent:

  • an enclosing repo's HEAD is never borrowed (resolve.rs, own_checkout);
  • a malformed VDB_BUILD_GIT_SHA fails the build instead of being recorded;
  • git_sha is JSON null, not a string, when unknown;
  • package.sh re-reads the built binary's own --describe build and refuses anything but a clean tree, release-static and the expected SHA.

The PR body already covers the fail-open tar tv | grep -q under pipefail, the stale SHA on packed slash refs, and the HDF5_DIR leak that linked system HDF5 under release-static. Each has a regression case, and most are mutation-checked, so I won't raise them again.

Optional / follow-up (not blockers):

  1. The Dockerfile's dependency cache now depends on build.rs and resolve.rs (Dockerfile:24, Dockerfile:36). Both are copied in before the "build dependencies only" layer, so any edit to either file rebuilds every dependency in the Docker build: the expensive layer the dummy-source trick exists to cache. Dependencies don't need the crate's own build script. A stub echo 'fn main() {}' > build.rs in that layer, with the real build.rs arriving with COPY src/COPY build.rs in the source layer, would keep the cache keyed on Cargo.toml/Cargo.lock only. Worth checking that cargo re-runs the real script there; it should, since the file's contents change.

  2. Every commit or checkout now rebuilds the lib and all binaries, docs-only commits included. logs/HEAD is watched (build.rs:946), so VDB_GIT_SHA changes and every env! consumer recompiles. Any embedded-SHA scheme pays this, and you measured the .git/index case down to 0. The per-commit cost is real for the make agent-check loop, though, and isn't mentioned. A line in AGENTS.md would stop the next person from chasing it as a regression.

  3. "latest" has no archives until the builds finish. On release: published, the release becomes "latest" before the three static builds (cmake HDF5 and vendored OpenSSL, uncached) attach anything. During that window the README one-liner refuses with "has no prebuilt archive". The message is clear, but the "Cutting a release" section could say to expect the gap, or to publish as a pre-release and promote to latest after publish goes green.

  4. tls-probe.sh depends on the wording of the Chroma engine's error messages (create collection failed vs create collection request failed, tls-probe.sh:2218–2222). Rewording either one fails the release check loudly, not silently, and src/** is in the path filter, so it surfaces on the PR that does it. A comment at the Chroma error sites pointing back at the probe would save that PR a confusing red job.

  5. Nothing yet stops comparing results from two different builds. params.build records the difference, but no result-reading code checks it. Until compare-results: warn when the two sides come from different builds (needs a build block in DumpHeader) #371 lands, results from the vendored OpenSSL 3.6 and from Debian's OpenSSL 3.0 can sit side by side in a comparison unflagged. The README paragraph says so, and that seems enough for now.

As the PR body says, clobber: true hasn't been exercised live, only against the fake gh. The post-merge dispatch it proposes (twice on a throwaway pre-release, the second time with clobber: true) is worth actually doing before the first real release.

Net: a large but carefully reviewed change. The new src/ surface is small, pure and unit-tested, every new guard is shown to fail when broken, and the release path was exercised end to end on a real throwaway release. The optional items are cache and docs polish, not correctness.

fcostaoliveira and others added 4 commits October 29, 2026 06:24
Blocking:
- fresh-host removes the stock image's libssl3 before running the archive,
  and verify.sh now asserts with ldd that no binary resolves libssl,
  libcrypto, libhdf5 or libz (or anything "not found").
- Publishing moved to scripts/release/publish.sh. The asset listing is a
  plain command substitution, so a gh/API failure aborts instead of reading
  as an empty release. --clobber is passed only when CLOBBER=true.

Also:
- Clobber deletes SHA256SUMS first, deletes stale archives of the tag,
  replaces the archives, then uploads SHA256SUMS.
- A re-run whose assets all have identical sha256 digests (a partial upload
  of the same build) completes the upload instead of refusing.
- The release toolchain is pinned (RELEASE_RUST_TOOLCHAIN=1.99.1) and checked.
- TLS is exercised in CI. tls-probe.sh makes a certificate-validating HTTPS
  request through the shipped binary on ubuntu:22.04 with ca-certificates
  (must succeed), and without them (must fail, as a negative control).
- make release-selftest (scripts/release/selftest.sh) covers the tag/version
  policy, the checksums.sh set checks, verify.sh's unsafe-archive refusals,
  and every publish.sh branch against a fake gh. It runs in the PR check.
- verify.sh inspects members (traversal, links) before extracting anything.
- build.rs also watches logs/HEAD (commit on a packed ref).
- Docker publish workflows pass VDB_BUILD_GIT_SHA=github.sha as a build arg,
  so images report their commit. Nothing else in them changes.
- The macOS archive's LC_BUILD_VERSION minos is checked against 11.0.
- LICENSE and README.md are added to the PR path filter (both are in the
  archive), and the Cargo.toml comment is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hdf5-sys builds its bundled HDF5 only when HDF5_DIR is unset. The Makefile
exports HDF5_DIR for ordinary builds, so `make release-archive` on a host with
libhdf5-dev linked the system library despite --features release-static. The
linkage check refused the archive (libhdf5_serial.so.103), which is how this
was found. The workflow's static unit-test step unsets it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release-event test run failed on macOS at `rustc -vV | grep -qx`.
grep exits at the match, rustc takes SIGPIPE, and pipefail reports 141.
The same pattern in verify.sh's link check failed OPEN: a link member early
in a listing larger than a pipe buffer made `tar tv | grep -q` exit 141,
which the `if` read as "no link", and the archive was extracted. The new
selftest case (link first, 3000 members) is red against the old check.

Membership tests now use a pipe-free in_list helper, the tar listing is
captured before it is matched, and the toolchain check compares a captured
value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fcostaoliveira fcostaoliveira changed the title Publish prebuilt Linux/macOS binaries as release assets (#366) Publish prebuilt Linux (x86_64, aarch64) and macOS arm64 binaries as release assets Oct 29, 2026
…build loop

Review of 33caa04 on #369:

- params.build now carries `target` (the triple cargo compiled for) and
  `features` (["release-static"] for release archives, [] for a source
  build), so a result from a release archive (vendored OpenSSL, bundled
  HDF5) is distinguishable from a source build of the same commit. Pinned
  in provenance_block_mirrors_the_constants, plus a target test checked
  against std::env::consts. Mutations (feature always on, key dropped,
  target hardcoded, target key dropped) each turn a test red.
- build.rs: reproduced the suspected loop. At 33caa04 every `cargo build`
  recompiled the crate, because `git status` rewrote .git/index, which is
  a watched input. git now runs with --no-optional-locks, and the dirty
  check is scoped to what the binaries are compiled from (build.rs, src,
  Cargo.toml, Cargo.lock: the same set cargo watches), so a README edit no
  longer marks a build dirty. Either change alone stops the loop; both
  are kept.
- tls-probe expect-fail first proves DNS+TCP reach to the same host:port
  in the same container, so the negative control can only fail at TLS.
  Documented the intentional unauthenticated DELETE/POST to api.github.com.
- install-release.sh: clear errors when there is no release, or when the
  latest release (today's bare 0.1.0) has no prebuilt archives.
- README: params.build target/features, macOS Gatekeeper note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fcostaoliveira fcostaoliveira left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from the deb/rpm packaging follow-up (#370), which is stacked on this branch. The packaging builds fine on top of it as-is (focal .deb and el8/el9 RPMs built and install-tested locally with --features release-static and the 1.99.1 pin), so nothing here blocks #369. Three things would make the stack cleaner if done here rather than after the fact:

  1. pull_request.paths: 'scripts/release/**' in release-binaries.yml now also fires on packaging-only changes (build-deb.sh, build-rpm.sh, test-package.sh, publish-apt.sh, publish-yum.sh, selftest-packages.sh), so every such PR runs the three static archive builds for nothing. Narrowing it to the archive scripts (lib.sh, package.sh, verify.sh, checksums.sh, publish.sh, selftest.sh, tls-probe.sh) avoids that. INV-A4 in tests/harness_invariants.rs asserts the literal 'scripts/release/**', so it needs the same edit.

  2. RELEASE_RUST_TOOLCHAIN: '1.99.1' lives only in the workflow's env:. #370 has to repeat the pin in release-packages.yml and adds an invariant that the two agree. If the pin lived in scripts/release/lib.sh (RELEASE_RUST_TOOLCHAIN="${RELEASE_RUST_TOOLCHAIN:-1.99.1}") and the workflow read it from there, both pipelines would follow one line, and make release-archive on a laptop would use the pinned toolchain too.

  3. The dirty-tree / VDB_BUILD_GIT_SHA == HEAD guard in package.sh (lines 54-58) is needed by the package builds as well. #370 adds require_clean_head and ensure_release_toolchain to lib.sh for that; if package.sh called the same helpers, the "tree must be committed" policy would have one implementation. Happy to rebase #370 onto whatever shape you prefer.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

…e gate)

B1 build.rs no longer watches .git/index. Any `git status` (IDE, prompt)
   rewrote it and recompiled the lib and every bin; reproduced at 1800bfa
   (1 recompile after a host `git status`), 0 now, 1 again when the
   watch is restored. The branch ref's directory is watched for the
   packed-ref + core.logAllRefUpdates=false commit case.
B2 Tests that kill the reported survivors. resolve(): a dirty probe
   must give "dirty" and ", dirty tree". provenance_of() is a pure
   function of BuildFacts (the null-SHA branch is tested). features are
   derived in build.rs from CARGO_FEATURE_* (features_from_env, unit
   tested), and checked against cfg!(feature) in both builds.
B3 CI coverage:
   - ci.yml reruns on release-binaries.yml, docker-build-pr.yml and
     scripts/release/**, which INV-A4 reads. INV-A4 asserts that, and
     also that docker-build-pr lists build.rs and passes the SHA, and
     that lib.sh's COMPILED_INPUTS equals build.rs's.
   - scripts/release/install-test.sh runs the installer against a local
     stub of GitHub releases: no-tag /latest install, explicit tag, no
     release, latest without archives, SHA256SUMS without the platform,
     checksum mismatch, a member outside DEST, a non-empty DEST.
   - selftest: package.sh's tag gate and dirty-tree refusals (modified
     source, untracked src file, modified README) fail before a fake
     cargo, with a clean-tree positive control. check_portable_linkage
     refuses /usr/bin/curl. verify.sh refuses a wrong --expect-tag, a
     wrong BUILD-INFO sha, a --version mismatch and missing
     release-static, against a stand-in archive that otherwise passes.
   - docker-build-pr builds with VDB_BUILD_GIT_SHA=github.sha (the
     merge commit it checks out) and requires --version to name it.
B4 The plan job runs check_release_tag on the tag's Cargo.toml before
   building. "Cutting a release" is documented in README and AGENTS.md.

1 Release tags with '+' are refused (GitHub may rename the assets).
2 package.sh and the test step unset OPENSSL_NO_VENDOR and HDF5_VERSION.
3 GNU tar is required with a clear error (require_gnu_tar) in
  package.sh and selftest.sh.
4 otool -l is captured before awk (the SIGPIPE pattern again).
5 install-release.sh checks members before extracting, refuses a
  non-empty DEST, and dies clearly without sha256sum/shasum.
6 params.build, --describe build and BUILD-INFO.txt add profile, rustc,
  and the OpenSSL / HDF5 loaded at run time. verify.sh requires the
  host run to load what the build recorded. openssl is now a direct
  Linux dependency (already in the lock) so its version can be read.
7 MAX_GLIBC=2.34 (current archives need 2.34). amazonlinux:2023 runs
  verify.sh and the expect-ok TLS probe.
8 Docs: the by-hand line is Linux-only, a shellcheck SC2016 annotation,
  the GitProbe::dirty doc, the minos comment, and a README note on
  crypto-policies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fcostaoliveira

Copy link
Copy Markdown
Collaborator Author

Status of each point in the automated first-pass review (posted on 33caa04), as of 1800bfa and e052840:

Worth fixing before merge

  1. params.build can't tell a release archive from a local build. Fixed.
    • 1800bfa added target and features.
    • e052840 adds profile, rustc, and the openssl / hdf5 versions loaded at run time.
    • features is derived in build.rs from cargo's CARGO_FEATURE_* variables, and checked against cfg!(feature) in both the default and release-static builds.
    • --describe build prints the block. package.sh copies it into BUILD-INFO.txt, and verify.sh requires the archive, run on the fresh host, to load the same OpenSSL and HDF5 that were recorded at build time.
    • Rendering is now a pure provenance_of(&BuildFacts), so the git_sha: null branch is tested.
    • Follow-up for compare-results to warn on differing builds: compare-results: warn when the two sides come from different builds (needs a build block in DumpHeader) #371.
  2. Extra rebuild from .git/index. Confirmed and fixed.
    • At 33caa04, every cargo build recompiled the crate.
    • 1800bfa added --no-optional-locks and scoped status. e052840 also stops watching .git/index: a host-side git status (IDE, prompt) had still caused one recompile.
    • Measured: 0 recompiles now, 1 with the watch restored.

Optional / follow-up

  • Weak negative TLS control. Fixed in 1800bfa. expect-fail first proves DNS plus a TCP connect to the same host:443 inside the same container (bash /dev/tcp), so only TLS is left to fail.
  • Unauthenticated DELETE/POST to api.github.com. Kept, and documented as intentional in tls-probe.sh. GitHub answers 404 and nothing is created, and a certificate-validating request is the point of the probe.
  • Stale / overly broad dirty flag. Fixed. The dirty check covers exactly what is compiled (build.rs, src, Cargo.toml, Cargo.lock, untracked files included), which is the same set cargo watches.
    • package.sh uses that same scope and flags, plus every tracked file, because the archive ships README, LICENSE, datasets/ and experiments/.
    • INV-A4 keeps the shell and Rust lists equal.
  • Misleading install-release.sh errors. Fixed: "no published release", and "the latest release, X, has no prebuilt archive". The latter is what 0.1.0 gives today.
    • scripts/release/install-test.sh now runs both against a local stand-in for GitHub on every PR, together with the no-tag install path (the README one-liner).
  • macOS Gatekeeper. README line added, with the xattr -d com.apple.quarantine workaround. Signing is deferred, because it needs an Apple Developer ID the repo doesn't have.
  • Mutation evidence for INV-A4. Each of these turns it red:
    • dropping 'build.rs' from the release workflow's PR paths;
    • dropping scripts/release/** from one of ci.yml's lists;
    • removing the Docker PR build's VDB_BUILD_GIT_SHA;
    • letting lib.sh's COMPILED_INPUTS drift from build.rs.
  • Stale "Still to do" list in the body. Gone. The body now lists what ran.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

2 similar comments
@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

…un docs)

B1 build.rs watches the nearest EXISTING ancestor of the branch ref's
   directory (resolve::nearest_existing_ancestor, unit-tested). Before,
   after `git pack-refs --all --prune` with reflogs off, a branch `feat/x`
   had no ref file and no refs/heads/feat to watch, so a commit did not
   re-run build.rs and the binary reported the previous commit.
   Reproduced in a clone: STALE with the old watch, the new SHA with the
   fix. Making the helper return the dir unconditionally fails its test.
B2 README, AGENTS.md and the workflow header: after a failed publish or
   post-upload check, use "Re-run failed jobs" (same artifacts, same
   digests). "Re-run all jobs" rebuilds and is refused. A deliberate
   rebuild is workflow_dispatch with clobber: true.

1 publish.sh: an asset whose upload never finished (state != uploaded)
  is deleted and re-uploaded without clobber. The refusal tells "no
  digest reported" apart from "different digest" and points at "Re-run
  failed jobs". The listing is name/state/digest, digest last, so an
  empty digest cannot shift fields under tab IFS. Selftest covers it
  (95 checks).
2 Exec bit restored on install-release.sh, verify.sh, install-test.sh.
3 package.sh defaults MACOSX_DEPLOYMENT_TARGET to MAX_MACOS_MINOS.
4 package.sh takes datasets/configs/LICENSE/README from `git archive
  HEAD`, not the working tree.
5 lib.sh: `grep -o GLIBC_ || true`, so a binary with no GLIBC symbols
  reports "none" instead of aborting under pipefail.
6 Comments no longer claim the HDF5/OpenSSL version checks catch a
  system link (Ubuntu 22.04's libhdf5 is also 1.10.7). The NEEDED and
  ldd checks do.
7 Stale comments: BuildSource.dirty, the install-release.sh header (HTTP
  stub, not file://), the tls-probe.sh header (amazonlinux:2023).
8 install-test.sh: no "Terminated" noise, plus link-member and
  old-glibc refusal cases (11 cases).
9 INV-A4 also requires docker-publish-{master,release}.yml to pass
  VDB_BUILD_GIT_SHA. ci.yml lists both, since the test reads them.
10 install-release.sh refuses glibc < 2.34 (getconf) before
   downloading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fcostaoliveira

Copy link
Copy Markdown
Collaborator Author

Replies as of e5d42d6.

The deb/rpm follow-up review (#370, on 1800bfa). All three points are deferred to #370, which owns that refactor, so that #369 doesn't change shape under the stacked PR:

  1. Narrow scripts/release/** in the release workflow's PR paths. Deferred.
    • The glob is now also listed in ci.yml, because INV-A4 reads those scripts. INV-A4 asserts the literal in both workflows.
    • If Native .deb/.rpm packages for the Redis APT/YUM repositories (#366 follow-up) #370 narrows it, both lists and INV-A4 need the same edit.
    • The narrowed list must keep install-test.sh, which the checksums job runs, alongside lib.sh, package.sh, verify.sh, checksums.sh, publish.sh, selftest.sh and tls-probe.sh.
  2. Move the RELEASE_RUST_TOOLCHAIN pin into lib.sh. Deferred. The pin is still the workflow env: (1.99.1), and INV-A4 requires the workflow to mention RELEASE_RUST_TOOLCHAIN.
  3. Share the clean-tree helper. Deferred. The current helper is uncommitted_changes in scripts/release/lib.sh:

The automated review on e052840:

  1. compare-results ignores params.build. Filed as compare-results: warn when the two sides come from different builds (needs a build block in DumpHeader) #371. It needs a build block in DumpHeader first.
  2. "Re-run failed jobs" vs "Re-run all jobs". Fixed in e5d42d6.
    • README, AGENTS.md and the workflow header now say: after a failed upload or post-upload check, use Re-run failed jobs, which reuses the same artifacts so the digests match.
    • Re-run all jobs rebuilds and is refused.
    • A deliberate rebuild is workflow_dispatch with clobber: true.
    • publish.sh's refusal now points at that too. It also replaces an upload that never finished (state != uploaded) without clobber, which is covered by the self-test.
  3. Assetless "latest" while the archives build. Deferred: Release binaries: avoid an assetless "latest" release while the archives build #372, which proposes publishing with make_latest: false and flipping it after the verified upload.
  4. PR body still said 2.35. Fixed. The body now says 2.34 everywhere.
  5. clobber: true not exercised live. Still open, and listed in the PR body. It needs the workflow on master: dispatch twice on a throwaway pre-release, the second time with clobber: true, before the first real release.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

@fcostaoliveira

Copy link
Copy Markdown
Collaborator Author

Replies to the automated review on e5d42d6, items 1, 2 and 4. Items 3 and 5 are already tracked in #372 and #371.

  1. build.rs and src/build_info/resolve.rs are copied before the dependency-only Docker layer. Agreed: editing either file rebuilds every dependency in the Docker build. That costs build time, not correctness. Both files are small and rarely change, and moving them after the dependency layer means restructuring the cached Dockerfile layers. Deferred to Release binaries follow-ups: Docker layer caching, recompile-on-commit note, TLS probe matching #373.
  2. Every commit or checkout recompiles the crate (logs/HEAD and the ref directory are watched). This is intended: the binary's embedded commit has to follow HEAD, and a stale commit is the failure this PR exists to prevent. The build.rs comment explains it. The one-line AGENTS.md note is folded into Release binaries follow-ups: Docker layer caching, recompile-on-commit note, TLS probe matching #373 so this PR doesn't need another CI round.
  3. tls-probe.sh matches the wording of the Chroma client's error messages. Agreed, and accepted for now. If the message is reworded, the release check fails closed: it goes red and never passes wrongly. The same-container TCP precheck already rules out DNS and network as the cause. Matching on a typed error kind needs the client to expose its cause chain. That is also tracked in Release binaries follow-ups: Docker layer caching, recompile-on-commit note, TLS probe matching #373.

Seven independent review passes now approve e5d42d6: release workflow, packaging, provenance, TLS/runtime, CI coverage, install and docs, and a maintainer pass.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

@fcostaoliveira
fcostaoliveira marked this pull request as ready for review October 29, 2026 09:46
@fcostaoliveira
fcostaoliveira merged commit 50b38e1 into master Oct 29, 2026
27 checks passed
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.

Publish prebuilt Linux binaries (x86_64, aarch64) as release assets

1 participant