Steps 1 and 2 of this milestone shipped with no CI coverage at all, and the
reason generalises: release.yml triggers only on main and tags, so nothing
inside it is exercised until a release is already running. That is the worst
place in the repo to be unguarded, because the failure mode is silence — a
version nobody can compare looks exactly like being up to date, and nobody
reports an update they were never offered.
The fix is not a test that reads YAML. The derivation moved into
ci/version.sh, so it can be RUN, and internal/server/release_version_test.go
runs it on every push. release.yml now calls the same script, so the thing
that ships and the thing under test are one artifact rather than two copies
that agree until they don't.
test-go.yml gains 'ci/**' and '.gitea/workflows/release.yml' in its paths.
Without that the guard exists but never fires on the changes it protects,
which is the same nothing it replaces.
What is pinned, and why each one:
- HHMM is zero-padded. A build at 00:42 must emit "0042"; a stripped
leading zero shifts the segment two orders of magnitude and reverses
comparisons against every other build that day. It only bites for a
tenth of the day, so it will not be found by chance.
- The name derives from the COMMIT and the code from the BUILD. Asserted
by holding one clock and moving the other: the name must not move, the
code must.
- The code clears 1895, the highest versionCode the retired commit-count
scheme shipped. Below that Android refuses the upgrade as a downgrade
and the channel becomes a one-way door.
- The tag is the name with a `v`, never chosen.
- release.yml still calls the script, and does not derive a commit count
again. This pins the WIRING: without it every other assertion keeps
passing while the shipped path silently drifts out of coverage.
The script rejects unusable clocks rather than emitting something plausible,
and those rejections are tested — a guard that cannot fail is worse than
none, because it reads as coverage.
Falsified before committing rather than after: ran the script against good
and broken inputs and watched all three failure paths fire; verified every
asserted value by executing it rather than by reading it; and checked the
two workflow predicates catch their regressions while staying immune to a
comment that merely names the old formula.
Step 5 of 5 — Scribe task #3812, milestone #390.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
157 lines
6.8 KiB
YAML
157 lines
6.8 KiB
YAML
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'
|
|
- 'Makefile'
|
|
- 'internal/**'
|
|
- 'cmd/**'
|
|
- '.golangci.yml'
|
|
- '.gitea/workflows/test-go.yml'
|
|
# The release lane's own trigger is `main` + tags, so nothing it
|
|
# contains is exercised until a release is already running. These two
|
|
# entries are what let internal/server/release_version_test.go guard
|
|
# the version derivation on ordinary dev pushes instead.
|
|
- 'ci/**'
|
|
- '.gitea/workflows/release.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: Generated code matches queries (sqlc)
|
|
run: make verify-generate
|
|
|
|
- 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 ./...
|