name: test-go # Go server: vet + golangci-lint + short race tests. Runs on push to # dev/main and PRs to main, scoped to Go-side files only — web-only or # Flutter-only diffs don't trigger this workflow. # # Two jobs: `test` (fast — vet + lint + `go test -short -race`, no DB) and # `integration` (full `go test -race` against an ephemeral Postgres). # # Integration-job DB wiring follows the act_runner shared-daemon pattern: # the runner's Docker daemon also runs the operator's dev compose stack, # so service containers get NO published ports (collision) and no # service-name DNS. We discover the service container by the job-scoped # name filter via the mounted docker socket and reach it by bridge IP. # The exactly-one assertion is a hard guard — pointing tests at the dev # Postgres would truncate it (the disaster Fable #339 exists to prevent). # # `web/build/` has a committed placeholder index.html so go:embed succeeds # without needing the SPA to be freshly built. Real builds happen in # release.yml (container) and locally during dev. on: push: branches: [dev, main] paths: - '**/*.go' - 'go.mod' - 'go.sum' - 'sqlc.yaml' - 'internal/**' - 'cmd/**' - '.golangci.yml' - '.gitea/workflows/test-go.yml' # pull_request trigger intentionally omitted — see test-web.yml for # the rationale (single-author repo, push covers PR-merge equivalent). concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: go-ci container: image: git.fabledsword.com/bvandeusen/ci-go:1.26 steps: - name: Checkout uses: actions/checkout@v4 - name: Toolchain versions run: | go version golangci-lint --version - name: go vet run: go vet ./... - name: golangci-lint run: golangci-lint run ./... - name: go test (short, race) run: go test -short -race ./... integration: runs-on: go-ci container: image: git.fabledsword.com/bvandeusen/ci-go:1.26 services: postgres: image: postgres:16-alpine env: POSTGRES_USER: minstrel POSTGRES_PASSWORD: minstrel POSTGRES_DB: minstrel_test # No `ports:` — the runner shares the operator's dev compose # Docker daemon; publishing a fixed host port collides. steps: - name: Checkout uses: actions/checkout@v4 - name: Integration suite (discover service by bridge IP, migrate, test) run: | set -eux # Discover THIS job's Postgres service container via the # mounted docker socket. act_runner attaches the job # container and its service container(s) to a shared per-job # network, so scope discovery to a postgres that sits on a # network THIS job container is also on. The old # `--filter name=integration` matched EVERY concurrent # integration run's postgres (a dev push + the main-merge run # overlap → 2 candidates → false "expected exactly 1" abort). # The operator's dev compose `minstrel-postgres-*` is never on # this job's network; skip it explicitly as belt-and-suspenders # (a wrong target would truncate real data). SELF=$(cat /etc/hostname) SELF_NETS=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$SELF") test -n "$SELF_NETS" echo "self ($SELF) networks: $SELF_NETS" PG_ID="" PG_NAME="" for cid in $(docker ps --filter "ancestor=postgres:16-alpine" -q); do nm=$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##') case "$nm" in *minstrel-postgres*|*_postgres_*) continue ;; esac for net in $(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$cid"); do case " $SELF_NETS " in *" $net "*) PG_ID="$cid"; PG_NAME="$nm"; break 2 ;; esac done done test -n "$PG_ID" || { echo "FATAL: no postgres service container on this job's network (self nets: $SELF_NETS)"; exit 1; } echo "selected postgres: $PG_ID $PG_NAME" PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID") test -n "$PG_IP" export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable" # Wait for Postgres to accept TCP (no health-check dependency). for i in $(seq 1 60); do (echo > "/dev/tcp/${PG_IP}/5432") 2>/dev/null && break; sleep 2; done # Relax durability on the throwaway CI Postgres. Our test pattern # is dbtest.ResetDB → TRUNCATE … RESTART IDENTITY CASCADE before # every test, and the per-TRUNCATE commit fsync is the dominant # cost of the integration suite. The CI DB is rebuilt every run so # fsync / full_page_writes / synchronous_commit buy nothing. Apply # via docker exec because: # - The act_runner `services:` block can't override the container # command, so `postgres -c fsync=off` at boot isn't an option. # - ALTER SYSTEM cannot run inside a transaction; psql -c # auto-commits each statement, which is what we need. # - fsync / full_page_writes are sighup GUCs and # synchronous_commit is user-context, so pg_reload_conf() picks # all three up with no restart. # Non-fatal: a perms surprise degrades to "slower", never red CI. docker exec "$PG_ID" psql -U minstrel -d minstrel_test \ -c "ALTER SYSTEM SET fsync = off" \ -c "ALTER SYSTEM SET synchronous_commit = off" \ -c "ALTER SYSTEM SET full_page_writes = off" \ -c "SELECT pg_reload_conf()" \ || echo "WARN: durability relax failed; continuing" # Apply embedded migrations to the fresh test DB, then run the # full suite (no -short → integration tests execute). -p 1: # every integration package TRUNCATEs the one shared test DB; # concurrent package binaries → TRUNCATE deadlocks. Serialize # package execution (the documented local invocation too). MINSTREL_DATABASE_URL="$MINSTREL_TEST_DATABASE_URL" go run ./cmd/minstrel migrate go test -p 1 -race ./...