Release CLI

Tag-driven CLI release: git tag v* → GitHub Actions runs GoReleaser → cross-compiled binaries upload to Vercel Blob → install script (/cli/install) auto-detects OS/arch.

Default stance

Use the script. Don't tag manually. ./scripts/release-cli.sh v0.x.y validates branch state, runs goreleaser check, optionally does a snapshot dry-run, creates and pushes the tag, monitors the workflow, and verifies the binaries are downloadable. Manual git tag && git push skips all of those checks and is how broken releases reach cli/latest/.

Releases happen from main, after CLI changes are merged through normal PR flow. Don't release from a feature branch. Don't release from staging. The pipeline trusts that main is releasable.

Use this skill when

Releasing the CLI, tagging a v* version, debugging a failed release, or verifying that a release succeeded end-to-end (binaries on Blob, install script working).

Quick release

./scripts/release-cli.sh v0.2.0

This:

  1. Validates you're on main and clean.
  2. Runs goreleaser check to validate .goreleaser.yml.
  3. Optional --snapshot dry-run build (locally, no upload).
  4. Creates and pushes the git tag.
  5. Monitors the GitHub Actions release workflow.
  6. Verifies binaries are downloadable from Blob.

Manual sequence (when the script doesn't fit)

  1. Ensure all CLI changes are merged to main.
  2. goreleaser check — validate config.
  3. Optional dry run: goreleaser build --snapshot --clean.
  4. git tag v0.2.0.
  5. git push origin v0.2.0.
  6. gh run watch — workflow triggers on v* tags.
  7. curl -sSf https://seed.unionstreet.ai/cli/install | sh — verify install works.

What the pipeline does

git tag v0.2.0 → push → GitHub Actions (Release CLI workflow):
  1. GoReleaser cross-compiles 5 targets (linux amd64/arm64, darwin amd64/arm64, windows amd64)
  2. Archives: tar.gz (linux/mac) + zip (windows)
  3. @vercel/blob SDK uploads to:
     - cli/v0.2.0/<arch>.tar.gz   (versioned — immutable)
     - cli/latest/<arch>.tar.gz   (latest — overwritten each release)

Build targets

OSArchArchive
linuxamd64tar.gz
linuxarm64tar.gz
darwinamd64tar.gz
darwinarm64tar.gz
windowsamd64zip

Blob storage layout

Binaries live at BLOB_BASE_URL/cli/.... The app serves them via:

RoutePurpose
/cliBrowser install page
/cli/installcurl | sh script — auto-detects OS/arch
/cli/download/[filename]302 redirect to Blob

Hard rules

  • Never tag from staging or a feature branch. Main only.
  • Never reuse a version. Tags are immutable; trying to overwrite a v* tag breaks cli/v*/ consumers and the install script's version detection.
  • Never bypass goreleaser check. A misconfigured .goreleaser.yml will produce broken or partial releases that look like they succeeded.
  • Don't skip the post-release verification step. A green Actions run doesn't prove the install script works — pull the binary and run it.

Version conventions

  • v0.x.y — pre-1.0, breaking changes allowed between minor versions.
  • Semantic versioning: vMAJOR.MINOR.PATCH.
  • Tags must match v* pattern to trigger the workflow.

Forking for a new product

When a fork ships its own CLI:

  1. Update project_name in .goreleaser.yml.
  2. Update cliName in lib/brand.ts.
  3. Verify BLOB_READ_WRITE_TOKEN is available (inherited from org secret, or set per-repo).
  4. First release: git tag v0.1.0 && git push origin v0.1.0 (or use the script).

Where things live

FilePurpose
.goreleaser.ymlGoReleaser config (build targets, archive format, Blob upload)
.github/workflows/release-cli.ymlGitHub Actions workflow — triggers on v*
scripts/release-cli.shOne-shot release entrypoint
lib/brand.tscliName (used in install script and binary naming)
app/cli/Public install page + install script + download redirect
cli/CLI source — see cli-development

Auxiliary content