Files
noitu/.github/workflows/ci.yml
T

205 lines
6.5 KiB
YAML

# Tests and packaging.
#
# Nothing here downloads the upstream wordlist. The image is built from the
# committed corpus, data/dictionary.txt, exactly as it ships; the test suites
# play against the small database derived from the checked-in word sample,
# which goes through the same builder the real one does.
#
# The wire contract has its own workflow: see proto.yml.
name: ci
on:
push:
branches: [main, dev]
pull_request:
release:
types: [published]
permissions:
contents: read
jobs:
go:
name: Go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version: stable
cache-dependency-path: server/go.sum
- name: Vet
run: go vet ./...
working-directory: server
# gofmt -l lists files that are not gofmt-clean; -d would only show the
# diff. Non-empty output is the failure signal, so it decides the exit
# code itself rather than leaning on the emptiness of a report nobody
# reads.
- name: Format check
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "not gofmt-clean:"
echo "$unformatted"
exit 1
fi
working-directory: server
# Default linters only: this is a signal every PR has to pass, not a
# style debate, so nothing beyond golangci-lint's own defaults is
# enabled here.
- name: Lint
uses: golangci/golangci-lint-action@v9
with:
working-directory: server
# The module's go directive is only the minimum. CI runs the newest
# toolchain, the way the image's golang:1 does, so vet and race results
# come from the compiler that ships. govulncheck reads the call graph
# against the current advisory database.
- name: Vulnerability scan
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
working-directory: server
# -race because the whole transport layer is goroutines and timers, and a
# data race there is exactly the kind of defect that passes without it.
- name: Test
run: go test ./... -race
working-directory: server
web:
name: Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci
working-directory: web
- name: Type check
run: npm run check
working-directory: web
- name: Lint
run: npm run lint
working-directory: web
# Runtime dependencies only: the frontend ships as static files, so a
# dev-tool advisory cannot reach production.
- name: Audit
run: npm audit --omit=dev --audit-level=high
working-directory: web
# npm test builds first, so this also proves the bundle compiles and
# carries no wordlist.
- name: Test
run: npm test
working-directory: web
e2e:
name: End to end
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version: stable
cache-dependency-path: server/go.sum
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build the fixture dictionary
run: go run ./cmd/build-dictionary --words ../testdata/fixture-words.txt --out ../data/fixture.db --min-words 150
working-directory: server
- run: npm ci
working-directory: web
- name: Install the browser
run: npx playwright install --with-deps chromium
working-directory: web
- name: Run the suite
run: npm run test:e2e
working-directory: web
# test-results holds the traces a failure leaves behind, which is what is
# actually worth downloading; the HTML report is not generated here.
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces
path: web/test-results/
retention-days: 7
image:
name: Container image
runs-on: ubuntu-latest
# e2e too: the moment this job gains a publish step, an image built past a
# red browser suite is an image nobody meant to ship.
needs: [go, web, e2e]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# The real dictionary, from the committed corpus: the image every run
# builds is the one that ships, so a corpus the builder rejects fails
# here rather than at deploy time.
- name: Build
run: docker build -t noitu:ci .
# CC BY-SA 4.0 applies to the derived wordlist wherever it is
# distributed, and an image is distribution. This is the assertion that
# the obligation actually shipped.
- name: The licence travels with the data
run: |
set -eu
docker create --name check noitu:ci
docker export check | tar -t > files.txt
docker rm check
for required in app/LICENSE app/data/LICENSE app/data/ATTRIBUTION.md app/NOTICE app/data/noitu.db; do
grep -qx "$required" files.txt || { echo "missing from the image: $required"; exit 1; }
done
# The upstream dump, compressed or not, must never reach the final
# image.
if grep -Eq '\.(bz2|xml)$' files.txt; then
echo "the upstream dump leaked into the image"
exit 1
fi
- name: It serves a game
run: |
set -eu
docker run -d --name noitu -p 8080:8080 noitu:ci
for _ in $(seq 1 30); do
if curl -fsS http://localhost:8080/healthz >/dev/null 2>&1; then break; fi
sleep 1
done
curl -fsS http://localhost:8080/healthz
# The image has no curl, so the HEALTHCHECK command is the binary
# itself; run it the way the container runtime does.
docker exec noitu /app/noitu-server -healthcheck
# A deep link is a client route, so the binary has to answer it with
# the app shell rather than a 404.
curl -fsS -o /dev/null -w '%{http_code}\n' 'http://localhost:8080/play?difficulty=2' | grep -qx 200
docker rm -f noitu