# Releasing Crewship Internal runbook for cutting a release. The pipeline is fully automated — this document explains *what* the steps do, *what* to check, and *how* to recover when something goes wrong. ## Versioning Crewship uses [Semantic Versioning](https://semver.org/). Pre-1.0 releases (`0.x.y`) treat **minor bumps as the breaking-change boundary**; patch releases are backwards-compatible fixes only. Pre-release suffixes follow semver: `-beta.1`, `-beta.2`, `-rc.1`. The goreleaser config has `prerelease: auto`, so a tag with a suffix lands as a GitHub pre-release; a clean `vX.Y.Z` tag becomes a stable release. ## Pre-flight checklist Before tagging, verify: - [ ] `main` is green: `go test ./... -count=1 && go vet ./...` and `pnpm lint && pnpm build && pnpm test --run`. - [ ] `gofmt -l internal/ cmd/` is empty for files touched in the release window. - [ ] Fragments are collected: on a release-prep branch run `python3 scripts/changelog-collect.py` (`--dry-run` first to preview). It moves every `changelog.d/-.md` to the top of `## [Unreleased]`, highest number first, and deletes the fragment files; it refuses, changing nothing, on an empty fragment or a missing heading. If it stops partway (crash, kill, full disk), just rerun it: it records what it collects in `CHANGELOG.md.collect-journal`, installs `CHANGELOG.md` durably before deleting any fragment, and the rerun finishes from that journal, so no note is lost or added twice. If you edited `CHANGELOG.md` in between, it refuses and says what to check. Commit the result with the version bump below. Fragments merged after that commit wait for the next release. - [ ] `CHANGELOG.md` has a populated `## [X.Y.Z]` section under `## [Unreleased]`. Move the entries down, leave `## [Unreleased]` empty; `changelog.d/` holds only its `README.md`. - [ ] `package.json` `version` matches the target tag (without the `v` prefix; `0.1.0-beta.1` for tag `v0.1.0-beta.1`). - [ ] No unresolved major review findings on the release PR; follow [Review before merge](CONTRIBUTING.md#review-before-merge). - [ ] Goreleaser dry-run passes: `goreleaser release --parallelism 2 --snapshot --clean --skip=publish,sign,sbom,homebrew`. - [ ] The `release` branch is at most a few commits behind `main` (`git log --oneline origin/release..origin/main`). The dogfood prod VM tracks `release` and a stale branch means you ship from old code. ## Execution-authority rollout (#3068) Before upgrading **each** stage or production instance to the Issue-operation authority contract, run the new release's read-only doctor against that exact instance's existing database. A report from a development or demo database is not evidence for another target. The production owner performs the production audit and rollout; development agents must not access production. ```bash DATABASE_URL='file:/absolute/path/to/target/crewship.db' crewship doctor execution-origins --json ``` Review the blocked and uncertain records with the operator before deployment; the report inventories possible impact, does not authorize work, and does not certify an upgrade. Unknown legacy queued work requires fresh authenticated admission. Retain the report privately with the target identity and release SHA. After deploying the matching backend and sidecar artifacts, recycle each affected crew's retained container to refresh its bind-mounted sidecar and credentials. Coordinate a safe idle window first: account for running and queued work and prevent new dispatch during that window. The command removes the runtime container; it is not a graceful drain and must not interrupt active work. Select the target server/workspace in the authenticated CLI, then run: ```bash crewship crew restart-agents ``` The next agent execution recreates the container. Verify an authorized Issue write before declaring the new authority guarantees active. After a restore, web-origin recurring templates pause at their next firing until reviewed and saved/re-enabled in a fresh full authenticated session; see [restore authorization](docs/guides/backup.mdx#restore). ## Shared dev release build profile `.env.production` is the single build profile and starts with `CREWSHIP_BUILD_PROFILE`. On main it is `main`: every experimental artifact surface is off and the Artifacts nav opens `/pages`. A shared dev release (a `demo/release-*` branch with a draft PR for CI) changes only this file: set `CREWSHIP_BUILD_PROFILE=release` and turn on the flags listed in `RELEASE_FLAGS` in `e2e/build-profile.ts`; the sharing mock stays off. The profile separates three things that must not be read as one: | Surface | Production default (`main`) | Shared dev release | Accepted | |---|---|---|---| | Artifacts library (which also moves old Pages addresses to `/artifacts/`), chat artifact context and projects, Craft lease, Design | off | on (`RELEASE_FLAGS`) | only on the exact release SHA whose `VALIDACE` records milestones A and B | | Dashboard (milestone C) | off | off (`UNACCEPTED_FLAGS`) | no | | Slides and Design System (milestone D) | off | off (`UNACCEPTED_FLAGS`) | no | A surface moves into `RELEASE_FLAGS` after its milestone is accepted, not to make it reachable for a demo; the unit test fails if a flag is listed in both, or if either profile turns on an unaccepted flag. This table covers the build-time `NEXT_PUBLIC_*` flags. Server-side flags (`crewship feature-flag`, environment `CREWSHIP_*`) live with each environment and are verified on the deployed slot. The feature-flag unit tests and the phone browser contract read the profile and check either build, so CI of the exact release SHA is green without skipping tests. Anything bound for main (a push to main or a merge-queue ref) must carry the `main` profile; `lib/__tests__/feature-flags.test.ts` fails otherwise. ## Cutting the tag ```bash # 1. Confirm you're on main and synced git checkout main git pull --ff-only origin main # 2. Tag (signed if you have GPG configured, plain otherwise) git tag -a v0.1.0-beta.1 -m "v0.1.0-beta.1: first public beta" git push origin v0.1.0-beta.1 ``` The push triggers `.github/workflows/release.yml`. The tagged commit must have successful CI, Security and CodeQL main-push runs and belong to main's history. The workflow builds a multi-platform image candidate, scans it, boots both platforms and signs its digest. It then builds the final binary assets once with GoReleaser publishing disabled, verifies checksums and signatures are present, scans the artifacts and boots the Linux archive. Only these verified assets are uploaded to a draft GitHub Release, which is published after upload completes. Image aliases and the atomic stable Homebrew update follow publication. Homebrew has a separate retryable job. Package signing credentials are required; a missing key stops publication. See [the CI/CD runbook](docs/runbooks/ci-cd-implementation-2026-09-11.md) for exact identity checks and rollout requirements. ## After the release lands - [ ] Install the binary from the release artifact and run `crewship version` — confirm the tag, commit SHA, and build date look right. - [ ] `brew tap crewship-ai/tap && brew install crewship` on a clean Mac and run `crewship version` — same check, plus the formula published cleanly. - [ ] Promote the GitHub Release notes (auto-generated from the changelog block + commit history) — re-edit if anything reads poorly. - [ ] Bump `## [Unreleased]` in `CHANGELOG.md` to the next planned version's heading, push the change. - [ ] Sync `release` branch from `main` so the dogfood prod VM rolls forward: `git push origin main:release`. - [ ] Announce: short post in the discussion forum + Discord/community channel; mention any breaking changes prominently. ## Recovery **Tag pushed too early / wrong commit.** Delete the remote tag, fix the issue, retag the right commit: ```bash git push --delete origin v0.1.0-beta.1 git tag -d v0.1.0-beta.1 # Fix things, then retag from the corrected SHA ``` If goreleaser already produced a GitHub Release for the bad tag, delete it via the UI before retagging — otherwise the next run errors on the existing draft. **Goreleaser fails mid-flight.** Re-run the workflow from the Actions tab. The build is reproducible (`-trimpath`, fixed `GOFLAGS`), so a re-run from the same commit produces identical bytes; cosign keyless signing is the only step that's not perfectly idempotent (it generates a fresh certificate against the same identity), but that's fine. **Homebrew formula push fails (403 / permissions).** Check that the `HOMEBREW_TAP_TOKEN` repo secret has not expired. Goreleaser writes the formula to a separate repo, so the main release is unaffected — re-run only the Homebrew step manually if needed. **Binary doesn't run on a user's machine.** Common causes: - *macOS Gatekeeper* — binaries are signed but not notarized in v0.1; users must `xattr -d com.apple.quarantine ./crewship` after download. Documented in the Quick Start. - *Linux missing glibc* — Crewship is built `CGO_ENABLED=0` so a static binary works on any modern distro. If the user is on Alpine + musl, the static build still runs. ## Hotfix workflow When a P0 / data-loss bug lands in a released version, ship a patch without waiting for the next planned release. ```bash # 1. Branch from the existing release tag (NOT main — main may have # incompatible changes that you don't want in the hotfix). git checkout -b hotfix/v0.1.1 v0.1.0 # 2. Cherry-pick the fix from main, or write the minimal fix directly # on this branch. Keep scope tight — a hotfix is "patch the bug", # not "while we're here, also...". git cherry-pick # 3. Tag and push. The release workflow fires off this tag exactly # like a normal release. git tag -a v0.1.1 -m "v0.1.1: hotfix for " git push origin hotfix/v0.1.1 v0.1.1 ``` Criteria for a hotfix (otherwise wait for the next regular release): - Data loss, data corruption, or silent data divergence. - Auth/authorisation bypass. - Security vulnerability (CVE-grade or equivalent). - Server fails to start on a version's own DB after a clean install. After shipping, forward-port the fix to `main` if it wasn't cherry-picked from there to begin with — otherwise the next regular release ships the bug again. **If the hotfix itself breaks something:** retag `v0.1.2` with a fix-of-the-hotfix. Don't try to "untag" v0.1.1 — once a tag is in the public release feed, customers have already pulled it. The fix- forward path is always cheaper than the rollback path. ## Distribution channels Three concurrent channels serve different audiences: | Channel | Trigger | Docker tag | Binary | Use case | |---|---|---|---|---| | **stable** | clean semver tag (`v0.1.0`) | `:vX.Y.Z`, `:vX.Y`, `:latest` | GitHub Release, Homebrew | Production / default `brew install crewship` | | **beta** | pre-release tag (`v0.1.0-beta.1`) | `:vX.Y.Z-beta.N` | GitHub Pre-release | Opt-in beta testers (`brew install crewship@0.1.0-beta.1` or `docker pull :vX.Y.Z-beta.N`) | | **nightly** | successful verified main CI + daily schedule | `:nightly`, `:main-` | Immutable `nightly-YYYYMMDD-rN` GH pre-release | Internal CI, brave testers wanting trunk | The `:latest` Docker tag only moves on clean semver tags — pre-releases must never overwrite `:latest`, or `docker pull crewship` would silently hand beta to users expecting stable. See `.github/workflows/release.yml` for the gating logic. ## Migration safety Migrations are the highest-risk part of every release — they touch production data and a bad one is hard to undo. Guardrails: 1. **`migration-lint` CI workflow** runs on every PR touching `internal/database/migrate.go`. Enforces append-only ordering (versions strictly increase, no rename of a version already in `main`). The Go test counterpart (`migrate_lint_test.go`) catches the same class of mistake locally. 2. **Auto-snapshot before apply** — `database.SnapshotBeforeMigrate` takes a `VACUUM INTO` copy of the live DB as `.pre-migrate-vN-to-vM-.bak` whenever any migration is pending. Last 10 snapshots are retained per database; opt out with `CREWSHIP_SKIP_MIGRATION_BACKUP=1`. 3. **Forward-only schema changes**. Never `DROP COLUMN` in the same release that stops reading it: ship "stop reading" in vX, then `DROP COLUMN` in vX+1. The previous-release client must remain compatible with the next-release schema for at least one minor bump. 4. **Restore-from-backup tests** in `internal/backup/` exercise the `restoreBackfill` hook chain — when a customer restores an older bundle into a newer schema, every migration between source and target gets a chance to populate any new columns. ## Telemetry (crash reporting) — Sentry setup Crewship ships with Sentry-backed crash reporting on both sides of the stack: - **Backend (Go):** wired through `internal/crashreport`, baked into the binary at build time via `-ldflags="-X ...DSN=$SENTRY_DSN"`. - **Frontend (Next.js):** wired through `sentry.client.config.ts` (+ `.server.ts` / `.edge.ts`), baked into the static export via the `NEXT_PUBLIC_SENTRY_DSN` env var that `pnpm build` consumes. Both streams route to the **same Sentry project** (`your-sentry-org/crewship-backend`) with a shared DSN secret. The Go events tag `ServerName` with the anonymous install ID; the JS events read that same install ID from `GET /api/v1/system/telemetry` so a backend error and a browser error from the same install group together in the Sentry UI. The frontend gates `Sentry.init` on the same `app_settings.telemetry_opt_in` row the Go side reads — when the operator runs `crewship telemetry off`, the next browser page load fetches `enabled: false` and skips init entirely. No client-side persistence, no separate browser-side consent prompt; the CLI is the single control surface. Any fetch failure on the consent endpoint defaults to NOT initialising (privacy bias). The runtime default is decided by build flavour (`crashreport.DefaultOptIn`, keyed on the version stamped at build time): - **Pre-release (-beta/-rc) and dev builds: default ENABLED.** There is no first-run prompt on `crewship start` — a deterministic default-on means a non-interactive deployment (Docker, systemd, CI) doesn't end up silently telemetry-less while a release is still baking. On first start, if no consent row exists in `app_settings`, `crashreport.Init` writes `"1"` and brings the Sentry client up. - **Stable release versions: default DISABLED, strictly opt-in.** Init writes nothing; telemetry stays off until the operator consents via the onboarding step (web wizard checkbox / `crewship setup` prompt / `--telemetry` flag) or `crewship telemetry on`. - **Explicit choices are sticky in both directions.** Once a consent row exists in `app_settings.telemetry_opt_in` ("1" or "0"), Init treats it as the operator's decision and no version default ever flips it, including across upgrades. - **Status visible any time** via `crewship telemetry status` — shows enabled/disabled, install ID, and the resolved DSN endpoint host. ### Routing override Operators who want to route crash data to their own Sentry (or a self-hosted instance) set the `CREWSHIP_SENTRY_DSN` env var: ```bash CREWSHIP_SENTRY_DSN=https://@.ingest.sentry.io/ crewship start ``` `crashreport.ResolveDSN()` prefers the env value over the ldflag-baked default. `crewship telemetry status` shows the resolved endpoint host + which source the DSN came from. Empty/unset env = vendor default. ### One-time project setup 1. Create a Sentry project (Platform: Go, or "Multi-stack" — the same project ingests both the Go SDK and the @sentry/nextjs browser SDK). Note its DSN. 2. Add `SENTRY_DSN` to the repo's GitHub Actions secrets. The `release.yml` and `nightly.yml` workflows pass it BOTH as a build-arg to goreleaser / the Docker image build (Go side) AND as the `NEXT_PUBLIC_SENTRY_DSN` env in `pnpm build` (frontend side). Local `go build` and PRs from forks leave the DSN empty, so they ship telemetry-disabled — same defense-in-depth applies to the JS bundle. 3. **Configure server-side data-scrubbing rules in the Sentry UI.** The client-side BeforeSend hook in `sentry_adapter.go` scrubs request headers, query strings, request bodies, the User field, and several context maps. It cannot reliably scrub free-form strings that *we* generate, e.g. `fmt.Errorf("auth failed for %s", userEmail)`. The only sound defense for that class of leak is regex-based scrubbing at the Sentry server. Project Settings → Security & Privacy → Data Scrubbing → add: - `@email-pattern` — emails in messages, breadcrumbs, exception values - `@password-pattern` — Sentry built-in - `@creditcard-pattern` — Sentry built-in - Custom rule: `[Mask] [Message] [^Bearer\s+\S+]` — bearer tokens - Custom rule: `[Mask] [Message] [^sk-[A-Za-z0-9]{20,}]` — OpenAI/Anthropic-style keys These rules run inside Sentry before the event is persisted; if the regex matches, the matched substring is replaced with `[Filtered]`. Verify by raising a test error containing the pattern and checking the resulting event in the UI. ### What gets sent Stack traces, exception messages (subject to server-side scrubbing above), Crewship version + commit, OS name, an anonymous install ID (random 32-hex, generated on first opt-in, stable across opt-out/opt-in cycles). ### What is never sent Workspace data, credential values, request bodies, Authorization or Cookie headers, query-string secrets, environment variables, the user's hostname (`ServerName` is overridden with the anonymous install ID), the Go module list, or any of the runtime/device/culture contexts that sentry-go's default integrations would normally attach. See `internal/crashreport/sentry_adapter.go::scrubEvent` for the backend filter and `crashreport_test.go::TestScrubEvent_DropsLeakyContexts` for the pinning test. The **frontend mirrors the same scrubbing posture** in `sentry.client.config.ts::scrubEvent`: device/runtime/culture contexts dropped, User cleared, Breadcrumbs.data cleared, Modules cleared. The pinning test lives in `lib/__tests__/sentry-scrub.test.ts` and runs in the standard `pnpm test` flow — a future @sentry/nextjs upgrade that silently un-scrubs one of these fields fails CI rather than leaking quietly into production events. ## Branch protection Changes to `main` go through a pull request and the merge queue. Follow [Review before merge](CONTRIBUTING.md#review-before-merge), [Review scaled by risk](CONTRIBUTING.md#review-scaled-by-risk) and [Merge through the queue](CONTRIBUTING.md#merge-through-the-queue) for the required aggregate checks, review evidence and exact-head submission procedure. A formal approval count or a green PR run does not replace that procedure or the queue's checks. See the [CI/CD runbook](docs/runbooks/ci-cd-implementation-2026-09-11.md) for exact-SHA release verification and the authorized repository-policy workflow. When auditing live protection, inspect the branch's [effective rules](https://docs.github.com/en/rest/repos/rules#get-rules-for-a-branch). A missing classic branch-protection API response alone does not mean `main` is unprotected: rulesets also enforce branch policy. ## Cadence Target cadence in pre-1.0: - **Beta minor (`v0.x.0-beta.1`)** — every 4–6 weeks while the API surface is still moving. - **Beta patch (`v0.x.y-beta.N`)** — within 1 week of finding a P0/P1. - **RC (`v0.x.0-rc.1`)** — once no open P0/P1 for 7 days. - **Stable (`v0.x.0`)** — promote RC after 7-day soak. ## Stability tiers (per-feature, in addition to the version) Inside a release, every documentation page carries one of five labels. The label is stored as `stability` frontmatter and rendered beside the page title through Mintlify's `tag` metadata: - **stable** — frozen surface; only backwards-compatible changes in patches. - **early** — available and supported, but the contract may shift in minor releases. - **experimental** — research preview; do not build production workflows on it. - **deprecated** — still available for compatibility, with a documented replacement and removal path. - **roadmap** — planned or documented for orientation, but not part of the shipped product contract. `early` replaces the former per-feature label `beta`; `beta` remains a valid SemVer release-channel name, but is not a feature-stability label. The Crew Journal, Paymaster, Lookout, Harbormaster, and Backup ship as **stable** in v0.1.0-beta.1. Episodic memory and Consolidate ship as **experimental** (packages exist, auto-wiring lands in v0.2). The full matrix and the policy for pages that cover more than one feature are in [docs/production-checklist.mdx](docs/production-checklist.mdx#stability-matrix).