Merge pull request #127 from thejefflarson/dependabot/github_actions/… #296
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |