203 lines
6.5 KiB
YAML
203 lines
6.5 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
|
|
|
|
# `github.sha` exists on GitHub and on Gitea (which aliases the github
|
|
# context). The `gitea.*` context only exists on Gitea, so a workflow
|
|
# under .github/ that used it could never tag an image here.
|
|
#
|
|
# The image is published by .gitea/workflows/build.yaml on the real
|
|
# runner, which is the one with registry access; this job only pushes
|
|
# when this repository has been given a token for it, so a GitHub runner
|
|
# without credentials builds and stops instead of failing the pipeline.
|
|
- name: Build
|
|
run: |
|
|
docker build --build-arg VERSION=${{ github.sha }} \
|
|
-t $IMAGE:${{ github.sha }} -t $IMAGE:latest .
|
|
|
|
- name: Push image
|
|
run: |
|
|
docker push $IMAGE:${{ github.sha }}
|
|
docker push $IMAGE:latest |