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 '