Skip to content

Declare certification compiler and report failed checks #3540

Declare certification compiler and report failed checks

Declare certification compiler and report failed checks #3540

Workflow file for this run

on:
push:
workflow_dispatch:
inputs:
e2e_mode:
description: Playwright suite. auto = light on feature branches, full on develop and v* tags.
type: choice
options:
- auto
- light
- full
default: auto
# One pipeline at a time on the shared runner. Mancave picks up several jobs
# at once, and a full suite is 4 shards x 2 browsers plus four debug servers
# next to clippy and nextest; two of those overlapping is what turns the e2e
# suite red run to run (reload-heavy specs time out waiting for the server).
# Queue instead of overlap; never cancel a run that is already going.
concurrency:
group: main-pipeline
# The default keeps only one pending run and replaces it on the next push.
queue: max
cancel-in-progress: false
# Main prefers Mancave (self-hosted) so Dagger cache volumes stay warm.
# GitHub-hosted runs ONLY when Mancave is unavailable — never as a retry
# after a red test suite.
#
# Flow:
# 1. Pick runner — online self-hosted? (optional CI_RUNNER_STATUS_TOKEN)
# 2. Main (Mancave) XOR Main (GitHub) — one path, not failure-failover
# 3. Main — aggregator for the required status check
#
# Availability check needs a PAT that can list repo runners (GITHUB_TOKEN
# cannot). Store it as Actions secret CI_RUNNER_STATUS_TOKEN. Without that
# secret we assume Mancave is up (same as before) and will not fall back
# when tests fail.
#
# Escape hatch (force hosted, skip Mancave):
# Settings -> Secrets and variables -> Actions -> Variables
# CI_RUNNER = ["ubuntu-latest"]
#
# Only this workflow uses the self-hosted runner. Releasing/deploying stay
# on ubuntu-latest — a release must not depend on one PC being awake.
#
# SECURITY: pull_request / pull_request_target are forced onto hosted
# runners (never Mancave).
name: "Main pipeline: build, lint, test"
jobs:
pick:
name: Pick runner
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
host: ${{ steps.pick.outputs.host }}
e2e-mode: ${{ steps.e2e.outputs.mode }}
steps:
- id: pick
env:
GH_TOKEN: ${{ secrets.CI_RUNNER_STATUS_TOKEN }}
FORCE_RUNNER: ${{ vars.CI_RUNNER }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "pull_request" ] || [ "$EVENT_NAME" = "pull_request_target" ]; then
echo "PR event — forcing GitHub-hosted (never run untrusted code on Mancave)"
echo "host=hosted" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$FORCE_RUNNER" = '["ubuntu-latest"]' ]; then
echo "CI_RUNNER escape hatch — forcing GitHub-hosted"
echo "host=hosted" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "${GH_TOKEN:-}" ]; then
echo "No CI_RUNNER_STATUS_TOKEN — assuming Mancave online"
echo "host=mancave" >> "$GITHUB_OUTPUT"
exit 0
fi
# Any online runner carrying the self-hosted label counts as
# available (busy is fine — the job will wait its turn).
# On API errors (bad/insufficient PAT), prefer Mancave — do NOT
# treat a 403 as "offline" or we skip the warm box forever.
set +e
body=$(gh api "repos/$REPO/actions/runners" 2>/tmp/runner-api.err)
api_status=$?
set -e
if [ "$api_status" -ne 0 ]; then
echo "Runner API failed (is CI_RUNNER_STATUS_TOKEN a fine-grained PAT with Repository Administration: Read?):"
cat /tmp/runner-api.err || true
echo "Assuming Mancave online"
echo "host=mancave" >> "$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$body" | jq -e \
'any(.runners[]; .status == "online" and any(.labels[]; .name == "self-hosted"))' \
>/dev/null; then
echo "Self-hosted runner online — using Mancave"
echo "host=mancave" >> "$GITHUB_OUTPUT"
else
echo "No online self-hosted runner — falling back to GitHub-hosted"
echo "host=hosted" >> "$GITHUB_OUTPUT"
fi
- id: e2e
env:
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
DISPATCH_MODE: ${{ github.event.inputs.e2e_mode }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# Light = @smoke Playwright. Full = every spec.
# Default full on develop/tags so staging/production never ship on
# a subset. Feature-branch pushes are light unless opted in.
mode=light
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${DISPATCH_MODE:-auto}" = "full" ]; then
mode=full
elif [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${DISPATCH_MODE:-auto}" = "light" ]; then
mode=light
elif [ "$REF" = "refs/heads/develop" ]; then
mode=full
elif [[ "$REF" == refs/tags/v* ]]; then
mode=full
elif printf '%s' "${COMMIT_MSG:-}" | grep -q '\[full-e2e\]'; then
mode=full
elif prs=$(gh api "repos/$REPO/commits/$SHA/pulls" -H "Accept: application/vnd.github+json" 2>/dev/null); then
if printf '%s' "$prs" | jq -e 'any(.[]; any(.labels[]; .name == "full-e2e"))' >/dev/null; then
mode=full
fi
fi
echo "e2e-mode=$mode (ref=$REF dispatch=${DISPATCH_MODE:-} event=$EVENT_NAME)"
echo "mode=$mode" >> "$GITHUB_OUTPUT"
mancave:
name: Main (Mancave)
needs: pick
if: needs.pick.outputs.host == 'mancave'
uses: ./.github/workflows/main-ci.yml
# `packages: write` lets the GITHUB_TOKEN push images to GHCR. An explicit
# permissions block replaces the defaults, so `contents: read` (checkout)
# has to come along.
permissions:
contents: read
packages: write
with:
runner: ${{ vars.CI_RUNNER || '["self-hosted", "Linux", "X64"]' }}
artifact-name: build-artifacts-mancave
host-profile: mancave
e2e-mode: ${{ needs.pick.outputs.e2e-mode }}
secrets:
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}
# Does this commit still build the closed-source control plane?
#
# atomic-saas depends on `../atomic-server/lib` and `../atomic-server/server`
# by path and used to pin the commit it built against, bumping the pin in a
# PR of its own after every change here — nineteen times between July and
# September 2026. Its CI now builds against develop head instead, so a lib
# rename that nobody carried over would surface there, days later. This job
# moves that failure to the PR that made it.
#
# Paired-PR rule: if atomic-saas has a branch named like this one, that is
# what gets built (a change that needs both repos lands as two PRs on one
# branch name); otherwise main. atomic-saas's CI applies the same rule in
# reverse.
#
# Mancave only. The token can read a private repo, and fork PRs never run
# here; PR events are routed to GitHub-hosted by `pick`, which also keeps
# this job off them. `clean: false` keeps both target/ dirs warm, the same
# measured reason atomic-saas's own CI gives.
downstream-saas:
name: Downstream (atomic-saas builds against this commit)
needs: pick
if: >-
needs.pick.outputs.host == 'mancave' &&
github.event_name != 'pull_request' &&
github.event_name != 'pull_request_target'
runs-on: ${{ fromJSON(vars.CI_RUNNER || '["self-hosted", "Linux", "X64"]') }}
timeout-minutes: 30
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Nothing here runs the dist in a browser; the embed only needs to exist.
SKIP_WASM_BUILD: "1"
steps:
- name: Checkout atomic-server (this commit)
uses: actions/checkout@v4
with:
path: atomic-server
clean: false
- name: Pick the atomic-saas branch
id: saas
env:
GH_TOKEN: ${{ secrets.ATOMIC_SAAS_DISPATCH_TOKEN }}
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::ATOMIC_SAAS_DISPATCH_TOKEN is not set; cannot read ontola/atomic-saas."
exit 1
fi
ref=main
source="main"
if [ "$BRANCH" != "develop" ] && gh api "repos/ontola/atomic-saas/branches/$BRANCH" >/dev/null 2>&1; then
ref="$BRANCH"
source="paired branch '$BRANCH'"
fi
sha=$(gh api "repos/ontola/atomic-saas/commits/$ref" -q .sha)
echo "atomic-saas $sha ($source)"
echo "ref=$sha" >> "$GITHUB_OUTPUT"
echo "### Downstream: atomic-saas" >> "$GITHUB_STEP_SUMMARY"
echo "Built \`$sha\` ($source) against this commit." >> "$GITHUB_STEP_SUMMARY"
- name: Checkout atomic-saas
uses: actions/checkout@v4
with:
repository: ontola/atomic-saas
ref: ${{ steps.saas.outputs.ref }}
token: ${{ secrets.ATOMIC_SAAS_DISPATCH_TOKEN }}
path: atomic-saas
clean: false
# Nothing below pushes to atomic-saas, so the PAT has no business
# staying in atomic-saas/.git/config on the self-hosted runner
# (where `clean: false` keeps the checkout around between runs).
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@1.98.1
with:
components: clippy
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.15.1
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 22
# Both frontends are compile-time embeds in atomic-saas (RustEmbed), so
# a `cargo check` needs them on disk; contents are irrelevant here.
- name: Build the data-browser embed
working-directory: atomic-server/browser
run: |
pnpm install --frozen-lockfile
pnpm run build
- name: Build the portal embed
working-directory: atomic-saas/portal
run: |
npm ci
npm run build
# Not `--locked`: this commit may have changed atomic_lib's own
# dependencies, which legitimately moves atomic-saas's lockfile. What is
# being checked is that the code still builds and passes; a lock update
# is reported so the saas side commits it on its next touch.
- name: cargo check, clippy and test atomic-saas against this commit
working-directory: atomic-saas
run: |
set -euo pipefail
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
# The managed node embeds this repo's server crate and is not a
# default workspace member, so the root build does not touch it.
# Its own CI runs check, clippy and test; the same here.
cargo clippy -p atomic-managed-node --all-targets -- -D warnings
cargo test -p atomic-managed-node
if ! git diff --quiet -- Cargo.lock; then
git diff --stat -- Cargo.lock
echo "::warning::atomic-saas's Cargo.lock drifted against this commit; run cargo check there and commit Cargo.lock."
fi
# Hosted only when pick said Mancave is unavailable (or escape/PR).
# Deliberately does NOT run when Mancave finished with test failures.
github:
name: Main (GitHub)
needs: pick
if: needs.pick.outputs.host == 'hosted'
uses: ./.github/workflows/main-ci.yml
permissions:
contents: read
packages: write
with:
runner: '["ubuntu-latest"]'
artifact-name: build-artifacts-github
host-profile: hosted
e2e-mode: ${{ needs.pick.outputs.e2e-mode }}
secrets:
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}
Main:
name: Main
needs: [pick, mancave, github, downstream-saas]
if: always() && needs.pick.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Resolve runner outcome
run: |
host="${{ needs.pick.outputs.host }}"
m="${{ needs.mancave.result }}"
g="${{ needs.github.result }}"
d="${{ needs.downstream-saas.result }}"
echo "host=$host mancave=$m github=$g downstream-saas=$d"
# The downstream build is skipped on PR events and on hosted runs;
# when it ran, it has to have passed.
if [ "$d" = "failure" ] || [ "$d" = "cancelled" ]; then
echo "atomic-saas no longer builds against this commit — fix it there on a branch with this name, or here."
exit 1
fi
if [ "$host" = "mancave" ] && [ "$m" = "success" ]; then
exit 0
fi
if [ "$host" = "hosted" ] && [ "$g" = "success" ]; then
exit 0
fi
exit 1