# Releasing The published artifact is the Docker image `lgtmsupplyco/commerce`. A release is driven by a **push to `main`** that bumps the `VERSION` file; tangled Spindles then builds + publishes the image. The changelog is generated from conventional commits by [git-cliff](https://git-cliff.org). Release-PR automation is active — see below. > **Why not tags?** This Spindle does **not** fire on tag pushes (the tangled docs > describe tag triggers the deployed instance doesn't implement). So publishing is keyed > off `VERSION` on a push to `main`. The git tag `vX.Y.Z` is still created as the > git-cliff changelog boundary, but it does not trigger CI. ## One-time setup - Docker Hub org `lgtmsupplyco` + an access token. - tangled repo-settings secrets: `REGISTRY_USER` = `lgtmsupplyco`, `REGISTRY_TOKEN` = the token. - `RAILWAY_TOKEN` secret (project token scoped to `production`) — see "Production deploy". - Railway `backend` service source pinned to `lgtmsupplyco/commerce:latest` — see below. - `KNOT_DEPLOY_KEY` secret (+ optional `TG_APP_PASSWORD`) — see "Release-PR automation". - Local: `brew install git-cliff`. ## Cut a release Two ways — both end the same: a `VERSION` bump on `main` triggers the build. **A. Merge the release PR (automated).** If Release-PR automation is set up (below), a `chore/release → main` PR is kept up to date with the next version's `VERSION` + `CHANGELOG` bump. Review it and **merge** — that's the release. No local steps. **B. Manual, from a clean `main`:** ```bash ./scripts/release.sh 0.3.0 # regenerates CHANGELOG, bumps VERSION, commits, tags v0.3.0 git push origin main v0.3.0 # the exact command release.sh prints ``` Pushing `main` triggers `.tangled/workflows/publish-image.yml`, which (when `v` isn't already on Docker Hub) builds and pushes: - `lgtmsupplyco/commerce:v0.3.0` - `lgtmsupplyco/commerce:latest` The image tag **and** the version baked into `GET /healthz` both come from `VERSION`. Verify: `docker pull lgtmsupplyco/commerce:v0.3.0` and `curl /healthz` (should report `version: 0.3.0` + the commit). ## Production deploy (Railway) After the push succeeds, the same workflow triggers a Railway redeploy of the `backend` service from its source (`railway redeploy --from-source`). This is deliberate: Railway's own auto-deploy fires the instant the image tag ref changes, which beats the ~10min build — so the first deploy(s) fail on an image pull and Railway falls back to the last-good image. Triggering from the workflow, *after* the push, is race-free. Prerequisites (one-time): - **`RAILWAY_TOKEN`** in tangled repo secrets — a Railway *project token* scoped to the `production` environment (Railway → project → Settings → Tokens). The workflow skips the deploy (with a log line) if it's unset, so forks still publish. - **Pin the `backend` service source to `lgtmsupplyco/commerce:latest`** (Railway → backend → Settings → Source). `--from-source` pulls whatever the source points at; with `:latest` every release is picked up without editing the pin. `:latest` tracks the released version digest (the Docker Hub immutable-`latest` issue is fixed). Rollback: redeploy an older digest, or temporarily re-pin the source to a specific `:vX.Y.Z` tag in the Railway dashboard. ## Release-PR automation (optional) `.tangled/workflows/release-pr.yml` runs on every push to `main`: it computes the next version (`git-cliff --bumped-version`) and force-pushes a `chore/release` branch bumping `VERSION` + `CHANGELOG`. A branch-based PR (`chore/release → main`) tracks that branch, so each push is a new round. **Merging the PR is the release** — the `VERSION` bump lands on `main` and `publish-image.yml` builds, deploys, and tags it (`scripts/ci/tag-release.sh`). Logic lives in checked-in scripts (`scripts/ci/*.sh`, sharing `scripts/prepare-release.sh` with the manual path) so it stays lintable and consistent. One-time setup: 1. **Bot SSH deploy key.** Generate a keypair, register the *public* half on the `lgtm.shop` account (which owns the repo), and store the *private* half as the `KNOT_DEPLOY_KEY` repo secret: ```bash ssh-keygen -t ed25519 -N "" -C "lgtm.shop release bot" -f ./lgtm-knot-deploy tg auth login lgtm.shop # or: tg --account lgtm.shop ... tg ssh-key add ./lgtm-knot-deploy.pub --account lgtm.shop --title "spindle release bot" # then paste ./lgtm-knot-deploy (the PRIVATE key) into tangled repo Settings → Secrets # as KNOT_DEPLOY_KEY, and delete the local copies. ``` 2. **Bot app-password (optional, for hands-off PRs).** Set `TG_APP_PASSWORD` (an app password for the `lgtm.shop` account) as a repo secret. With it, the workflow opens the `chore/release → main` PR if none is open, and otherwise submits a new **round** and refreshes the body each run — title `chore: release`, body `v`. `tg` comes from the flake (`nix run github:alyraffauf/tg`). Without it, tangled doesn't auto-round on push, so open/resubmit the PR by hand (it logs a reminder). Merging the release PR is the release. Because tangled applies the latest **round**, click **Resubmit** before **Merge** if you're not using `TG_APP_PASSWORD`. Until `KNOT_DEPLOY_KEY` is set the workflow logs a skip and does nothing — releases behave exactly like the manual path above. The tag step in `publish-image.yml` is likewise a no-op without the key (so you'd keep tagging via `release.sh`). ## Commit messages drive the changelog Use [conventional commits](https://www.conventionalcommits.org): `feat:`, `fix:`, `docs:`, `perf:`, `refactor:`, `test:`, `build:`/`ci:`. Scopes render as `*(scope)*`. Anything else (and `release:` commits) is omitted from `CHANGELOG.md`. The `[Unreleased]` section accumulates between releases. ## Manual fallback If CI is unavailable: `docker login` to `lgtmsupplyco`, then `./scripts/publish-image.sh v0.3.0` (defaults to `v` if omitted).