Releases

Two complementary tools own the release pipeline:

ToolOwns
release-plzversion bumps, git tags, crates.io publish, per-crate CHANGELOG.md
cargo-distcross-target binary tarballs, curl | sh / PowerShell installers, sha256 sidecars

They run on the same tag (nexo-rs-v<version>) and stay independent — no overlapping config. Phase 27 brings both online; Phase 27.2 wires the GitHub Actions workflow that combines them on tag push.

What ships

The nexo binary is the only artifact in release tarballs. Every other binary in the workspace (driver subsystem, dispatch tools, companion-tui, mock MCP server) carries [package.metadata.dist] dist = false so cargo-dist excludes it. Dev / smoke programs (browser-test, integration-browser-check, llm_smoke) live as [[example]] entries under examples/ for the same reason.

Build provenance — nexo version

build.rs injects four stamps captured at compile time:

  • NEXO_BUILD_GIT_SHA — short git SHA of the build commit (or unknown outside a git checkout)
  • NEXO_BUILD_TARGET_TRIPLE — full Rust target triple
  • NEXO_BUILD_CHANNEL — opaque channel marker; defaults to source. The release workflow overrides via NEXO_BUILD_CHANNEL=apt-musl (etc.) so support tickets carry install-channel provenance.
  • NEXO_BUILD_TIMESTAMP — UTC ISO8601 timestamp of the build

Operators see them with:

nexo version
# nexo 0.1.1
#   git-sha:   abc1234
#   target:    x86_64-unknown-linux-musl
#   channel:   apt-musl
#   built-at:  2026-04-27T12:34:56Z

nexo --version (without --verbose or the subcommand) prints the short form nexo <version>.

Local validation

make dist-check

Builds the host-target tarball via dist build --target $(rustc -vV | sed -n 's|host: ||p') and runs scripts/release-check.sh. The smoke gate verifies every present tarball contains the bin + LICENSE-* + README.md and that the host-native --version output matches the workspace version. Targets the local toolchain can't satisfy emit [release-check] WARN lines instead of failing.

Full setup notes (cargo-dist, cargo-zigbuild, zig, rustup targets): packaging/README.md.

What's automatic vs manual

StepOwner
Bump version + open release PRrelease-plz (CI on push to main)
Tag commit + crates.io publishrelease-plz (on PR merge)
Build 2 musl tarballs (x86_64 + aarch64)release.yml (Phase 27.2 ✅) — cargo-dist
Build Termux .deb (aarch64-linux-android)release.yml (Phase 27.2 ✅) — packaging/termux/build.sh
Upload tarballs + Termux deb + sha256 sidecarsrelease.yml (Phase 27.2 ✅)
Smoke-test nexo --version + provenance stampsrelease.yml (Phase 27.2 ✅)
Sign tarballs + Termux deb (cosign keyless)sign-artifacts.yml (Phase 27.3 ✅)
Generate CycloneDX + SPDX SBOMssbom.yml (Phase 27.9 🔄)
Apt repo publish + signed Release filePhase 27.4 deferred
Yum / dnf repo publishPhase 27.4 deferred
Termux pkg indexPhase 27.8 deferred
Homebrew bottle auto-PRPhase 27.6 PARKED (Apple targets dropped)
nexo self-updatePhase 27.10 deferred

Adding a new bin to the release

  1. Declare the [[bin]] in the appropriate crate's Cargo.toml.
  2. If the crate hosts the bin via [package.metadata.dist] dist = false, either remove that opt-out or move the bin to a new crate that doesn't carry it.
  3. Re-run make dist-check and confirm the new bin shows up under [bin] in the dist plan output.
  4. Update scripts/release-check.sh's per-archive content check if the new bin should be required.

Adding a new target

  1. Append the target triple to targets = […] in dist-workspace.toml.
  2. Append the matching tarball name to EXPECTED_TARBALLS in the smoke gate.
  3. Land the toolchain story in the GH Actions release workflow (Phase 27.2) — without that, the target builds locally only.