Skip to content

build(dist): ship attested cross-platform binaries, all built on Linux - #77

Merged
wenzowski merged 5 commits into
mainfrom
claude/cloud-173-dist
Aug 7, 2026
Merged

wenzowski merged 5 commits into
mainfrom
claude/cloud-173-dist

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Implements CLOUD-173. Every release to date shipped zero assets — mise run release is release-plz release, which tags and creates the release entry but never builds. There was nothing to install, which blocks dogfooding Batten as consumer #1 and left house-style §12 prose with no mechanism.

What ships

mise-tasks/dist builds --profile dist (the profile Cargo.toml already declared and nothing used) for one target and stages a named archive. release-artifacts.yml calls that same task per matrix leg on release: published, attests each binary, and uploads it — so the archive CI publishes is one a maintainer reproduces locally.

Every leg runs on ubuntu-latest. No macOS runner, no Windows runner.

Target Linker via
x86_64-unknown-linux-gnu cargo (native)
aarch64-unknown-linux-gnu cross
x86_64 / aarch64-unknown-linux-musl cross
x86_64-pc-windows-gnu cross
aarch64 / x86_64-apple-darwin cargo-zigbuild

rustup target add installs only the standard library; the linker is what blocks a cross-build. cross supplies one per target via containers. For Darwin, zig does — verified on this repo before adopting: both triples link from Linux and produce Mach-O 64-bit arm64/x86_64 executable carrying LC_CODE_SIGNATURE, the ad-hoc signature Apple Silicon requires before it will exec an arm64 binary at all. No Apple SDK involved.

Windows ships the gnu triple rather than msvc because gnu links from Linux; msvc can only be linked on Windows and would cost a 2×-billed runner.

Provenance binds to the binary, not the archive, so repackaging cannot launder it. mise-action runs with cache: false — zizmor flags cache-poisoning as high severity on artifact-publishing workflows, and a poisoned entry would land inside a signed artifact that the attestation would then faithfully vouch for.

The dependency-drift risk, and two gates for it

