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
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-imagemeans do not bump the wrapper chart to that version — it cannot unpublish anything. The image isFROM scratch, so trivy sees exactly one target, the Go binary and its module graph. - The wrapper chart's
values.yamlhas twotag:lines — the app image and the python backup sidecar — sochart-bumpis given--imageto say which one moves. Once the wrapper chart drops the sidecar and declares apostgresqlCR instead, there is onetag:line again, and--imagebecomes 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.