ops(ci): automate testing and container image releases

Add PostgreSQL-backed Go validation for application changes and semantic-version release publishing to GHCR.

Default deployments to the public image and prevent host setup from starting the application before deployment.
This commit is contained in:
cole-maxwell1
2026-08-07 11:38:23 -05:00
parent d118f52387
commit 2ff14e6221
6 changed files with 229 additions and 8 deletions
+3
View File
@@ -3,6 +3,7 @@
## Boundaries
- `app/` is the Go 1.22 module. The server entrypoint is `app/cmd/server/main.go`; handlers own HTTP behavior, `internal/db` owns xorm persistence, and `app/web` embeds templates plus compiled CSS into the binary.
- `.github/workflows/go-tests.yml` gates app changes with PostgreSQL-backed tests. Published semantic-version GitHub Releases run `.github/workflows/publish-image.yml` and publish the multi-architecture image to GHCR.
- `infra/` is one OpenTofu root module and one shared state for both production and staging. `infra/environments/*` contain input files, not independent stacks.
- `ansible/playbooks/setup-node.yml` configures both hosts; `deploy-app.yml` pulls and restarts the rootless Podman/Quadlet app. Environment-specific behavior is selected by generated inventory groups and `deployment_environment`.
- Staging restores unchanged production data. Treat staging, backups, logs, real tfvars, Vault files, and state as production-sensitive; inspect examples instead of secret-bearing files.
@@ -12,6 +13,7 @@
- Run repository checks with `make validate`: Go tests, `go vet`, OpenTofu validation, then both Ansible syntax checks.
- Run one Go test from `app/`: `go test ./internal/handler -run '^TestName$'` or `go test ./internal/db -run '^TestStoreLifecycle$'`.
- `internal/db` integration tests silently skip unless `TEST_DATABASE_URL` points to PostgreSQL. Normal `make test` needs no database.
- GitHub CI supplies PostgreSQL so the `internal/db` integration test must run rather than skip.
- Format Go and OpenTofu files with `make format`; it runs `gofmt` and recursive `tofu fmt` (including real ignored tfvars), but does not format YAML or templates.
- Build the embedded app with `make build-app`. This first runs Tailwind and writes `app/web/static/app.css`; `make css` performs only that asset step.
- Build the deployment image with `docker build -t todo:local app`. Its CSS stage is self-contained and does not use the local `app/tailwindcss` binary.
@@ -31,4 +33,5 @@
- Run Ansible from `ansible/`, as the Make targets do, or set `ANSIBLE_CONFIG=ansible/ansible.cfg`; otherwise the repository role path is not loaded.
- `make deploy-ansible` is an operational deployment, not a verification command. It regenerates inventory, runs host setup before app deployment, requires decryptable Vault files, and acts on both production and staging.
- The Quadlet uses `Pull=never`; `deploy-app.yml` intentionally pulls `app_image` before restarting `app-todo.service`.
- The example Ansible configuration defaults to the public `ghcr.io/cole-maxwell1/todo-hetzner:latest` image. Prefer a version or digest override when reproducibility matters.
- Production WAL-G archives under a prefix ending in `/prod`. Staging's scheduled `db-sync` destructively replaces `/var/lib/pgsql/data` from `LATEST` and leaves the app stopped on failure; do not weaken its path/prefix guards or use it as a routine test.