The SDK-free macOS build holds only while nothing links a macOS system framework. Nothing caught that before: cross-check runs cargo check, which stops at codegen-to-metadata and never links, so the first symptom would have been the release workflow failing after a tag was cut.

  • macos-link-check (in the hk gate, ~1s) resolves the graph with --filter-platform aarch64-apple-darwin — load-bearing, since a macOS-only transitive dep is otherwise invisible from a Linux host. Fails on any package declaring a native links key (general, no list) or a named framework crate (covers the build-script case the first rule can't see). Verified both directions: green as-is, and adding native-tls fails it naming all five offending packages.
  • darwin-link (its own parallel CI job) closes the residual gap the only way with no false negatives — by actually linking. One target per matrix leg, concurrent with ci and cross.

CI cost

Net work goes down. Both Darwin triples left cross-check, where they were cargo checked and never caught this anyway; cross-check now covers windows-gnu alone, so the cross job — the current critical path at ~62s — sheds two thirds of its work.

darwin-link measured 7s per leg (dev profile: linking is what's under test, so LTO proves nothing extra), against a critical path of 46–63s. A test asserts it never grows a dist profile.

Tests

tests/dist.bats, tests/macos-link-check.bats, tests/darwin-link.bats — 38 bats tests total, plus actionlint and zizmor clean. Proven end to end locally: a 494 KB archive extracting to a working batten reporting 0.0.14, and both Darwin targets linking.

Known gaps

  • The release matrix has never run. Linux and Darwin are proven locally; the cross/musl/windows legs are first-run on the next release. fail-fast: false keeps one bad leg from taking the others down, and workflow_dispatch re-runs against an existing tag.
  • The 7s timing is local with warm deps. The first CI run on a cold rust-cache will be slower; the per-target cache key should settle it. If it lands over the critical path, drop to a single Darwin target.
  • macOS binaries are ad-hoc signed, not notarized. Fine for curl/mise fetches (no quarantine attribute); a browser download would need Developer ID signing and notarization. That belongs with CLOUD-65, which remains blocked by CLOUD-22 on licensing.

Refs CLOUD-173.


Generated by Claude Code

@linear-code

linear-code Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
CLOUD-173 Attach attested, cross-platform binaries to every GitHub release

Why

Every release to date ships zero assets. mise run release is only release-plz release — tag plus a GitHub release entry — and no workflow ever runs cargo build --release. cross-check type-checks three targets (cargo check, codegen-to-metadata, no linking) so it never produces a linkable binary; a green cross reads as "we build for those platforms" when we do not.

That blocks dogfooding, which is the point: Batten is its own consumer #1, and there is no compiled batten for a maintainer to install and run. It also leaves house-style §12 ("a single static binary, installable binary-first, signed and attested") entirely prose — no mechanism, which non-negotiable rule 2 calls half a change.

[profile.dist] already exists in Cargo.toml, documented as "the profile the distributed single binary is built with", and nothing references it.

Scope

This issue is the artifact half only: build, archive, attest, upload. Package-manager distribution and cargo binstall are CLOUD-65 (Phase 4, blocked by CLOUD-22 on licensing). Attaching assets to a private repo's releases needs no licensing decision, and CLOUD-65 consumes these assets when it lands, so this unblocks rather than duplicates it.

Definition of done

  • A dist task builds --profile dist for a given target and stages a named archive — the same command CI runs and an agent runs locally
  • Release workflow builds a matrix of x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-apple-darwin, x86_64-pc-windows-msvc
  • Each binary carries a GitHub build-provenance attestation (§12: signed and attested)
  • Archives upload to the GitHub release created by release-plz
  • No binary is committed to the repository

Acceptance

  • A release publishes with one archive per target plus attestations
  • mise run dist <target> produces the same archive locally that CI uploads
  • Archive naming is stable and parseable, so CLOUD-65's cargo binstall path can resolve it

Ready — refinement specialization

  • Source of truth (clause 1): the archive name and build profile are defined once in the dist task; the workflow calls it per target and never re-implements the build. [profile.dist] in Cargo.toml is the profile authority.
  • Mechanism / gate (clause 2): mise run test:bats over the dist script — naming convention, per-OS archive format, and refusal on a missing target are computable predicates with exit codes. mise run dist itself exits non-zero on a failed build.
  • Effect (clause 3): dist is write — it creates dist/, which the caller can recreate. Publishing is workflow-side, not a Batten verb.
  • Output & exit (clause 5): dist prints the archive path (a pointer, never the bytes) and exits 0; a failure is non-zero.
  • Commit / bump (clause 6): ci/build — tooling, no semver bump on the crate.
  • Test (clause 7): bats over the naming and format logic; the first real release is the end-to-end proof.
  • Blockers (clause 8): none. Does not inherit CLOUD-65's CLOUD-22 licensing blocker, which governs public package-manager distribution.

Cost note: the repo is private, so macOS runners bill at 10× and Windows at 2×. A release build is a few minutes per target and runs only on release, not per PR.

Review in Linear

@wenzowski
wenzowski marked this pull request as ready for review August 7, 2026 04:22
@wenzowski wenzowski closed this Aug 7, 2026
@wenzowski wenzowski reopened this Aug 7, 2026
claude added 5 commits August 7, 2026 04:45
Every release so far shipped zero assets. `mise run release` is
release-plz only — tag plus a release entry, never a build — and
cross-check runs `cargo check` (codegen to metadata, no linking), so a
green `cross` proved compilation without ever producing a runnable
binary. There was nothing to install, which blocks dogfooding Batten as
consumer #1 and left house-style §12 prose with no mechanism.

mise-tasks/dist builds --profile dist (the profile Cargo.toml already
declared and nothing used) for one target and stages a named archive.
The release workflow calls that same task per matrix leg, so the archive
CI uploads is the one a maintainer reproduces locally.

The archive name is a contract, not a convenience: CLOUD-65's binstall
path resolves assets by name, so naming and per-platform format are
pinned in tests/dist.bats rather than left to whatever the last release
emitted.

Provenance binds to the binary, not the archive, so repackaging cannot
launder it. mise-action runs with cache: false — a poisoned cache entry
would land inside a signed artifact and be faithfully attested.

Refs CLOUD-173.
…ows runners

The first cut of this workflow ran macOS and Windows legs, which bill at
10x and 2x on a private repo. Replace them: every leg now runs on
ubuntu-latest, with non-native targets built by cross, which compiles
inside a per-target container carrying that target's linker and sysroot.
`rustup target add` supplies only the standard library — the linker is
what actually blocks a cross-build.

Targets: linux gnu and musl on x86_64 and aarch64, plus windows-gnu.
musl is the statically linked binary §12 asks for, portable across glibc
versions. Windows ships the gnu triple because it links from Linux;
msvc would need a Windows runner.

macOS produces no artifact and cannot: linking Darwin needs Apple's SDK,
which no container can carry, so it requires a macOS runner. That is a
spending decision, recorded on the issue rather than silently incurred.

Refs CLOUD-173.
Darwin was recorded as the one target requiring a paid macOS runner.
That was wrong, and testing it here disproved it: cargo-zigbuild links
both apple-darwin targets from Linux, producing a real Mach-O 64-bit
arm64/x86_64 executable carrying LC_CODE_SIGNATURE — the ad-hoc
signature Apple Silicon requires before it will exec an arm64 binary.
No Apple SDK, no container image, no macOS runner.

zig ships the Darwin linker support, which is the piece rustup does not
provide: rustup target add installs only the standard library, and the
linker is what blocks a cross-build.

The whole matrix is now ubuntu-latest: linux gnu and musl on x86_64 and
aarch64, windows-gnu, and both macOS targets.

The caveat is dependency drift, not the toolchain. A crate linking a
system framework (CoreFoundation, Security) would need SDKROOT and a
real macOS SDK, which is where Apple's licensing question returns.
Nothing in the tree does today.

Refs CLOUD-173.
The macOS artifacts are linked on Linux by zig with no Apple SDK, which
holds only while nothing links a macOS system framework. Nothing catches
that today: cross-check runs cargo check, which stops at codegen and
never links, so the first symptom would be the release workflow failing
after a tag was already cut.

macos-link-check resolves the graph with --filter-platform for
aarch64-apple-darwin, so a macOS-only transitive dep is visible from a
Linux host, and fails on either a package declaring a native links key
(general, needs no list) or a named framework-linking crate (covers the
build-script case the first rule cannot see).

Verified in both directions: green on the tree as it stands, and adding
native-tls — the crate a default-featured HTTP client pulls in — fails
it naming all five offending packages.

The named list is incomplete by construction; the residual gap closes by
actually linking the Darwin targets, which is the next step on the issue.

Refs CLOUD-173.
…l path

macos-link-check reads a list of framework-linking crates, so it is
incomplete by construction. Close that gap the only way with no false
negatives: link the target. It either links or it does not.

Structured to cost no wall-clock. It is its own CI job with one target
per matrix leg, so the legs run concurrently with each other and with
ci/cross, and nothing was added to mise run ci or the pre-commit hook.
Measured 7s per leg against a critical path of 46-63s.

7s, not the 35s an optimized build takes: linking is what is under test,
so the dev profile proves the same thing without LTO. A test asserts the
task never grows a dist profile, so that cannot regress quietly.

Net CI work goes down. Both Darwin triples leave cross-check, where they
were cargo check'd — which never links and so never caught this class of
break anyway. cross-check now covers windows-gnu alone.

A file task, not an inline one: mise appends CLI args to inline task
bodies, which passed a stray positional to cargo.

Refs CLOUD-173.
@wenzowski
wenzowski force-pushed the claude/cloud-173-dist branch from a11bb6a to 6730729 Compare August 7, 2026 04:46
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 6730729 into main Aug 7, 2026
7 checks passed
@wenzowski
wenzowski deleted the claude/cloud-173-dist branch August 7, 2026 04:48
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.

2 participants