Files
zeekayandHanzo Dev 38881e6607 ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:09:45 -07:00

164 lines
6.8 KiB
YAML

name: CI
# Runs on the native runners at git.hanzo.ai (git-runner, label
# linux-amd64). There are no GitHub-hosted runners in this
# organisation and none is a permitted fallback: a workflow that names one
# queues forever, which reads from the outside as a test suite that never fails.
#
# Every branch, not only main. A compliance engine's tests are the evidence that
# its controls work, so they run before the merge that would rely on them.
on:
push:
branches: ["*"]
tags-ignore: ["v*"]
pull_request:
branches: ["*"]
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: [linux-amd64]
timeout-minutes: 30
# A runner may carry a machine-level go env that routes some module paths
# around the proxy and the checksum database. GOENV=off ignores it, so every
# module resolves through proxy.golang.org and is verified against
# sum.golang.org — a tag that moves later cannot change what was tested.
env:
GOENV: off
GOWORK: off
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: Modules verify against the checksum database
# An AML engine's dependency graph is part of its audit surface. This
# fails on a moved tag rather than building whatever the cache holds.
run: |
go mod download
go mod verify
- name: Formatting
run: |
unformatted=$(gofmt -l pkg cmd)
if [ -n "$unformatted" ]; then
echo "::error::not gofmt-clean:"; echo "$unformatted"; exit 1
fi
- name: Vet
run: go vet ./...
- name: Test with the race detector
# Ingest is concurrent by construction — the aggregate windows and the
# behavioural model are written on the request path — so -race is the
# test run, not an extra one.
run: go test -race -count=1 ./...
- name: Build the daemon
# The daemon is the API and nothing else — the console is a separate
# artifact with its own job below, so this needs no Node toolchain.
run: |
CGO_ENABLED=0 go build -trimpath -o /tmp/amld ./cmd/amld
test -x /tmp/amld
- name: It serves, and it refuses what it should
# Two facts worth a check on every commit, because both have regressed
# before: the process comes up and answers, and an unauthenticated
# request is refused rather than served somebody's tenant data.
#
# AML_CLIENT_ID is required — the daemon refuses to start without it,
# because it is the audience every token is pinned to and a plane that
# cannot check the audience authenticates any first-party token. A real
# bearer is a brand-issued IAM token CI cannot mint, so the smoke checks
# what needs no token: it serves, and an unauthenticated read is refused.
#
# The key is hex from od/tr, and its length is asserted before use. It was
# xxd, which is not in the runner image; the substitution failed to an
# EMPTY key and nothing caught it, because an env prefix on a backgrounded
# command reports the status of the async list, not of the substitution.
# The smoke ran green for weeks against a daemon holding no key.
run: |
set -euo pipefail
dir=$(mktemp -d)
key=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
[ ${#key} -eq 64 ] || { echo "::error::token key is ${#key} hex chars, want 64"; exit 1; }
AML_TENANT_HEADER=X-Org-Id \
AML_CLIENT_ID=aml-ci-smoke \
AML_TOKEN_KEY="$key" \
/tmp/amld serve --dir "$dir" --http 127.0.0.1:8791 --dev=false &
pid=$!
for i in $(seq 1 60); do
curl -sf -o /dev/null http://127.0.0.1:8791/v1/aml/health && break
sleep 1
done
curl -sf -o /dev/null http://127.0.0.1:8791/v1/aml/health \
|| { echo "::error::amld never answered /v1/aml/health"; kill $pid; exit 1; }
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8791/v1/aml/cases)
kill $pid
if [ "$code" != "401" ]; then
echo "::error::an unauthenticated read of /v1/aml/cases returned $code, want 401"
exit 1
fi
echo "serves, and refuses an unauthenticated read"
console:
runs-on: [linux-amd64]
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Install from the lockfile
# `npm ci` fails on drift rather than resolving fresh, so what CI builds
# is what the image builds.
run: npm --prefix ui ci
- name: Typecheck
# The console's own script, the same way every other step here reaches it.
# `npm --prefix ui exec tsc -- --noEmit` ran the compiler from the REPO
# root, where there is no tsconfig, so tsc printed its help and exited 1 —
# this job has never once passed, and the browser suite below it therefore
# never ran in CI at all.
run: npm --prefix ui run typecheck
- name: Build the bundle
run: |
npm --prefix ui run build
test -f ui/dist/index.html
- name: The console under the CSP it is served with, signed in
# The policy in ui/csp.txt is the one charts/app/values/hanzo/aml-ui.yaml
# sets on the static server, and e2e/serve.mjs sends it verbatim. The kit
# styles by inserting rules at runtime, which is the thing a strict
# style-src refuses, so whether the arrangement in ui/src/gui.ts holds is
# a question only a browser can answer — and the signed-in tree is where
# the six screens that do the styling live.
#
# The browser comes from the console's own script. It was `npm --prefix ui
# exec playwright install --with-deps chromium`, and npm's parser ate
# `--with-deps` before playwright saw it: the browser downloaded, its
# shared libraries did not, and every test that opened a page died on
# libnspr4.so — 7 of 12, the whole signed-in suite, on the first run this
# job ever reached. Same shape as the tsc call above. `npm run` hands a
# script's arguments to the script, so the package states the command once
# and CI has no second copy to get wrong.
run: |
npm --prefix ui run e2e:browser
npm --prefix ui run e2e