Files
terdut-server/docs/development.md
T
Niklas Ye 44b2eb2cc3 Rewrite the README as highlights with screenshots; move the detail into docs/
The README was 1,240 lines of reference material and still described a
SQLite quick start. It is now a short tour (highlights, screenshots of the
web UI, an accurate quick start against Postgres), and each topic has its
own page under docs/ with an index: deployment, configuration, Alertmanager,
incidents, notifications, escalation, dead man's switches, single sign-on,
web UI, API and development. SERVICE-ACCOUNTS.md is rewritten from a
proposal into a reference, and TEAM-LOOKUP.md is gone with the endpoint it
described. The "Upgrading to ..." sections for an unreleased product are
dropped.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-09 14:56:13 +02:00

5.1 KiB

Development and releasing

Building, testing and releasing the server. Back to the README and the documentation index.

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

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.