Releasing & publishing
Everything is lockstep-versioned (ADR 0023): one vMAJOR.MINOR.PATCH git tag drives every published artifact. Push the tag and CI does the rest. What each release contained is recorded in CHANGELOG.md.
What a vX.Y.Z tag publishes
| Artifact | Where | Workflow |
|---|---|---|
App image (claude-transcripts-app — webapi + webui SPA + docs + bundled CLI) | GHCR: ghcr.io/<owner>/claude-transcripts-app | publish-image.yml |
| Mirrored backing images (CouchDB, Garage, Meilisearch + admin UIs) | GHCR: ghcr.io/<owner>/claude-transcripts-* | mirror-images.yml (also on-demand) |
| CLI binaries (Linux + macOS, x64 + arm64) + SHA-256 sums | GitHub Release assets | release-cli.yml |
CLI on npm (@claude-transcripts/cli, a bun-runnable bundle) | npmjs.org | release-cli.yml — skipped with a warning until NPM_TOKEN is set (below) |
The CLI needs bun at runtime (it uses Bun APIs). So: the compiled binaries are the zero-dependency option (curl the one for your platform from the Release); the npm package is for bun users (bunx @claude-transcripts/cli, or a global install with bun on PATH).
App image tags
| Tag | Means | Pushed by |
|---|---|---|
vX.Y.Z | that exact release | a v*.*.* tag |
latest | the newest release | a v*.*.* tag |
main | the tip of main, rebuilt on every merge | a push to main |
<sha> | one specific commit | any of the above, and manual dispatch |
latest deliberately tracks releases, not main — install uses it, and pointing it at unreleased code would hand users something untested. But that leaves a gap the main tag fills: between releases there would otherwise be no image of the current code at all. After 0.0.1 the app image sat at schema v5 while main reached v7, so an install could only pair a current CLI with a months-old app — which breaks the lockstep-versioning invariant (ADR 0023) silently, since everything starts and only some later read misbehaves.
Accordingly install pins the app image to the CLI's own version when the CLI is a release, and to main when it isn't, then reports the version the running app actually announces so a mismatch is visible rather than inferred.
Building on every merge to main also means the image vulnerability scans (grype + trivy, failing on HIGH) now run per-merge instead of only at release — earlier warning, at the cost of a noisier signal when a base image picks up a CVE.
Cutting a release
bun run scripts/release.ts 0.1.0 # stamp the lockstep version into every manifest
git commit -am "chore(release): 0.1.0" && git push # via a PR, per branching.md
git tag v0.1.0 && git push origin v0.1.0 # from main, once merged
scripts/release.ts <semver> stamps the version into the root package.json, each packages/*/package.json, and the hook's .claude-plugin/plugin.json (ADR 0023); --check verifies they all match without writing. The tag is what CI reacts to — the stamped manifests just keep the tree honest about which release it is.
CI then builds + publishes all of the above. A manual release-cli / mirror-images dispatch (Actions tab → Run workflow) is available for testing — release-cli on dispatch uploads the binaries as workflow artifacts without publishing.
One-time setup ("click-admin")
CI uses the built-in GITHUB_TOKEN to push to GHCR — no secret needed for the images. The manual bits, once:
- Make the GHCR packages public (so anyone can
docker pullwithout auth): after the first publish, each package appears under your profile/org → open it → Package settings → Change visibility → Public. Do this forclaude-transcripts-appand each mirroredclaude-transcripts-*. (Repo → Settings → Actions → General → Workflow permissions should allow read/write.) - npm:
- Own the scope: create the
claude-transcriptsorg on npmjs (the package is@claude-transcripts/cli, published with--access public). - Create an npm Automation access token (npmjs → Access Tokens) and add it as the repo secret
NPM_TOKEN(Settings → Secrets and variables → Actions). - (Optional) a protected
releaseenvironment (Settings → Environments) if you want a manual approval gate before publishing.
After releasing: pull from your own registry
Point the stack at your mirrored images instead of external registries (so an unmaintained/unverified upstream can never ship you a surprise):
# in .env
IMAGE_NS=ghcr.io/<owner>
Then bun run stack:up (without --upstream) pulls couchdb, garage, meilisearch, the admin UIs, and the app image from ghcr.io/<owner>/… only. Re-run mirror-images (or tag a release) whenever you bump a pinned upstream tag in scripts/mirror-images.ts + .env.template.
Notes
- Backing image tags are pinned in
scripts/mirror-images.ts,.env.template, and the app model (packages/shared/src/model/services.ts) — keep them in lockstep. - The npm bundle inlines all dependencies (
bun build --target=bun), so the published package declares no runtime deps (the workflow drops theworkspace:protocol before publishing). The binaries embed the bun runtime, so they need nothing.