ci: add Renovate configuration for automated dependency updates #60
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: | |
| 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, ',') }}" |