Skip to content

Merge pull request #127 from thejefflarson/dependabot/github_actions/… #296

Merge pull request #127 from thejefflarson/dependabot/github_actions/…

Merge pull request #127 from thejefflarson/dependabot/github_actions/… #296

Workflow file for this run

name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
server:
# Self-hosted watcher-runners (meshed) so the cargo build uses the shared
# in-cluster sccache redis. The runners now carry a postgres sidecar (cluster
# repo: charts/actions/runners/values-watcher.yaml) reachable at localhost:5432,
# so the ARC-can't-run-a-service-container reason for staying hosted is gone.
runs-on: watcher-runners
env:
DATABASE_URL: postgres://watcher:watcher@localhost:5432/watcher
# sccache -> the shared Cloudflare R2 bucket (cluster repo: charts/sccache,
# ADR-0020). The backend config -- SCCACHE_BUCKET/ENDPOINT/REGION plus the
# bucket-scoped S3 token -- is injected into the runner POD from the `sccache-r2`
# Secret via envFrom, so none of it belongs in this workflow and no credential is
# committed here.
#
# The redis backend is deliberately NOT set here any more (JEF-564): sccache selects
# ONE backend from its env, so setting both would make which one you get an
# implementation detail of sccache's precedence -- the R2 cutover could read as done
# while every build still went to redis. If the pod env is missing, sccache degrades
# to a local disk cache: slower, green, and obvious.
# CARGO_INCREMENTAL=0 -- sccache can't cache incremental artefacts.
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
defaults:
run:
working-directory: server
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-targets: false
workspaces: server
# Stable key shared across branches/PRs (not the per-branch default), so a
# PR's first run restores main's warm target/ instead of compiling cold.
shared-key: watcher-server
- name: Install sccache
# --force: rust-cache restores cargo's "already installed" metadata on a warm
# cache, but not the binary, so a plain binstall skips and sccache is absent
# from PATH (RUSTC_WRAPPER then fails). Force a real install every run.
run: cargo binstall --no-confirm --force sccache
# The postgres sidecar boots in parallel with the runner container; block until
# it accepts TCP before building/testing. bash /dev/tcp avoids needing a client.
- name: Wait for postgres sidecar
run: |
for i in $(seq 1 60); do
(echo > /dev/tcp/127.0.0.1/5432) >/dev/null 2>&1 && { echo "postgres up after ${i}s"; exit 0; }
sleep 1
done
echo "postgres sidecar never came up" >&2; exit 1
- run: cargo fmt --check
# clippy replaces a standalone `cargo build`: it's check-speed (no codegen) and
# gates the compile too, while `cargo test` below does the actual codegen. Only
# the workspace crate runs through clippy-driver; deps still compile via sccache.
# -D warnings matches the repo's warnings-as-errors policy.
- run: cargo clippy --locked --all-targets -- -D warnings
- run: cargo test --locked # ingest->query integration test against the sidecar DB
- name: sccache stats
if: always()
run: sccache --show-stats
ui:
runs-on: watcher-runners
defaults:
run:
working-directory: ui
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: ui/package-lock.json
- run: npm ci
# Accessibility floor (JEF-431): eslint-plugin-jsx-a11y gates keyboard-
# operable rows, labeled charts, and no silent-live tables from regressing.
- run: npm run lint
# Runtime a11y route-smoke (JEF-442) runs inside `npm test`: each top-level
# route is mounted (jsdom, API mocked) and scanned by axe-core, failing on
# serious/critical structural violations. Complements the static lint above —
# it catches ARIA that only resolves against the rendered tree.
- run: npm test
- run: npm run build
# One image now: the server builds the UI and embeds it (rust-embed), so there
# is no separate watcher-ui image. Build context is the repo root.
#
# Native per-arch on the self-hosted runner (watcher-runners), offloading to the
# two in-cluster BuildKit builders — amd64 on `buildkit` (node-3), arm64 on
# `buildkit-arm64` (an arm64 Pi) — each building NATIVELY, no QEMU. BuildKit
# enforces mTLS, so we point the remote driver at the client cert the runner
# already mounts (BUILDKIT_TLS_DIR) and verify each endpoint against its
# servername; without the cert the gRPC handshake is rejected. The build also pushes a layer cache to GHA
# (cache-from/to type=gha): paired with cargo-chef in the Dockerfile, the dep
# compile becomes a portable layer any node can pull — unlike the per-node
# BuildKit daemon cache, which a cold node can't reach.
image:
needs: [server, ui]
if: github.ref == 'refs/heads/main'
runs-on: watcher-runners
permissions:
contents: read
packages: write
id-token: write # keyless cosign signing (OIDC token -> Sigstore Fulcio)
attestations: write # Sigstore-signed SLSA build-provenance attestation (protector verifies it)
steps:
- uses: actions/checkout@v7
- name: Connect to BuildKit (mTLS, native per-arch)
# Two native builders share one CA (cluster: charts/actions/buildkit +
# values-arm64.yaml): amd64 on `buildkit` (node-3), arm64 on `buildkit-arm64`
# (an arm64 Pi). buildx --append joins them into one builder so each platform
# builds NATIVELY and buildx merges the manifest — no QEMU (arm64 Rust under
# QEMU ran for hours). The runner's existing client cert (BUILDKIT_TLS_DIR)
# authenticates to both; each endpoint verifies its own server SAN via servername.
env:
BUILDKIT_ARM64_HOST: tcp://buildkit-arm64.dev.svc.cluster.local:1234
BUILDKIT_ARM64_TLS_SERVER_NAME: buildkit-arm64.dev.svc.cluster.local
run: |
for i in 1 2 3 4 5; do
docker buildx rm watcher-remote >/dev/null 2>&1 || true
if docker buildx create --name watcher-remote --use \
--driver remote --platform linux/amd64 \
--driver-opt cacert="$BUILDKIT_TLS_DIR/ca.crt",cert="$BUILDKIT_TLS_DIR/tls.crt",key="$BUILDKIT_TLS_DIR/tls.key",servername="$BUILDKIT_TLS_SERVER_NAME" \
"$BUILDKIT_HOST" \
&& docker buildx create --name watcher-remote --append \
--driver remote --platform linux/arm64 \
--driver-opt cacert="$BUILDKIT_TLS_DIR/ca.crt",cert="$BUILDKIT_TLS_DIR/tls.crt",key="$BUILDKIT_TLS_DIR/tls.key",servername="$BUILDKIT_ARM64_TLS_SERVER_NAME" \
"$BUILDKIT_ARM64_HOST" \
&& timeout 90 docker buildx inspect --bootstrap; then
echo "connected to buildkit (amd64 + arm64, native)"; exit 0
fi
echo "buildkit connect failed (attempt $i/5), retrying…"; sleep 10
done
echo "could not reach buildkit"; exit 1
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v6
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/watcher-server
tags: |
type=raw,value=latest
type=sha
- uses: docker/build-push-action@v7
id: build
with:
context: .
file: server/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Portable layer cache on GitHub's Actions Cache (~10GB/repo, free). With
# cargo-chef the dep-compile layer cache-hits here until Cargo.lock moves,
# so a cold node skips it. mode=max also caches intermediate (build-stage)
# layers, which is what carries the cook layer — at the cost of a larger
# WAN transfer (see JEF-87 / measure in JEF-82).
# sccache's R2 backend for the in-image cargo build (JEF-584, ADR-0020).
# `secret-envs` (key=envname) reads these straight out of the RUNNER POD's
# env, where the `sccache-r2` Secret is injected via envFrom (cluster repo:
# charts/actions/runners/values-watcher.yaml) — the repo has no Actions secret
# for R2, and this needs no lift step or on-disk secret file. Secrets, not
# build-args: a build-arg persists in `docker history` on every pushed image.
# Unset (e.g. the Secret is missing) -> the mounts are empty and
# scripts/start-sccache-docker.sh degrades to a local disk cache rather than
# failing the build.
secret-envs: |
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY
SCCACHE_BUCKET=SCCACHE_BUCKET
SCCACHE_ENDPOINT=SCCACHE_ENDPOINT
SCCACHE_REGION=SCCACHE_REGION
SCCACHE_S3_KEY_PREFIX=SCCACHE_S3_KEY_PREFIX
SCCACHE_S3_USE_SSL=SCCACHE_S3_USE_SSL
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- uses: sigstore/cosign-installer@v3
- name: Sign the image (keyless)
# Sign by immutable digest, not tag. Keyless: GitHub's OIDC token is
# exchanged for a short-lived Fulcio cert and the signature is recorded in
# the Rekor transparency log — no private key to store. cosign reuses the
# docker login above to push the signature to GHCR.
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: cosign sign --yes "ghcr.io/${{ github.repository_owner }}/watcher-server@${DIGEST}"
# Sigstore-signed SLSA build-provenance attestation (keyless: Fulcio + Rekor), pushed as an
# OCI referrer — what protector's provenance observer verifies to "provenance-verified".
- name: Attest build provenance (Sigstore-signed SLSA)
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-name: ghcr.io/${{ github.repository_owner }}/watcher-server
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true