Auto-deploy: ship main to dev and tagged releases to prod (no more hand-deploys) #134
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Category: ops · Effort: M
Problem
Neither environment auto-deploys. Every deploy is the hand-run checklist in
CLAUDE.md(steps 5–9): ssh in,git pull,docker compose up --build -dwith theGIT_SHA/GIT_DESCRIBEstamp, then check logs/health by hand.Consequences seen in practice:
docker-compose.ymlhad an uncommitted local edit (all four servicesrestart: unless-stopped→on-failure, made 2026-07-09 with a.bakbeside it) that no one was tracking. It survived because deploys are manual and nobody diffs the tree. It fought the v7.1.0 deploy (which edits that same file) and was only caught because the deploy collided with it. Reverted 2026-07-16 as stale..forgejo/workflows/ci.ymlheader already anticipates this: "Image build/push is intentionally omitted: the Actions runner exposes no Docker daemon to jobs. Revisit alongside CD."Proposal
Dev — deploy
mainon every green CI run. Low risk, gives the "does it boot" signal automatically instead of me driving it by hand each time.Prod — deploy on a version tag (
v*) only, after CI is green. Keeps prod deliberate (matching how releases are cut today) while removing the drift and the hand-typedGIT_SHAstamp.Both must preserve what the manual checklist gets right:
GIT_SHA/GIT_DESCRIBEso the admin version panel stays truthful;/healthafter the deploy and fail loudly if it doesn't come back;git reset --hard. Surface it instead.Open questions
forgejo-runnerVM 150), so this may already be viable; #103 (build the prod image in CI) is the natural prerequisite and should probably land first.Blocked on / related
Rhoving/iac-repo(forgejo-runner-ansible); the CD workflow itself lives here.Acceptance criteria
mainwith green CI deploys to dev automatically, with the version stamp intact, and fails the run if/healthdoesn't return{"status":"ok"}.v*tag with green CI deploys that exact tag to prod, stamped, with the same health gate.CLAUDE.md's post-phase checklist is updated to describe the automated path and keep the manual steps only as the documented fallback.Status: still blocked, deliberately not started. Two of the three prerequisites are now done, and two open questions are resolved — recording that so whoever picks this up doesn't re-derive it.
Prerequisites
109c53a) — CI now builds both production images on every push, so a Dockerfile/peer-dep regression can't reach a deploy. The job also asserts the migrations copy landed in the image.109c53a) —/healthnow actually probes DB + Redis and returns 503 when either is unreachable. This mattered more than it looked: the deploy gate in this issue's acceptance criteria was going to check/health, and until now/healthreturned{"status":"ok"}regardless of whether the database was up. An auto-deploy gated on the old endpoint would have cheerfully reported success over a broken instance.c1ed843): API job failed on a commit that passestsc, passes 242/242 locally, passes against a from-scratch DB+Redis, and passed on re-run. Gating deploys on that signal means either good releases blocked at random, or — if the gate is made advisory to compensate — deploying on a coin flip. Both are worse than the current manual checklist.Open questions — two now answered
https://git.rhoving.com/v2/→401, i.e. present and requiring auth;has_packages: trueon the repo). So the image-based CD path is viable without standing anything up: CI builds → pushes togit.rhoving.com/rbrooks/tealeaves-api:<tag>→ the host pulls that tag. Auth via a scoped Actions secret (see theforgejo-actions-secretsskill in iac-repo).ci.ymlcomment claiming "the Actions runner exposes no Docker daemon to jobs" was stale — it described the old docker-host runner (removed 2026-07-01, iac-repo#32). The dedicated runner VM mounts the DinD socket into every job container on purpose (forgejo-runner-ansible:forgejo_runner_container_options: "-v /var/run/docker.sock:/var/run/docker.sock"— "sodocker buildworks inside CI jobs"). Comment corrected in109c53a; the newimagejob proves it in practice.Recommended sequence
mainon green CI. Low blast radius, and it exercises the whole path.v*tags once dev has run clean for a while.Not starting the implementation until (1) lands — building a deploy pipeline on a signal we currently can't trust would bake the flake into releases.
Yes — Watchtower is the right call, and it's already your pattern
docker-host already runs per-stack Watchtower instances (
marginalia-watchtower,chore-machine-watchtower,nickfedor/watchtower:latest), so this isn't new infrastructure — it's extending an established convention. That also settles the two open questions from my earlier comment:And it resolves the docker-socket tension honestly: I argued against an autoheal sidecar on #137 partly because it needs
/var/run/docker.sock(root-equivalent). Watchtower needs the same socket — but you've already accepted that trade for two other stacks, and per-stack Watchtower instances keep the blast radius contained. For a routine, high-value workflow that's a defensible cost in a way a rare-brownout fix wasn't.What it takes (the actual work, when you want it)
git.rhoving.com/v2/). #103 already builds both images; add a push step:main→…/tealeaves-api:main(+ app) for dev auto-deployv*tag →…/tealeaves-api:release(moving tag) for prodGIT_SHA/GIT_DESCRIBEas build args so the admin version panel stays truthful (the stamp is baked into the image at build time).image: git.rhoving.com/rbrooks/tealeaves-api:release) instead of building locally, so there's a digest for Watchtower to watch. Prod and dev point at their respective tags.tealeaves-watchtower(mirroring the marginalia one) scoped to the TeaLeaves containers, watching that tag. Optionally trigger it from CI via Watchtower's HTTP API for an immediate deploy instead of waiting on its poll interval.Migrations need no special handling — they run on API boot, so a pulled image self-migrates on recreate.
The one caveat to design around
Watchtower does not health-gate or roll back: it recreates the container and leans on the restart policy + healthcheck. A bad release still auto-deploys. Mitigations already in place: CI must be green before the image is pushed (so
tsc+ 261 tests + image build gate it), and #97's/health+depends_on: service_healthystop nginx proxying to a not-yet-ready API. If you want true rollback-on-failure later, that's a Watchtower lifecycle-hook or a small deploy wrapper — worth a follow-up, not v1.Prereq reminder: the CI flake that was blocking this is fixed (
128e17b) with two consecutive green API runs since — so the "don't gate deploys on a flaky signal" objection is gone. Ready to build when you are; leaving this open pending your go-ahead on the CI-push + registry-image change.Done — image-based auto-deploy is live on both environments.
What shipped
.forgejo/workflows/release-images.yml— on push tomainpublishesgit.rhoving.com/rbrooks/tealeaves-{api,app}:beta; on avX.Y.Ztag publishes:stable(API images stamped withGIT_SHA/GIT_DESCRIBEvia--build-arg,--target production). A "Check publish preconditions" gate makes the job a no-op green skip unlessREGISTRY_TOKENexists and the ref is trusted (refs/heads/mainor av*tag), soworkflow_dispatchcan't publish from an arbitrary ref.deploy/—compose.yaml(name: tealeaves, so the existingtealeaves_*volumes carry over),.env.example, andREADME.mdwith the channel table + cutover procedure. Services pull:${TEALEAVES_CHANNEL}; a per-stack,--scope tealeaveslabel-scoped Watchtower auto-redeploys on a new image and prunes the old one. API self-migrates on boot, so an image swap migrates itself.c263861).Channels
:betamain:18090):stablevX.Y.Z:8055)Validated end-to-end
ok, Watchtower scoped + polling, and a subsequentmainpush auto-redeployed without intervention.:stableimages (2026-07-17): DB backed up first,downwithout-v, image stack up -> api healthy,TEA_LEAVES_GIT_DESCRIBE=v7.2.1, data intact (1 entry / 1 motif / 2 projects / 2 users, exactly matching the pre-cutover backup — no loss), runningtealeaves-{api,app}:stable, Watchtowerscope=tealeavespolling every 5 min, andhttps://tealeaves.newromanticsproject.com/health-> HTTP 200 through the public reverse proxy.Both hosts now deploy from
~/tealeaves-deploy/. The old source dirs are idle; the docs warn against building there alongside the image stack.Follow-ups already tracked: healthcheck-driven restart split to #137 (autoheal declined -> iac-repo issue opened); registry runner flakiness in iac-repo#159 (root-caused: tests fired real AI fetches under DinD — fixed via
AI_PROVIDER=anthropicin test setup).