Files
cole-maxwell1 2ff14e6221 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.
2026-08-07 11:38:23 -05:00

4.1 KiB

Repository Guide

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.

Commands

  • 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.

Generated And Runtime Behavior

  • Edit Tailwind source in app/web/input.css and templates under app/web/templates/; regenerate the tracked app/web/static/app.css with make css after class or style changes.
  • The standalone Tailwind binary is downloaded at pinned version v3.4.17 to ignored app/tailwindcss.
  • The server requires DATABASE_URL, defaults APP_ADDR to :8080, and runs xorm Sync2 for the todos table during startup. Keep xorm.io/xorm paired with github.com/lib/pq in this stack.
  • make generate-inventory reads applied OpenTofu output and overwrites ignored ansible/inventory/hosts.yml; rerun it after server replacement or address changes.

Infrastructure And Deployment

  • OpenTofu commands must use both var files against the root stack, for example tofu -chdir=infra plan -var-file=environments/prod/prod.tfvars -var-file=environments/staging/staging.tfvars. The staging file only supplies staging-specific overrides.
  • Prefer TF_VAR_hcloud_token, TF_VAR_s3_access_key, and TF_VAR_s3_secret_key; values in *.tfvars override those environment variables.
  • Keep the AWS provider on ~> 5.69.0: newer releases wait for an AWS-only response header absent from Hetzner Object Storage.
  • 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.