Skip to content

CI

CI #2710

Workflow file for this run

name: CI
on:
merge_group:
pull_request:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
partition: [1/3, 2/3, 3/3]
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: taiki-e/install-action@v2.86.1
with:
tool: cargo-nextest,just
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v7.0.0
with:
node-version: lts/*
cache: npm
cache-dependency-path: private-web/package-lock.json
# The private-server tests serve the embedded React SPA via rust-embed,
# so they need private-web/dist/ to exist. `just test` sets
# SKIP_FRONTEND_BUILD=1 (build.rs won't build it) and a fresh checkout has
# no dist/, so build it explicitly here. Dev machines already have a dist/
# from working on the frontend.
- name: Build frontend
run: npm ci && npm run build
working-directory: private-web
# `just test` runs nextest against a throwaway tmpfs Postgres that
# scripts/ramdisk-pg.sh spins up with the runner's own initdb/pg_ctl
# (preinstalled on ubuntu-latest), so no system Postgres service or
# role/db setup is needed here — same path developers run locally.
- run: just test --partition hash:${{ matrix.partition }}
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup component add clippy
- uses: taiki-e/install-action@v2.86.1
with:
tool: just
- uses: Swatinem/rust-cache@v2
- run: just lint
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup component add rustfmt
- uses: taiki-e/install-action@v2.86.1
with:
tool: just
- run: just fmt-check
e2e:
name: Playwright
runs-on: ubuntu-latest
# The fixture (private-web/e2e/fixture.ts) spawns the private-server
# binary plus a Vite dev server itself, so this job builds the rust
# binaries the fixture expects, installs the frontend deps + chromium,
# and runs `npm run test:e2e`.
#
# Sharded across 4 parallel jobs via Playwright's --shard; each shard
# runs a quarter of the tests. Artifacts are suffixed with the shard
# index so uploads don't collide.
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v7.0.0
with:
node-version: lts/*
cache: npm
cache-dependency-path: private-web/package-lock.json
- name: Build private-server + migrate
# SKIP_FRONTEND_BUILD avoids private-server/build.rs running an npm
# install + vite build to embed dist/ — the e2e fixture uses Vite
# at runtime so the embedded bundle isn't needed here.
env:
SKIP_FRONTEND_BUILD: "1"
run: cargo build --bin private-server --bin migrate --locked
- name: npm ci
run: npm ci
working-directory: private-web
# Frontend unit tests (vitest). They need no browser or database, so
# they run here before the expensive Playwright setup and fail fast.
- name: Frontend unit tests
run: npm run test
working-directory: private-web
- name: Cache Playwright browsers
uses: actions/cache@v6.1.0
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('private-web/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
working-directory: private-web
- name: Playwright tests
# The wrapper spins up a throwaway tmpfs Postgres and exports
# CANOPY_E2E_ADMIN_DATABASE_URL pointing at it; the fixture creates its
# per-worker databases on that cluster. Same DB harness as `just test`.
run: ../scripts/ramdisk-pg.sh npm run test:e2e -- --shard=${{ matrix.shard }}/4
working-directory: private-web
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-${{ matrix.shard }}
path: |
private-web/playwright-report
private-web/test-results
retention-days: 7
if-no-files-found: ignore
- name: Upload Playwright screenshots
# Every test (pass or fail) takes an end-of-test screenshot (see
# playwright.config.ts `use.screenshot`), so this always uploads,
# not just on failure, to make visual review of a PR's UI trivial.
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-screenshots-${{ matrix.shard }}
path: private-web/test-results/**/*.png
retention-days: 5
if-no-files-found: ignore
generated:
name: Generated files
runs-on: ubuntu-latest
# The committed OpenAPI documents and the generated API client must be what
# the current code produces. Regenerating needs no database and no network:
# the documents come from compile-time annotations and the client comes from
# the committed public document.
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: taiki-e/install-action@v2.86.1
with:
tool: just
- uses: Swatinem/rust-cache@v2
- run: just check-generated
- run: just check-api-deps
semver:
name: API compatibility
runs-on: ubuntu-latest
# The public API stays compatible unless a break is coordinated, and
# `.workhorse/specs/platform/api-compatibility.md` makes that mechanical: the
# published client generated after a change must not be a semver-breaking
# change against the one generated before.
#
# cargo-semver-checks reads the bump in the crate's manifest (stamped from the
# document's info.version) against what is on crates.io, so an uncoordinated
# break fails here, while a break whose major has been raised in the document
# passes — that raise being where the coordination is recorded.
steps:
- name: Free up space
run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup || true
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: taiki-e/install-action@v2.86.1
with:
tool: cargo-semver-checks,just
- uses: Swatinem/rust-cache@v2
- run: just semver-checks
api-crate:
name: API client
runs-on: ubuntu-latest
# The published client is a cargo project of its own (see release-plz.toml), so
# the workspace jobs above do not build, lint, format-check or test it. Without
# this, the one crate with outside consumers is the one nothing checks.
steps:
- uses: actions/checkout@v7.0.1
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable --component clippy,rustfmt
rustup default stable
- uses: taiki-e/install-action@v2.86.1
with:
tool: just
- uses: Swatinem/rust-cache@v2
- run: just check-api-crate
# Dummy job to have a stable name for PR requirements
tests-pass:
if: always() # always run even if dependencies fail
name: Tests pass
needs:
- test
- clippy
- fmt
- e2e
- generated
- semver
- api-crate
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}