# Development and releasing _Building, testing and releasing the server._ Back to the [README](../README.md) and the [documentation index](./README.md). ## Upgrading The schema is a single baseline (`internal/db/migrations/001_schema.sql`) and no release has shipped yet, so there is no upgrade path from earlier development databases: start from an empty one. Changes after the first release arrive as new numbered migrations. ## Development ```bash make test-db # start a local Postgres for the tests (podman or docker) make test # run all tests go build ./... # compile all packages go run ./cmd/terdut # run locally (needs TERDUT_DB_DSN) ``` The tests need a real Postgres, because the server does — there is no in-memory Postgres. `TERDUT_TEST_DSN` says where it is, `make test-db` starts one on port 5433 and prints the DSN, and `make test-db-stop` removes it. Each test gets its own schema on that server, so tests cannot see each other's rows. An unset `TERDUT_TEST_DSN` fails the suite rather than skipping it: a run that quietly tests nothing is worse than one that does not run. `make fmt lint test helm-lint` is the gate. It mirrors `.gitea/workflows/ci.yaml` step for step, so a green run here means a green pipeline — with one deliberate exception: `make test` adds `-race`, which CI does not. The sweeper, the notifier goroutine and the dead man's switch sweep all run concurrently against the same database, and a race between them would surface as a flaky incident in production rather than as a red build. The web UI lives in `internal/web/static/` as plain HTML, CSS and ES modules, embedded into the binary with `go:embed`. It has no build step and no npm, so editing a file and restarting the server is the whole loop. ## Releasing ``` push or PR → ci.yaml gofmt, go vet, go test -race govulncheck, gitleaks helm lint + render push tag vX.Y.Z → release.yaml the same gate, then publish: git.ryuvia.com/niklas/terdut-server:vX.Y.Z oci://git.ryuvia.com/niklas/terdut-server X.Y.Z then trivy-scan the pushed image PR to Ryuvia/charts → bump the wrapper chart to X.Y.Z; on merge Flux reconciles and the release rolls out ``` Both artifacts go to the **personal** Gitea namespace rather than `ryuvia`, because Gitea scopes package visibility to the owner with no per-package override — so `ryuvia/*` is private because the org is. Publishing to `niklas` keeps them anonymously pullable, which is why no pull secret is needed in the cluster. Same reasoning, and the same choice, as riksdata and rd-web. Saying **"Release"** runs all three rows: the `release` skill commits, pushes, tags, waits for the pipeline, and opens the `Ryuvia/charts` PR, stopping before the merge. See `~/.claude/skills/release/`, or `.release.conf` here for this repo's part of it. The chart is published **only** from the tag, by the `chart` job. There used to be a second publisher on every `charts/**` push to main, and the two raced for the same chart version with different answers — chart 0.9.0 went out reading `appVersion: "latest"` that way. One publisher, triggered by the tag (`766f439`). The cost is that a chart-only change has no version of its own and rides the next app tag. Both workflows are thin drivers over the Makefile: `ci.yaml` runs `make fmt lint test` and `make helm-lint`, `release.yaml` adds `make binaries`, `make push`, `make helm-package` and `make helm-push`. That is deliberate — it is what makes a green local gate and a green pipeline the same code rather than two descriptions of it, and it is how riksdata and rd-web have always worked. `make push` builds and pushes in one step, unlike those two, because the image is `linux/amd64,linux/arm64` and buildx cannot load a multi-platform result into the local image store. `make build` stays single-platform and local-only. Both refuse `VERSION=dev`: publishing is one command, so it is also one command to run by accident. Publishing happens by pushing a tag. Two things the release process needs to know about this repo: - **The image scan runs after publishing**, like riksdata's and rd-web's: trivy cannot read a locally built image on this runner, so it pulls the pushed one. A red `scan-image` means do not bump the wrapper chart to that version — it cannot unpublish anything. The image is `FROM scratch`, so trivy sees exactly one target, the Go binary and its module graph. - **The wrapper chart's `values.yaml` has two `tag:` lines** — the app image and the python backup sidecar — so `chart-bump` is given `--image` to say which one moves. Once the wrapper chart drops the sidecar and declares a `postgresql` CR instead, there is one `tag:` line again, and `--image` becomes belt and braces. The wrapper chart must have **its own `version:` bumped in the same commit**. Flux reconciles with `reconcileStrategy: ChartVersion`, so a chart whose version did not change produces no new artifact and the change is never deployed — with no error anywhere.