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
+38 -5
View File
@@ -21,7 +21,7 @@ Staging contains an unchanged copy of production todo data. Treat staging hosts,
- Docker or Podman for image builds
- `curl` and `make`
- A Hetzner Cloud project, API token, SSH key, and Object Storage credentials
- A container registry if deploying a non-public image
- A container registry account only if replacing the public application image with a private image
The app intentionally uses `xorm.io/xorm` with `github.com/lib/pq`, which is the PostgreSQL driver supported by xorm in this stack.
@@ -50,6 +50,40 @@ docker build -t todo:local app
`make css` downloads the pinned standalone Tailwind CLI to `app/tailwindcss`. The Docker build independently downloads the CLI before compiling CSS, so it does not depend on a pre-existing generated stylesheet.
## Published Container Image
GitHub Releases publish the application image to GitHub Container Registry for Linux AMD64 and ARM64 hosts:
```sh
docker pull ghcr.io/cole-maxwell1/todo-hetzner:latest
```
The `latest` tag tracks the newest stable release and is the convenient default for this demonstration stack. Version tags are safer for reproducible deployments. Release `v1.2.3`, for example, publishes `v1.2.3`, `1.2.3`, `1.2`, and `1`; prereleases publish only versioned tags. Pin the registry digest when the deployed bytes must be immutable:
```yaml
app_image: ghcr.io/cole-maxwell1/todo-hetzner@sha256:replace-with-published-digest
```
After the initial visibility step below, the package is public and does not require registry credentials to pull. A fork can change `app_image` in `ansible/inventory/group_vars/all/main.yml` to use another public or private image.
## Continuous Integration And Releases
The `Go tests` workflow runs for pull requests targeting `main` and pushes to `main` when files under `app/` or the workflow itself change. It starts PostgreSQL, runs every Go test including `TestStoreLifecycle`, and runs `go vet`. Infrastructure-only changes do not trigger the workflow.
Configure a branch protection rule for `main` that requires pull requests. Do not make the path-filtered test job a globally required status check: GitHub leaves that check pending when an infrastructure-only pull request does not trigger the workflow. The repository cannot define branch protection in workflow YAML.
Published GitHub Releases build and publish the container. Release tags must use semantic versioning with a `v` prefix. Create a release after its commit is merged and CI succeeds:
```sh
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0
gh release create v0.1.0 --verify-tag --generate-notes
```
The release workflow checks out the tagged commit, reruns tests and vet with PostgreSQL, builds the multi-architecture image, publishes its tags, and records a provenance attestation. Draft releases do not publish images. Stable releases update `latest`; prereleases do not.
GHCR creates the package as private on its first publication. After the first release workflow succeeds, open the package settings, change its visibility to public, and verify that an unauthenticated pull succeeds. This is a one-time repository-owner operation; package visibility cannot be declared by this repository's workflow files.
## Hetzner Credentials And Access
The stack needs two independent Hetzner credentials:
@@ -299,15 +333,14 @@ export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.config/ansible/todo-vault-password"
make deploy-ansible
```
Build and push the image, then run configuration in this order:
The default Ansible configuration pulls the public `latest` image. Run configuration in this order:
```sh
docker build -t registry.example.com/todo:2026-08-06 app
docker push registry.example.com/todo:2026-08-06
docker pull ghcr.io/cole-maxwell1/todo-hetzner:latest
make deploy-ansible
```
`setup-node.yml` installs and configures the host. `deploy-app.yml` optionally authenticates to a registry, pulls the image, reloads the user systemd manager, restarts `app-todo.service`, and checks health. Omit registry variables for a public image. Run `make generate-inventory` again after OpenTofu replaces a server or changes a public address.
The explicit local pull is optional and only verifies public access from the operator's machine. `setup-node.yml` installs and configures the host. `deploy-app.yml` optionally authenticates to a private registry, pulls `app_image` on each host, reloads the user systemd manager, restarts `app-todo.service`, and checks health. Leave the registry variables unset for the default public image. Run `make generate-inventory` again after OpenTofu replaces a server or changes a public address.
## Backup Operations