Align compiler dependency with workspace override #3541
Workflow file for this run
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
| 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 |