Skip to content

ci: add Renovate configuration for automated dependency updates #60

ci: add Renovate configuration for automated dependency updates

ci: add Renovate configuration for automated dependency updates #60

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
# Cancel superseded runs on the same ref to save CI minutes. Only cancel on PRs —
# never cancel an in-flight run on main, which could be the one catching a regression.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Least privilege: jobs only read the repo (setup-protoc gets the token it needs).
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# CI-only: skip debuginfo in dev/test builds. Cuts compile+link time ~30-40%
# and shrinks caches; the only cost is line numbers in CI backtraces.
CARGO_PROFILE_DEV_DEBUG: 0
jobs:
# Decide which areas changed so we only run the jobs that matter. Pure doc edits
# (README, CLAUDE.md, AGENTS.md, LICENSE, .agents/**, .claude/**) match no filter,
# so every build job below is skipped while the `ci-status` gate stays green.
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
rust: ${{ steps.filter.outputs.rust }}
dashboard: ${{ steps.filter.outputs.dashboard }}
web: ${{ steps.filter.outputs.web }}
other: ${{ steps.filter.outputs.other }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
rust:
- 'apps/backend/**'
- 'xtask/**'
- 'libs/proto/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'clippy.toml'
- 'rustfmt.toml'
- 'deny.toml'
- '.github/workflows/**'
dashboard:
- 'apps/dashboard/**'
- 'package.json'
- 'bun.lock'
- 'nx.json'
- '.github/workflows/**'
web:
- 'apps/web/**'
- 'package.json'
- 'bun.lock'
- 'nx.json'
- '.github/workflows/**'
# Catch-all: anything that is NOT purely docs/agent files. Guards against
# the default-skip trap where a change matching none of the area filters
# above (e.g. a new Dockerfile or top-level script) would silently skip all
# build jobs while ci-status stays green. The rust lanes treat `other` as a
# baseline trigger, so non-doc changes always compile + test the core.
other:
- '!**/*.md'
- '!.claude/**'
- '!.agents/**'
- '!LICENSE'
- '!AGENTS.md'
lint:
name: Rust lint (fmt + clippy)
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: apps/backend
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Toolchain (channel + rustfmt/clippy components) comes from rust-toolchain.toml.
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master
with:
toolchain: "1.94"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Workspace root is the REPO root (Cargo.lock + target/ live there);
# cargo just runs from apps/backend. Pointing rust-cache at the member
# dir would cache the nonexistent apps/backend/target.
workspaces: ". -> target"
# Lint has its own key: GH caches are immutable and clippy saves
# check-artifacts that are useless for `cargo test` codegen, so
# sharing a key with the test lanes lets whichever job finishes first
# (always lint) poison the cache for the others.
shared-key: linux-lint
cache-on-failure: true
- run: cargo fmt --all -- --check
# --no-default-features drops `embed-dashboard`, whose RustEmbed derive needs
# apps/dashboard/dist at compile time (not built in CI).
- run: cargo clippy --no-default-features --all-targets --locked -- -D warnings
security:
name: Supply-chain (cargo-deny)
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2
with:
command: check
test:
name: Tests (${{ matrix.db }})
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: apps/backend
strategy:
fail-fast: false
matrix:
# SQLite runs fully parallel (in-memory, isolated per test). Postgres and
# MySQL run database-per-test against one shared server, so concurrent
# tests are capped to keep the connection demand well under the server's
# ceiling on high-core runners.
include:
- db: sqlite
test_database: sqlite
test_database_url: ""
test_args: ""
- db: postgres
test_database: postgres
test_database_url: postgres://postgres:postgres@localhost:5432/postgres
test_args: "--test-threads 4"
- db: mysql
test_database: mysql
test_database_url: mysql://root:root@localhost:3306/mysql
test_args: "--test-threads 4"
env:
TEST_DATABASE: ${{ matrix.test_database }}
TEST_DATABASE_URL: ${{ matrix.test_database_url }}
# Both services run for every lane; the sqlite lane simply ignores them
# (GitHub Actions can't attach services conditionally per matrix value).
# Service containers are Linux-only, which is why Windows/macOS live in the
# separate `test-os` job (SQLite only).
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 2s
--health-timeout 5s
--health-retries 15
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot"
--health-interval 2s
--health-timeout 5s
--health-retries 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master
with:
toolchain: "1.94"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Workspace root is the REPO root (Cargo.lock + target/ live there).
workspaces: ". -> target"
# All 3 db lanes compile an identical binary (db is chosen at runtime
# via TEST_DATABASE), so one shared cache serves all of them; the
# save race between lanes is harmless. NOT shared with lint: clippy
# saves check-artifacts that are useless for test codegen.
shared-key: linux-test
cache-on-failure: true
- uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7
with:
tool: cargo-nextest
# nextest runs all test binaries' tests in ONE parallel pool (cargo test
# runs the binaries sequentially). Process-per-test also isolates env-var
# mutation between tests.
# --no-default-features drops `embed-dashboard` (RustEmbed needs a built
# apps/dashboard/dist at compile time, which CI doesn't produce).
- name: cargo nextest
run: cargo nextest run --no-default-features --locked ${{ matrix.test_args }}
# Cross-platform coverage. The binary ships on Windows/macOS/Linux, so the suite
# must run off Linux too. Service containers don't run on these runners, so only
# the SQLite lane (in-memory, no external server) is exercised here.
test-os:
name: Tests (${{ matrix.os }}, sqlite)
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
working-directory: apps/backend
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
env:
TEST_DATABASE: sqlite
TEST_DATABASE_URL: ""
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master
with:
toolchain: "1.94"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Workspace root is the REPO root (Cargo.lock + target/ live there).
workspaces: ". -> target"
shared-key: ${{ matrix.os }}
cache-on-failure: true
- uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7
with:
tool: cargo-nextest
# --no-default-features drops `embed-dashboard` (no built dashboard/dist in CI).
- name: cargo nextest
run: cargo nextest run --no-default-features --locked
frontend:
name: Frontend (dashboard)
needs: changes
if: needs.changes.outputs.dashboard == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- run: bun install --frozen-lockfile
# Persist the Nx local cache so dashboard:build replays across runs.
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .nx/cache
key: nx-${{ runner.os }}-${{ hashFiles('apps/dashboard/**', 'bun.lock', 'package.json', 'nx.json') }}
restore-keys: |
nx-${{ runner.os }}-
- name: Biome CI
run: bunx biome ci
working-directory: apps/dashboard
- name: Typecheck
run: bunx nx run dashboard:typecheck
- name: Build
run: bunx nx run dashboard:build
packaging:
name: Packaging checks
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master
with:
toolchain: "1.94"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: ". -> target"
shared-key: linux-packaging
cache-on-failure: true
- name: xtask tests
run: cargo test -p xtask --locked
- name: package dry-run
run: |
cargo run -p xtask --locked -- package --kind all --version 0.0.0-ci --dry-run --skip-build --target-os linux --arch amd64
cargo run -p xtask --locked -- package --kind host --version 0.0.0-ci --dry-run --skip-build --target-os macos --arch arm64
cargo run -p xtask --locked -- package --kind host --version 0.0.0-ci --dry-run --skip-build --target-os windows --arch amd64
web:
name: Web (landing + docs)
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- run: bun install --frozen-lockfile
# Persist the Nx local cache so web:build replays across runs.
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .nx/cache
key: nx-${{ runner.os }}-${{ hashFiles('apps/web/**', 'bun.lock', 'package.json', 'nx.json') }}
restore-keys: |
nx-${{ runner.os }}-
- name: Biome CI
run: bunx biome ci
working-directory: apps/web
- name: Typecheck
run: bunx nx run web:typecheck
- name: Build
run: bunx nx run web:build
# Single required status check. Branch protection should require ONLY this job.
# It passes when every needed job either succeeded or was skipped (path filter),
# and fails if any of them actually failed or was cancelled. This avoids the
# "skipped required check stays pending forever" trap of naive path filtering.
ci-status:
name: CI status
needs: [changes, lint, security, test, test-os, frontend, packaging, web]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify no job failed
run: |
if echo '${{ join(needs.*.result, ',') }}' | grep -Eq 'failure|cancelled'; then
echo "A required job failed or was cancelled: ${{ join(needs.*.result, ',') }}"
exit 1
fi
echo "All jobs succeeded or were skipped: ${{ join(needs.*.result, ',') }}"