ci(release): pin both halves of the zig toolchain, and cut v0.8.1 - #199
Conversation
…more
The aarch64-unknown-linux-musl leg of the release matrix fails at link time:
error: unsupported linker arg: --fix-cortex-a53-843419
rustc stable passes that flag for aarch64 targets (the Cortex-A53 erratum
workaround), and zig's linker rejects unknown args outright rather than warning.
cargo-zigbuild's job is to translate rustc's linker invocation into something
zig accepts, and it learned to filter this one in 0.23.0
(rust-cross/cargo-zigbuild#452, `filter_linker_arg` in src/zig.rs).
The pin was 0.19.6, released 2024-12-16 — long before the flag existed. Nothing
in this repo changed; `dtolnay/rust-toolchain@stable` moved under it, which is
the hazard of pinning one half of a toolchain pair and floating the other. This
broke the v0.8.0 tag: three targets built, aarch64 did not, so no release was
created.
Zig deliberately stays on 0.13.0 — 0.15+ wants clang 18+ for crates using
bindgen, and duckdb-sys does.
Claude-Session: https://claude.ai/code/session_01GxGWpo3L4BRMkwtoa9X54W
…ld fix v0.8.0 was tagged and built three of four targets; the aarch64 leg failed to link, so `release.yml` never reached the create-release step and nothing shipped. The tag stays where it is — deleting a pushed tag buys nothing when no release, no asset, and no deploy ever referenced it — so the content moves to 0.8.1 instead. The CHANGELOG section is renamed rather than duplicated: release.yml extracts the section matching the tag and fails the build if there is none, and a two-line 0.8.1 section pointing at a 0.8.0 section would have produced release notes that omitted everything the release actually contains. The section now opens by saying there is no released 0.8.0 to upgrade from, so the gap in the version sequence is explained where someone will look for it. Claude-Session: https://claude.ai/code/session_01GxGWpo3L4BRMkwtoa9X54W
…ting Upgrading cargo-zigbuild fixed the aarch64 link error and broke both Linux targets a different way: they built, but produced dynamically linked binaries, which the static-link check rejected, so no tarball was produced. The cause is that `ziglang` is a pip dependency of cargo-zigbuild with an open upper bound (`ziglang>=0.9.0`), and the zig it installs lands on PATH ahead of whatever `setup-zig` provided. So this workflow has never actually built with the zig version it declared — pip's resolution decided, and it had drifted to 0.16, where the static build silently stops being static. Both halves are now pinned to what the matrix has really been building with: cargo-zigbuild 0.23.4 (has the aarch64 arg filter) and ziglang 0.13.0. The `setup-zig` step is dropped rather than left to imply a version it never controlled, and the install step now prints the zig version so the next person does not have to infer it from a libcxx include path in a build log. Claude-Session: https://claude.ai/code/session_01GxGWpo3L4BRMkwtoa9X54W
My last commit removed it on the reasoning that pip's ziglang wins on PATH and
the action was therefore decorative. That was wrong, and the next run said so:
libpcap's configure died with "C compiler cannot create executables". The two
installs serve different consumers.
* the action puts a `zig` *executable* on PATH, which the libpcap step needs
because it builds C with `CC="zig cc -target …"`. The pip package exposes
`python -m ziglang` and no `zig` binary, so nothing else provides it.
* pip's `ziglang` is what cargo-zigbuild uses to link the Rust side, and its
open-bounded requirement (`ziglang>=0.9.0`) is what had drifted to 0.16 and
quietly turned both Linux builds dynamic.
So both are required and both are now pinned to 0.13.0. The install step prints
each version separately, because a mismatch between them is exactly the kind of
thing that otherwise surfaces three steps later as an unrelated-looking C
compiler error.
Claude-Session: https://claude.ai/code/session_01GxGWpo3L4BRMkwtoa9X54W
There was a problem hiding this comment.
I have what I need. Writing the review.
Summary
This PR fixes the release pipeline: bumps cargo-zigbuild 0.19.6 → 0.23.4 (the aarch64-musl leg was failing on rustc's --fix-cortex-a53-843419), pins ziglang==0.13.0 (which pip had silently dragged to 0.16, turning the Linux builds dynamic), and renumbers the version to 0.8.1 across VERSION / Cargo.toml / package.json / CHANGELOG.md since v0.8.0 never produced a release. The diagnosis is sound and the comments explain the two-zigs/two-consumers split well; the version and changelog edits are self-consistent and I verified the release-note extraction still yields a full 92-line section for [0.8.1]. The one thing worth flagging before merge is that the version bump is one file short: server/Cargo.lock still records all 15 workspace crates at 0.8.0, so a PR whose whole point is a version renumber leaves the lockfile stale. Recommendation: APPROVE — the lockfile drift is cosmetic and no build path uses --locked (I checked), so it can't fail a release, but it should be a follow-up.
Suggestions
- server/Cargo.lock:1363–1693 — 15 workspace members (
h-api,h-capture,h-common,h-ebpf-common,h-export,h-llm,h-metrics,h-pcap-extract,h-protocol,h-storage,h-storage-aglake,h-storage-clickhouse,h-storage-duckdb,h-turn,heron) are still pinned atversion = "0.8.0"whileserver/Cargo.toml:11now says0.8.1. Both thelinuxandmacosrelease jobs (release.yml:191, release.yml:268) run plaincargo zigbuild/cargo buildwith no--locked, so the lockfile is auto-rewritten in CI and nothing breaks — but the committed file no longer agrees with the manifest, and a localcargo build --lockedwill fail for anyone who tries it. Regenerate with a plaincargo check -p heronand commit. - scripts/routers/shared/bump.sh:63–76 —
check_drift()only comparesVERSION,server/Cargo.toml, andconsole/package.json;Cargo.lockis invisible to it. That is why this drift slipped through:just bump checkreports "✓ in sync" while the lockfile is stale. Adding the workspace members' lockfile entries to the drift check (or runningcargo update --workspace --offlineat the end of a bump) would close the class rather than this instance. Non-blocking, and arguably out of scope for a release-fix PR. - CHANGELOG.md:9 — the
[0.8.1]header date is2026-09-12, identical to the[0.8.0]date it replaces, which is correct here (same-day re-cut) but means the two entries are indistinguishable by date if a[0.8.0]section is ever restored. The prose paragraph directly below already disambiguates, so this is fine as-is — noting it only so a future reader doesn't "fix" the date. - .github/workflows/release.yml:143 —
pip install cargo-zigbuild==0.23.4 ziglang==0.13.0pins both, which is right, butsetup-zig@v2at release.yml:122 is still itself unpinned to a major (the version it installs is, though). If the action'sversion: 0.13.0input ever changes its accepted format, the failure mode is the libpcapconfigureerror the commit message describes. Low risk; the twoecholines you added make the mismatch immediately visible, which is the right mitigation.
Questions
- The commit chain is four commits that walk a fix forward and then partly back it out:
3b46b2earemovessetup-zigon the reasoning that pip'sziglangwins on PATH, andcfaec6ffrestores it because libpcap'sCC="zig cc -target …"needs a realzigexecutable. That is a good story, but it means the intermediate commits on this branch are each independently broken. If this branch is ever bisected or the commits are replayed individually, two of the four fail. Was squashing considered — or is the step-by-step history intentional as documentation of the diagnosis? PATCH 3/4's message claims the zig on PATH from pip "lands ahead of whateversetup-zigprovided," andPATCH 4/4's message contradicts it by saying the pip package "exposespython -m ziglangand nozigbinary." The final comment block (release.yml:112–121) states the correct version. Flagging only because the two messages are in the same diff and a future reader consultinggit logwill hit the wrong one first — worth knowing the third message was superseded rather than trusting it.
Verified
- Release-note extraction still works. Traced the
awkat release.yml:328–332 against the renamed header:^## \[0.8.1\]matches CHANGELOG.md:9 (regex is right-unanchored, so the trailing em-dash + date are harmless),flagis set and the header printed, output runs to line 100 and exits at the## [0.7.3]header on line 101 — ~92 lines covering### Changed/### Added/### Fixed.[ -s release-notes.md ]passes; no other line in the file matches## [0.8.1]. - Version SSOT chain intact.
VERSION= 0.8.1;server/app/heron/src/main.rs:69uses clap'sversionattribute and main.rs:355/701/920 useenv!("CARGO_PKG_VERSION"), so the binary reports 0.8.1 fromserver/Cargo.toml:11;console/vite.config.ts:7,12reads../VERSIONdirectly, so the console version does not depend onpackage.jsonat all — thepackage.jsonbump is bookkeeping consistent with the bump script's declared derived files. - No
--lockedanywhere in the release path. Grepped release.yml and ci.yml: the only--lockediscargo install bpf-linker --locked(ci.yml:251), unrelated to the workspace lockfile. This is what makes theCargo.lockdrift cosmetic rather than a release-breaker. - Test compatibility.
server/app/heron/tests/cli_smoke.rs:13-25assertsheron --versioncontainsenv!("CARGO_PKG_VERSION")— i.e. it reads the same manifest value being bumped, so it tracks the bump rather than pinning a literal. No test hardcodes0.8.0. - CHANGELOG gap is intentional and explained. Confirmed no
[0.8.0]section remains (renamed, not duplicated, per the commit message) and the[0.8.1]preamble at CHANGELOG.md:11–14 explains the missing release to anyone reading the version sequence. - Leakage check. Read the full release.yml and the CHANGELOG diff: no private IPs, credentials, private keys, internal hostnames, or machine-specific absolute paths. Version pins, public GitHub URLs (
tcpdump.org, cargo-zigbuild issue refs), and CI runner paths only. - Scope. Changes are confined to
.github/workflows/release.yml,CHANGELOG.md,VERSION,console/package.json,server/Cargo.toml— no Rust logic, no console source, no SQL, no capture pipeline. The schema-drift / prompt-cache-key / body-scan / window-width classes from the review checklist are not reachable from this diff.
🤖 Reviewed by the review bot • workflow run
v0.8.0was tagged and never released: three of four targets built,aarch64-unknown-linux-muslfailed to link, sorelease.ymlnever reached thecreate-release step. Nothing in Heron caused it — two build dependencies drifted
under a pin that is two years old.
What was broken
cargo-zigbuildwas too old. rustc stable passes-Wl,--fix-cortex-a53-843419for aarch64 targets, and zig's linker rejectsunknown arguments outright:
cargo-zigbuild added a filter for it in 0.23.0
(rust-cross/cargo-zigbuild#452).
The pin here was 0.19.6, released 2024-12-16 — before the flag existed.
dtolnay/rust-toolchain@stablemoved underneath it.ziglangwas not pinned at all. It is a hard pip dependency ofcargo-zigbuild declared
ziglang>=0.9.0, so pip resolved it to whatever wasnewest — it had reached 0.16, where both Linux targets silently produce
dynamically linked binaries. The static-linkage check caught that, which is
why it surfaced as "no tarball" rather than as a broken release asset.
So the matrix has never actually built with the zig version this workflow
declares. Both halves are now pinned to 0.13.0 / 0.23.4, and the install step
prints each version — a mismatch between them otherwise appears three steps
later as an unrelated-looking C compiler error.
Both zig installs are needed, for different consumers: the action provides a
zigexecutable for the libpcap step (CC="zig cc -target …"), while pip'scopy is what links the Rust side. I removed the action at one point on the
assumption it was redundant; libpcap's configure immediately died with "C
compiler cannot create executables". The comment now records why both exist.
Verification
workflow_dispatchon this branch, all four targets:The upload step fails on this branch and that is expected, not a defect: the
package name is
heron-${GITHUB_REF_NAME}-<target>, and a branch namecontaining
/puts the tarball in a subdirectory wheredist/*.tar.gzcannotmatch it. On a
v*tag the ref name has no slash. Worth knowing before usingworkflow_dispatchto validate the matrix from afix/…branch.The version bump
v0.8.0stays where it is — deleting a pushed tag buys nothing when no release,asset, or deploy ever referenced it — so this cuts 0.8.1 with the same
content plus the build fix. The CHANGELOG section is renamed rather than
duplicated:
release.ymlextracts the section matching the tag and fails whenthere is none, and a stub 0.8.1 section pointing at 0.8.0 would have produced
release notes omitting everything the release contains. The section opens by
saying there is no released 0.8.0, so the gap in the sequence is explained where
someone will look.
https://claude.ai/code/session_01GxGWpo3L4BRMkwtoa9X54W