release flow: tagged releases with a prebuilt dist asset #96

Closed
opened 2026-07-18 17:22:23 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-18 17:22:23 +00:00 (Migrated from github.com)

The problem

cast has a version (package.json says 0.1.0) but no tags, no GitHub releases, and no CHANGELOG.md. install.sh downloads archive/refs/heads/main and builds on the operator's machinenpm ci, tsc, then prune — so every install pulls devDependencies and compiles, and "what cast is this?" means "whatever main was when you last curl'd". For the tool that holds the keys to the fleet's secrets and reconciles production Coolify, installs should be reproducible and named.

This is the cast half of the flow designed in heavy-duty/box#83 — same release shape, plus the piece unique to cast: a prebuilt dist asset, because cast is the one repo where the source tarball is not the package.

The flow

1. Grow the release surface.

  • Add CHANGELOG.md with an ## Unreleased section; feature PRs land their entry as part of the PR.
  • cast --version reads package.json — the version's single source of truth (no separate VERSION file; the ecosystem already has one).

2. A release is a PR, then a tag.

  • A small PR — release: X.Y.Z — bumps package.json and stamps the Unreleased section with version + date. CI green on it.
  • Merge, tag the merge commit bare X.Y.Z (matching box's and rig's tag scheme), push the tag.

3. release.yml, on tag push — this is where cast differs:

  • Assert tag == package.json version — fail loudly on mismatch, create nothing.
  • Build the package once, in CI: npm ci && npm run build && npm prune --omit=dev, then tar the runnable tree (bin/, dist/, production node_modules/, package.json) into cast-X.Y.Z.tgz.
  • gh release create with the version's CHANGELOG.md section as the body and the tarball attached as the asset.

4. The installer defaults to the latest release asset, not a source build.

  • CAST_REF unset → resolve the latest tag via the releases/latest redirect (Location header — no API, no token), download cast-$TAG.tgz, extract into $DEST. No npm ci, no tsc, no devDependencies on the operator's machine — the build happened once, in CI. Node >= 22.12 and age remain runtime prerequisites exactly as today.
  • CAST_REF set (a branch or tag with no asset) → fall back to today's build-from-source path. That keeps CAST_REF=main as the dev channel.
  • README documents the channels: default = latest release, CAST_REF=X.Y.Z = pinned, CAST_REF=main = dev build-from-source.

Order

  1. CHANGELOG.md + cast --version.
  2. release.yml with the dist-asset build + asset-aware installer.
  3. Cut 0.1.0 — the first cast an operator can install without compiling it.
## The problem cast has a version (`package.json` says `0.1.0`) but no tags, no GitHub releases, and no `CHANGELOG.md`. `install.sh` downloads `archive/refs/heads/main` and **builds on the operator's machine** — `npm ci`, `tsc`, then prune — so every install pulls devDependencies and compiles, and "what cast is this?" means "whatever main was when you last curl'd". For the tool that holds the keys to the fleet's secrets and reconciles production Coolify, installs should be reproducible and named. This is the cast half of the flow designed in heavy-duty/box#83 — same release shape, plus the piece unique to cast: a **prebuilt dist asset**, because cast is the one repo where the source tarball is *not* the package. ## The flow **1. Grow the release surface.** - Add `CHANGELOG.md` with an `## Unreleased` section; feature PRs land their entry as part of the PR. - `cast --version` reads `package.json` — the version's single source of truth (no separate `VERSION` file; the ecosystem already has one). **2. A release is a PR, then a tag.** - A small PR — `release: X.Y.Z` — bumps `package.json` and stamps the Unreleased section with version + date. CI green on it. - Merge, tag the merge commit bare `X.Y.Z` (matching box's and rig's tag scheme), push the tag. **3. `release.yml`, on tag push — this is where cast differs:** - Assert tag == `package.json` version — fail loudly on mismatch, create nothing. - Build the package once, in CI: `npm ci && npm run build && npm prune --omit=dev`, then tar the runnable tree (`bin/`, `dist/`, production `node_modules/`, `package.json`) into `cast-X.Y.Z.tgz`. - `gh release create` with the version's `CHANGELOG.md` section as the body and the tarball attached as the asset. **4. The installer defaults to the latest release asset, not a source build.** - `CAST_REF` unset → resolve the latest tag via the `releases/latest` redirect (`Location` header — no API, no token), download `cast-$TAG.tgz`, extract into `$DEST`. **No `npm ci`, no `tsc`, no devDependencies on the operator's machine** — the build happened once, in CI. Node >= 22.12 and `age` remain runtime prerequisites exactly as today. - `CAST_REF` set (a branch or tag with no asset) → fall back to today's build-from-source path. That keeps `CAST_REF=main` as the dev channel. - README documents the channels: default = latest release, `CAST_REF=X.Y.Z` = pinned, `CAST_REF=main` = dev build-from-source. ## Order 1. `CHANGELOG.md` + `cast --version`. 2. `release.yml` with the dist-asset build + asset-aware installer. 3. Cut **0.1.0** — the first cast an operator can install without compiling it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#96
No description provided.