Files
fancy-dns/.github/workflows/ci.yml
T
2026-09-27 16:30:16 +02:00

194 lines
5.9 KiB
YAML

name: ci
on:
push:
branches: [main]
pull_request:
# A push to a fork's branch is the common case for a PR-based workflow, so
# run on any branch that has a PR rather than only on main.
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# Must track the `go` directive in go.mod; an older runner fails every
# build with "go.mod requires go >= ...". The fmt check only has real teeth
# when the toolchain is new enough (gofmt is part of the Go toolchain in 1.27).
GO_VERSION: "1.27"
NODE_VERSION: "22"
IMAGE: registry.k6n.net/fancydns
jobs:
go:
name: Go build, vet and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
# The placeholder keeps //go:embed valid when the console has not been
# built in this job, so the Go checks do not depend on the web toolchain.
- name: Ensure the embed directory exists
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "gofmt needed on:"; echo "$unformatted"; exit 1
fi
- name: go vet
run: go vet ./...
# The race detector is not optional here: the whole design leans on
# atomic holders and swap hooks, and a regression there is silent at
# runtime and only shows up as a data race under load.
- name: go test -race
run: go test -race -count=1 ./...
- name: go test -short (no race, coverage)
run: go test -short -count=1 -coverprofile=coverage.out ./...
- name: Coverage summary
run: go tool cover -func=coverage.out | tail -20
- name: Build without the console
run: CGO_ENABLED=0 go build -trimpath -o /tmp/fancy-dns ./cmd/fancy-dns
web:
name: Console typecheck and build
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install
run: npm ci --no-audit --no-fund
- name: Typecheck
run: npx tsc -b --noEmit
- name: Build
run: npm run build
- name: Assert the console landed where the Go embed looks
run: test -f ../internal/web/dist/index.html
binary:
name: Binary with the console embedded
runs-on: ubuntu-latest
needs: [go, web]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install console dependencies
working-directory: web
run: npm ci --no-audit --no-fund
- name: Build the console
working-directory: web
run: npm run build
- name: Build the binary
run: |
CGO_ENABLED=0 go build -trimpath \
-ldflags "-s -w -X git.k8s-git-k6n.net/mats/fancy-dns/internal/app.Version=${{ github.sha }}" \
-o fancy-dns ./cmd/fancy-dns
# A binary that starts, binds, and validates a config end to end: this
# catches a broken embed or a bad default that unit tests never exercise.
- name: Smoke test the real binary
run: |
set -euo pipefail
mkdir -p /tmp/smoke
# Unprivileged ports, and trust_loopback off: the example config trusts
# loopback, which would make the 401 assertion below pass for the
# wrong reason (the request would be authorised as a local caller).
sed \
-e 's#127.0.0.1:8080#127.0.0.1:18080#' \
-e 's#0.0.0.0:53#127.0.0.1:15353#g' \
-e 's#\[::\]:53#[::1]:15353#g' \
-e 's#trust_loopback: true#trust_loopback: false#' \
deploy/config.example.yaml > /tmp/smoke/config.yaml
./fancy-dns -config /tmp/smoke/config.yaml -check
./fancy-dns -config /tmp/smoke/config.yaml &
pid=$!
trap 'kill $pid 2>/dev/null || true' EXIT
for i in $(seq 1 40); do
curl -fsS http://127.0.0.1:18080/api/healthz >/dev/null 2>&1 && break
sleep 0.5
done
echo "--- health"
curl -fsS http://127.0.0.1:18080/api/healthz | tee /dev/stderr | grep -q '"status"'
echo "--- console is embedded"
curl -fsS http://127.0.0.1:18080/ | grep -qi '<!doctype html'
echo "--- unauthenticated admin API is refused"
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/api/whoami)
test "$code" = "401" || { echo "expected 401, got $code"; exit 1; }
echo "--- authenticated admin API works"
curl -fsS -H "X-API-Key: CHANGE-ME-long-random-string" \
http://127.0.0.1:18080/api/whoami | grep -q '"can_write":true'
echo "smoke test passed"
- name: Upload the binary
uses: actions/upload-artifact@v4
with:
name: fancy-dns-linux-amd64
path: fancy-dns
retention-days: 14
image:
name: Container image
runs-on: amd64
needs: [binary]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Build
run: |
docker build -t $IMAGE:${{ gitea.sha }} -t $IMAGE:latest .
- name: Push image
run: |
docker push $IMAGE:${{ gitea.sha }}
docker push $IMAGE:latest