# Releases

`arbe` ships as standalone binaries built with `bun build --compile` for `darwin-arm64`, `linux-x64` (Linux ARM and Intel Mac deferred — `scripts/release-build.ts` `TARGETS` is canonical). This page is the durable contract — installer scripts, Quickstart, and release tooling all reference it. Change here first, then update consumers.

Versioning is the semver in `apps/cli/package.json`; commit SHA + UTC build date are baked in via `--define process.env.ARBE_BUILD_COMMIT` / `process.env.ARBE_BUILD_DATE`. `arbe --version` prints `arbe <version> (<short-commit>, <yyyy-mm-dd>)` (or `arbe dev` in dev). `arbe upgrade` compares only the version string against `latest.json` — commit SHA and date don't affect the upgrade decision.

```
dist/release/latest/
  arbe-<target>.tar.gz       # single `arbe` at archive root
  SHA256SUMS                 # one per line, sorted
  release-latest.json        # → arbe/latest.json (mutable, 60s cache, must-revalidate)
  release-<version>.json     # → arbe/<version>.json (immutable, 1y)
```

R2 holds two pointers to the same bytes: `arbe/latest/` (mutable — newest published build) and `arbe/<version>/` (pinned, never overwritten in practice). Manifests at `arbe/latest.json` / `arbe/<version>.json` follow `LatestReleaseManifest` (`apps/cli/src/update/shared.ts`) — `version`, `commit`, `published_at`, `artifacts.<target>.{url,sha256,size}` all required, plus an optional `notes` string carrying that version's changelog section as markdown (manifests published before it existed simply omit it — `arbe upgrade` prints it when present and says nothing when not). Always fetch the tarball and `SHA256SUMS` from the same prefix together — `latest/` is mutable so they can mismatch mid-publish; `latest.json` is the only stable metadata pointer; the commit SHA baked into the binary is authoritative for "what build do I have."

Installer at `apps/www/static/install.sh` (one-liner: `curl -fsSL https://arbe.0sk.ar/install.sh | sh`) reads the manifest, picks target by `uname`, downloads + verifies sha256, drops `arbe` in `$ARBE_INSTALL_DIR` (default `~/.local/bin`). POSIX `sh`, no jq/python — only `curl`/`wget`, `tar`, `sha256sum`/`shasum`. Knobs: `ARBE_VERSION=latest|<x.y.z>`, `ARBE_TARGET=<target>` (cross-arch testing), `ARBE_INSTALL_DIR=<path>`. Drift is bounded by the manifest schema.

Cut a release: `bun run release [patch|minor|major|x.y.z]` (`release:bump` is kept as an alias) closes the changelog's `Unreleased` section into `v<x.y.z> — YYYY-MM-DD`, bumps `apps/cli/package.json`, jj-commits only those two files, advances the `release` bookmark, and pushes (`--dry-run` writes both files but stops, printing the manual jj steps). It refuses when `Unreleased` has no bullets (`--allow-empty` overrides), and runs no checks or tests of its own — run `bun run check` and `bun run test` first. The [`cli-release`](../../.github/workflows/cli-release.yml) GitHub Action runs `release:build` + `release:upload` against R2 on pushes to the `release` branch. `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN` repo secrets do the auth — wrangler picks them up automatically; one-time setup in [system/deployment](deployment.md).

The release pushes `main` and `release` together — after a release both point at the stamped commit.

Verify after (like deploys, releases get a recipe): watch the `cli-release` run to green (`gh run list --workflow cli-release`), then check `arbe/latest.json` and the pinned `arbe/<x.y.z>.json` both report the new version and the release commit, `notes` carries the changelog section, and a downloaded tarball's sha256 matches its manifest entry. The extracted binary must print the new version and commit: `arbe <x.y.z> (<sha>, <date>)`.

Break-glass: `bun run release:build` then `bun run release:upload` mirror manually; local invocation needs `bunx wrangler whoami` authed against the right Cloudflare account. Always overwrites. `Cache-Control` is set per-key on upload — `latest/*` and `latest.json` get `max-age=60, must-revalidate`; `<version>/*` and `<version>.json` get `max-age=31536000, immutable`. Releases land on the edge within ~60s.

## Changelog

How entries are written — the rules, voice, and ordering — lives in the changelog skill ([packages/skills/changelog/SKILL.md](../../../packages/skills/changelog/SKILL.md)). Mechanically: `bun run release` moves the whole `Unreleased` body under a new `## v<x.y.z> — YYYY-MM-DD` heading and leaves a fresh empty `Unreleased` above it, in the same commit as the version bump. `scripts/release-build.ts` reads that section back out and stamps it into both manifests as `notes`, so `arbe upgrade` prints what's new right after installing.

Code: `scripts/release-build.ts`, `scripts/release-upload.ts`, `scripts/release-bump.ts`, `scripts/lib/changelog.ts`, `apps/cli/src/update/shared.ts` (`LatestReleaseManifest`), `.github/workflows/cli-release.yml`.<br>
See [cli](../../cli.md#install), [system/deployment](deployment.md).
