# Release Sidecar

> Release new versions of sidecar. Covers version tagging with semver, td dependency updates, go.mod validation, CHANGELOG updates, GoReleaser automation, Homebrew tap updates, and verification steps. Use when preparing or executing a release.

- Skill: `marcus/release-sidecar` (Agent Skill)
- Install (CLI): `npx skillmds@latest add marcus/release-sidecar`
- Raw SKILL.md: https://api.skillmd.com/api/skills/marcus/release-sidecar/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: marcus (https://skillmd.com/u/marcus)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/marcus/release-sidecar

---


# Releasing a New Version

Operator contract: **`docs/guides/active/releasing.md`**. Enforcement lives in `scripts/` and
`BUMP=minor make release` (or `RELEASE_VERSION=vX.Y.Z make release` for an explicit
version). Prefer the one-shot command over replaying this checklist by hand.

## Prerequisites

- Go matching `go.mod`
- Clean working tree; `main` identical to live `origin/main`
- Tests and **Go CI** green on the commit you will tag (tests *and* lint) —
  `check-release-state.sh` now checks this itself via `gh run list
  --workflow=go-ci.yml` and fails closed if it's red/running/missing, so you
  don't have to remember to look
- GitHub CLI authenticated with push access to `marcus/homebrew-tap`
- No `replace` directives in `go.mod`
- `HOMEBREW_TAP_TOKEN` secret present in the GitHub repo (CI tap job)

**Beware of go.work**: always use `GOWORK=off` when updating dependencies and
when validating install paths.

Local lint must match CI's golangci-lint **v2.12.2**, or trust CI:

```bash
gh run list --workflow=go-ci.yml --limit=1
```

## Prepare (sidecar-specific)

### 1. Version

```bash
git tag -l 'v*' | sort -V | tail -1
```

SemVer: major / minor / patch as usual.

### 2. Sibling dependencies (td, tasks)

```bash
make sync-deps   # pins every github.com/marcus/* requirement to its latest tag
```

`check-release-state.sh` enforces this and refuses to tag when one is behind.
`go.work` resolves those imports to the local checkouts, so drift is invisible
locally — both the gate and `sync-deps` use `GOWORK=off`. If a sibling jumped
several minors, decide deliberately (pin for a focused release vs take latest
and note it under Dependencies) and smoke its tab in the app.

### 3. CHANGELOG

```markdown
## [Unreleased]

### Features
- …

### Bug Fixes
- …

### Dependencies
- …
```

Commit the changelog (and any dependency bump) on `main`, then push so
`HEAD == origin/main`. Leave the heading as `## [Unreleased]` — `make release`
stamps it to `## [vX.Y.Z] - YYYY-MM-DD` for you.

## Publish

```bash
# Dry-run (optional but recommended for tooling changes)
make release-snapshot
./scripts/verify-release-archives.sh dist
./scripts/test-release-guards.sh dist
./scripts/test-release-publication.sh
make release-dry-run BUMP=minor   # prints the derived version + plan, no mutation

# Cut the release: derive the version, stamp the changelog, commit, push,
# then the fail-closed preflight → tag → CI → formula verify/publish
BUMP=minor make release
```

The version is stated exactly once — via `BUMP` derived from the latest tag, or
by setting `RELEASE_VERSION=vX.Y.Z` yourself (also works if you stamped the
CHANGELOG heading by hand). `scripts/release.sh` refuses an empty `[Unreleased]`
section, a tree dirty beyond `CHANGELOG.md`, a tag that already exists, and a
`RELEASE_VERSION` that contradicts an already-stamped heading, before handing off
to `scripts/publish-release.sh`. What it enforces and does end to end is
documented in `docs/guides/active/releasing.md`.

Resume only the tap step if the tag/release already exists:

```bash
RELEASE_VERSION=vX.Y.Z make release-tap
```

### CI jobs (on tag push)

1. **`verify`** — tag points at live `main`, tests, snapshot archives, release guards
2. **`release`** — GoReleaser publishes GitHub release + binaries
3. **`update-homebrew-tap`** — renders `packaging/homebrew/sidecar.rb.tmpl` and
   pushes `Formula/sidecar.rb` with downgrade/idempotency/race guards

td/nightshift formulas are **not** auto-bumped; edit them by hand when co-releasing.

## Verify

```bash
gh run list --workflow=release.yml --limit=1
gh release view vX.Y.Z --json assets -q '.assets[].name'

GOBIN=$(mktemp -d) GOWORK=off go install github.com/marcus/sidecar/cmd/sidecar@vX.Y.Z
"$GOBIN/sidecar" --version
```

`go install @vX.Y.Z` can 500 from the checksum DB for a minute or two after the
tag — wait and retry. Prefer a throwaway `GOBIN` so verification does not
clobber a dev machine's `sidecar`.

## Dev machine after release

```bash
# Return to the canonical main development build:
make install-local

# Or keep the released Homebrew build active:
make use-homebrew

# In either case, prove the managed link and both login-shell modes:
make install-status
```

## Recovery

Prefer a new patch release. Keep tags. Resume tap with `make release-tap`.
See `docs/guides/active/releasing.md`.

## Checklist

- [ ] Go CI green (tests + lint) on the commit to tag — enforced automatically by `check-release-state.sh`
- [ ] Working tree clean; `main` == `origin/main`
- [ ] td bump considered; td tab smoke if td moved
- [ ] No `replace` in go.mod; `GOWORK=off` build works
- [ ] Agent integration assets: if any bundled asset's bytes changed, its version constant, `internal/agentlifecycle/capabilities.json`, and the golden in `internal/agentintegration/asset_golden_test.go` all moved with it (the golden test fails if they did not)
- [ ] CHANGELOG bullets under `## [Unreleased]`
- [ ] `BUMP=minor make release` (or `RELEASE_VERSION=vX.Y.Z make release`) succeeded
- [ ] Release assets present; formula URL/sha match (automatic)
- [ ] `go install` verified into throwaway `GOBIN`
- [ ] `make install-status` proves the dev machine is on the intended binary

