9029d48584
- TERDUT_OPERATOR_KEY creates or re-keys the instance-scoped service account
"terdut-operator" at every start, so terdut-operator needs no bootstrap
handshake. An instance-scoped account now acts as owner of every team's
configuration, but is not a member of any team.
- POST /api/teams takes an external_id (instance service accounts only) and
is idempotent on it, so automation finds its own team again after a crash
instead of adopting by display name. GET /api/teams?name= is removed.
- Integration and dead man's switch names are unique per team (409). The
escalation PUT accepts usernames and resolves them itself.
- The 18 migrations are squashed into 001_schema.sql, with no Default team.
TERDUT_DEADMAN_* and the env seeding of switches are removed: teams carry
their own. Existing development databases must be recreated.
Security and robustness:
- GET /api/users no longer returns other people's email or ntfy topic to
non-admins.
- The access log records the route pattern, so integration keys and ack
tokens in the path are not written to the log. Server errors are logged.
- Rate limits take the client address TERDUT_TRUSTED_PROXIES hops from the
right of X-Forwarded-For instead of trusting the first, forgeable entry.
- /api/bootstrap runs in a transaction under an advisory lock, so two
concurrent calls cannot both create an administrator.
- API key last_used_at is written at most every five minutes.
Cleanup: remove GET /api/incidents/{id}/alerts, unused exports, SQLite
remnants in comments and config.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
155 lines
6.0 KiB
YAML
155 lines
6.0 KiB
YAML
name: CI
|
|
|
|
# The release workflow gates a tag, which is late: a broken commit sits green until
|
|
# somebody decides to publish. This runs the same checks on the way in.
|
|
#
|
|
# push is scoped to main rather than all branches so that a branch pushed as part of a
|
|
# pull request is not checked twice.
|
|
#
|
|
# No actions/checkout, deliberately -- same as the letsvisit and charts workflows. The
|
|
# runner image is ubuntu:22.04 whose `nodejs` package is Node 12, and actions/checkout@v4
|
|
# is built with ES2022 static initialiser blocks, so it dies with
|
|
# `SyntaxError: Unexpected token '{'` before running. Cloning with git directly avoids JS
|
|
# actions entirely. This repo is public, so the clone needs no credential at all.
|
|
#
|
|
# `${{ }}` values are passed through `env:` and referenced as quoted shell variables: a
|
|
# ref name is attacker-influenced by anyone who can push a branch or open a PR, and
|
|
# expanding one straight into `run:` is a shell-injection vector.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
# A rapid series of pushes only needs the last one checked.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
REPO_URL: https://git.ryuvia.com/niklas/terdut-server.git
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
# Runs inside the toolchain image rather than installing Go per job. Note this puts
|
|
# the job on the dind bridge, which cannot reach github.com or get.helm.sh --
|
|
# proxy.golang.org and git.ryuvia.com are reachable, which is all this job needs.
|
|
image: golang:1.26.6-bookworm
|
|
# act_runner destroys a job's own volumes when it finishes, so without these every
|
|
# run re-downloads the whole module graph. The names must appear in the runner's
|
|
# container.valid_volumes allowlist (charts/act-runner in the k8s repo); unlisted
|
|
# volumes are dropped silently, so a workflow that looks correct can still be
|
|
# running uncached.
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
|
|
# The suite needs a real Postgres -- there is no in-memory Postgres,
|
|
# so each test gets its own schema on a shared server instead.
|
|
# The job and the service share the dind bridge, so the service is reachable by its
|
|
# name rather than on localhost.
|
|
services:
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
env:
|
|
POSTGRES_USER: terdut
|
|
POSTGRES_PASSWORD: terdut
|
|
POSTGRES_DB: terdut_test
|
|
options: >-
|
|
--health-cmd "pg_isready -U terdut -d terdut_test"
|
|
--health-interval 5s
|
|
--health-timeout 5s
|
|
--health-retries 12
|
|
|
|
env:
|
|
# `make test` fails without this rather than skipping, so a green job here means
|
|
# the tests actually ran against a database.
|
|
TERDUT_TEST_DSN: postgres://terdut:terdut@postgres:5432/terdut_test?sslmode=disable
|
|
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
# A pull_request ref_name is "<n>/merge", which is not a fetchable branch.
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
# The gate is the Makefile's rather than a second copy of it here, the way riksdata
|
|
# and rd-web already do it. `make fmt lint test` is exactly what a developer runs, so
|
|
# a green pipeline and a green working copy mean the same thing by construction
|
|
# instead of by remembering to update two files together.
|
|
#
|
|
# The reasoning that used to live here moved with the targets: why gofmt is checked
|
|
# at all (import order survives `go vet`, and both repos sat unformatted through a
|
|
# green run and a release -- 9046f6e), why both of gofmt's failure modes need
|
|
# handling, and why `test` adds -race when this job does not have to.
|
|
- name: Format, vet and test
|
|
run: make fmt lint test
|
|
|
|
# Runs on every push and pull request, unlike the image scan, which needs something
|
|
# published to scan and so lives in release.yaml. Both are needed: govulncheck reads the
|
|
# source and its module graph, trivy reads the built artifact, and neither sees what the
|
|
# other does.
|
|
security:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: golang:1.26.6-bookworm
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
- name: Go vulnerability scan (govulncheck)
|
|
run: make security-go
|
|
|
|
- name: Secret scan (gitleaks)
|
|
run: make security-secrets
|
|
|
|
- name: Code security scan (gosec)
|
|
run: make security-code
|
|
|
|
# Host mode, no `container:`: helm is baked into the runner image, and a container job
|
|
# could not install it -- get.helm.sh is unreachable from the dind bridge. Same reason
|
|
# release.yaml's chart job runs on the host.
|
|
#
|
|
# The chart had no lint step in any workflow until 2026-09-01: release.yaml packaged and
|
|
# pushed it without rendering it first, so a template that did not compile would have
|
|
# been found by Flux rather than here.
|
|
chart:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
- name: Lint and render the chart
|
|
run: make helm-lint
|