268 lines
8.1 KiB
YAML
268 lines
8.1 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
env:
|
|
ZIG_VERSION: "0.16.0"
|
|
NODE_VERSION: "24"
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-24.04
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Zig
|
|
uses: mlugg/setup-zig@v2
|
|
with:
|
|
version: ${{ env.ZIG_VERSION }}
|
|
|
|
- name: Run test suite (unit + hermetic loopback integration)
|
|
run: zig build test -Dintegration
|
|
|
|
test-aarch64:
|
|
runs-on: ubuntu-24.04
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Zig
|
|
uses: mlugg/setup-zig@v2
|
|
with:
|
|
version: ${{ env.ZIG_VERSION }}
|
|
|
|
# qemu-user, not qemu-user-static: Zig execs the bare `qemu-aarch64`
|
|
# name, and the -static package only ships `qemu-aarch64-static`.
|
|
- name: Install qemu-user
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -qq -y --no-install-recommends qemu-user
|
|
|
|
- name: Run test suite under qemu (plain suite, no -Dintegration)
|
|
run: zig build test-aarch64 -fqemu
|
|
|
|
frontend:
|
|
runs-on: ubuntu-24.04
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: npm
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
working-directory: web
|
|
run: npm ci
|
|
|
|
- name: Check formatting
|
|
working-directory: web
|
|
run: npm run format:check
|
|
|
|
- name: Lint
|
|
working-directory: web
|
|
run: npm run lint
|
|
|
|
- name: Typecheck
|
|
working-directory: web
|
|
run: npm run typecheck
|
|
|
|
- name: Run tests
|
|
working-directory: web
|
|
run: npm test
|
|
|
|
- name: Build
|
|
working-directory: web
|
|
run: npm run build
|
|
|
|
cross:
|
|
runs-on: ubuntu-24.04
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Zig
|
|
uses: mlugg/setup-zig@v2
|
|
with:
|
|
version: ${{ env.ZIG_VERSION }}
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: npm
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Build the web UI
|
|
working-directory: web
|
|
run: |
|
|
npm ci
|
|
npm run build
|
|
|
|
# ReleaseSafe because the < 15 MiB budget (PLAN §18) is for release
|
|
# binaries; a Debug build strips to roughly 25 MiB and can never meet it.
|
|
- name: Build static musl executables
|
|
run: zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
|
|
- name: Install file(1) and strip tooling
|
|
run: |
|
|
missing=""
|
|
command -v file >/dev/null 2>&1 || missing="$missing file"
|
|
command -v objcopy >/dev/null 2>&1 || missing="$missing binutils"
|
|
command -v aarch64-linux-gnu-objcopy >/dev/null 2>&1 || missing="$missing binutils-aarch64-linux-gnu"
|
|
if [ -n "$missing" ]; then
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -qq -y $missing
|
|
fi
|
|
|
|
# The size budget applies to stripped binaries (PLAN §18) and
|
|
# `zig build cross` does not strip, so the assert measures a
|
|
# stripped copy and leaves the built artifact untouched.
|
|
- name: Assert executables are statically linked and within the size budget
|
|
run: |
|
|
set -euo pipefail
|
|
size_limit=$((15 * 1024 * 1024))
|
|
for triple in x86_64-linux-musl aarch64-linux-musl; do
|
|
binary="zig-out/cross/$triple/nxdns"
|
|
if [ ! -f "$binary" ]; then
|
|
echo "missing executable: $binary"
|
|
exit 1
|
|
fi
|
|
description=$(file -b "$binary")
|
|
echo "$triple: $description"
|
|
case "$description" in
|
|
*"statically linked"*) ;;
|
|
*)
|
|
echo "not statically linked: $binary"
|
|
exit 1
|
|
;;
|
|
esac
|
|
case "$triple" in
|
|
x86_64-*) strip_tool=objcopy ;;
|
|
aarch64-*) strip_tool=aarch64-linux-gnu-objcopy ;;
|
|
esac
|
|
"$strip_tool" --strip-all "$binary" "$binary.stripped"
|
|
size=$(stat -c %s "$binary.stripped")
|
|
echo "$triple: stripped size $size bytes"
|
|
if [ "$size" -ge "$size_limit" ]; then
|
|
echo "stripped executable exceeds the 15 MiB budget: $binary"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# PLAN §18 also budgets the binary without web assets (< 10 MiB). A
|
|
# separate prefix keeps the with-assets artifacts above intact.
|
|
- name: Build static musl executables without web assets
|
|
run: zig build cross -Doptimize=ReleaseSafe --prefix zig-out/nodist
|
|
|
|
- name: Assert asset-free executables are within the size budget
|
|
run: |
|
|
set -euo pipefail
|
|
size_limit=$((10 * 1024 * 1024))
|
|
for triple in x86_64-linux-musl aarch64-linux-musl; do
|
|
binary="zig-out/nodist/cross/$triple/nxdns"
|
|
if [ ! -f "$binary" ]; then
|
|
echo "missing executable: $binary"
|
|
exit 1
|
|
fi
|
|
case "$triple" in
|
|
x86_64-*) strip_tool=objcopy ;;
|
|
aarch64-*) strip_tool=aarch64-linux-gnu-objcopy ;;
|
|
esac
|
|
"$strip_tool" --strip-all "$binary" "$binary.stripped"
|
|
size=$(stat -c %s "$binary.stripped")
|
|
echo "$triple: stripped size without assets $size bytes"
|
|
if [ "$size" -ge "$size_limit" ]; then
|
|
echo "stripped asset-free executable exceeds the 10 MiB budget: $binary"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
docker:
|
|
runs-on: ubuntu-24.04
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Zig
|
|
uses: mlugg/setup-zig@v2
|
|
with:
|
|
version: ${{ env.ZIG_VERSION }}
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: npm
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Build the web UI
|
|
working-directory: web
|
|
run: |
|
|
npm ci
|
|
npm run build
|
|
|
|
- name: Build static musl executables
|
|
run: zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
|
|
- name: Build the image
|
|
run: docker build -t nxdns:ci -f deploy/docker/Dockerfile .
|
|
|
|
- name: Smoke test the container
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
docker run --rm nxdns:ci version
|
|
|
|
mkdir -p etc-nxdns
|
|
cat > etc-nxdns/config.zon <<'EOF'
|
|
.{
|
|
.groups = .{ .{ .name = "default" } },
|
|
.upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
|
|
}
|
|
EOF
|
|
|
|
cid=$(docker run -d --name nxdns-smoke \
|
|
-p 127.0.0.1:8080:8080 \
|
|
-v "$PWD/etc-nxdns:/etc/nxdns:ro" \
|
|
nxdns:ci)
|
|
trap 'docker rm -f nxdns-smoke >/dev/null 2>&1 || true' EXIT
|
|
|
|
# The published port works when the job runs on the docker host or in
|
|
# DinD; the container IP covers a runner that shares the daemon over
|
|
# a mounted socket.
|
|
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid")
|
|
healthy=""
|
|
for _ in $(seq 1 30); do
|
|
if [ "$(docker inspect -f '{{.State.Running}}' "$cid")" != "true" ]; then
|
|
echo "container exited during startup"
|
|
docker logs "$cid" || true
|
|
exit 1
|
|
fi
|
|
if curl -fsS "http://127.0.0.1:8080/api/health" \
|
|
|| { [ -n "$ip" ] && curl -fsS "http://$ip:8080/api/health"; }; then
|
|
healthy=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ -z "$healthy" ]; then
|
|
echo "no /api/health response within 30 seconds"
|
|
docker logs "$cid" || true
|
|
exit 1
|
|
fi
|
|
|
|
docker stop -t 30 nxdns-smoke
|
|
exit_code=$(docker inspect -f '{{.State.ExitCode}}' nxdns-smoke)
|
|
echo "exit code after SIGTERM: $exit_code"
|
|
docker logs nxdns-smoke || true
|
|
test "$exit_code" -eq 0
|