CI/CD: build the Docker image once on main, promote it on release tags
CI / test (push) Failing after 11m55s
CI / build-image (push) Skipped

Previously release.yml fully rebuilt and re-ran lint/typecheck/test/build
from source on every tag push, duplicating what ci.yml had already just
verified on main. Now ci.yml builds and pushes the image (tagged by commit
SHA) once, right after its checks pass, but only for pushes to main.
release.yml no longer builds anything: it pulls that exact image, retags
it as the release version + latest, and deploys it.

This guarantees the deployed image is byte-for-byte what CI verified, and
turns "tag a commit that was never pushed to main or hasn't finished CI"
into a loud, immediate failure (no image to promote) instead of a rebuild
that silently skips the checks.
This commit is contained in:
2026-09-06 22:24:07 +02:00
parent 526a908c80
commit f1cd07b3ad
3 changed files with 110 additions and 56 deletions
+16 -7
View File
@@ -3,12 +3,18 @@
Production deployment is fully CI/CD-driven via Gitea Actions. There is no `update.sh` and the production server never builds anything — it only ever pulls a tested image from the registry.
```text
Developer → git push → Gitea → Gitea Actions
→ Tests → Docker Build → Registry Push
→ Release (git tag vX.Y.Z) → SSH → deploy.sh
→ docker compose pull → migrate → up -d → Healthcheck → LIVE
Developer → git push main → Gitea Actions (CI)
→ Tests → Docker Build → Registry Push (tagged by commit SHA)
→ Release (git tag vX.Y.Z) → promote that exact image to vX.Y.Z + latest
→ SSH → deploy.sh → docker compose pull → migrate → up -d → Healthcheck → LIVE
```
Building and testing happen exactly once, on `main` — tagging a release never
rebuilds or re-tests, it only promotes the already-verified image for that
commit. Tagging a commit that hasn't been pushed to `main` (or whose CI run
hasn't finished yet) fails the release with a clear error instead of quietly
building from source and skipping the checks.
## One-time server setup
```bash
@@ -64,15 +70,17 @@ Gitea Actions takes it from there (see below). Once it's live, seed the original
Runs on every push and PR: `npm ci` → lint → typecheck → test → build, on the `ubuntu-latest`-labelled runner. No database is required — every Payload-backed route in this app is `force-dynamic`, so a production build never touches Postgres. (There's no automated test suite yet — `npm test` is an honest placeholder; add a real one, e.g. vitest, whenever the project needs it, and it'll run here automatically.)
On top of that, every push to `main` that passes the checks above also builds the Docker image and pushes it to the registry tagged by commit SHA (`git.maro.run/maro/anouma:<sha>`) — this is the one and only build of that commit; nothing later rebuilds it.
### Release (`.gitea/workflows/release.yml`)
Triggered by pushing a tag matching `v*` (e.g. `v1.2.0`):
1. Re-runs the same quality gates as CI.
2. Builds the Docker image and pushes `git.maro.run/maro/anouma:vX.Y.Z` **and** `:latest`.
1. Pulls the image CI already built and pushed for that commit's SHA — fails immediately with a clear error if it isn't there (the tagged commit was never on `main`, or CI for it hasn't finished yet).
2. Retags that same image as `git.maro.run/maro/anouma:vX.Y.Z` **and** `:latest`, and pushes both.
3. SSHes into the production server as `anouma-deploy` and runs `deploy.sh deploy vX.Y.Z`.
Production always runs a specific version tag — `latest` is pushed for convenience/reference only, `deploy.sh` never uses it.
Production always runs a specific version tag — `latest` is pushed for convenience/reference only (and always points at the most recently *released* build, not just the newest commit on `main`), `deploy.sh` never uses it.
## `deploy.sh` (server-side, `/opt/anouma/deploy.sh`)
@@ -190,6 +198,7 @@ Neither `setup.sh` nor `deploy.sh` ever runs `docker system prune`, `docker volu
| Symptom | Check |
| --- | --- |
| `setup.sh` fails at "Datenbank wurde nicht rechtzeitig healthy" | `docker compose logs postgres` (from `/opt/anouma`) — usually a bad `POSTGRES_PASSWORD`/`DATABASE_URI` mismatch if you hand-edited `.env` |
| Release workflow fails at "Pull the tested image" | The tagged commit was never pushed to `main`, or `main`'s CI/build-image run for it hasn't finished yet — push to `main`, wait for CI to go green, then tag |
| Release workflow fails at "Deploy to production" | Check the Gitea secrets are all set correctly, and that the server's SSH port is reachable from the runner |
| App container unhealthy after a deploy | `ssh` directly to the server (not via the restricted key) and run `docker compose logs app` from `/opt/anouma` |
| Video calls connect but no audio/video | Likely a restrictive NAT — configure a TURN server (see above) |