diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..8025201ee --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +[build] +# Keep intermediate artifacts outside the checkout so every clone and worktree +# on a machine shares one copy instead of each rebuilding the dependency graph. +# {cargo-cache-home} expands to CARGO_HOME, so this is portable: no absolute +# path, no assumption about where the repo lives. Cargo does not expand ~ or +# $HOME, and target-dir does not support templating, so build-dir is the only +# form that gets this. +# +# Final artifacts still land in /target, so anything locating a built +# binary by path keeps working. +build-dir = "{cargo-cache-home}/build/browseros" diff --git a/.github/workflows/rust-cache-bust.yml b/.github/workflows/rust-cache-bust.yml new file mode 100644 index 000000000..2549b9e0a --- /dev/null +++ b/.github/workflows/rust-cache-bust.yml @@ -0,0 +1,69 @@ +name: Rust Cache Bust + +# The shared cargo build directory is cached as an extra `cache-directories` +# entry, and rust-cache prunes only workspace target dirs, never extra ones. So +# that entry is cached wholesale and accumulates artifacts for dependencies and +# profiles that no longer exist. Left alone it grows against the repository's +# 10GB Actions cache allowance and evicts entries that are still wanted. +# +# Dropping it on a schedule keeps that bounded. The next warm run rebuilds it +# from scratch, so the only cost is one cold Rust compile. +on: + schedule: + # 1st and 15th, so roughly fortnightly. Ahead of the Monday 06:00 warm run + # rather than alongside it, and the warm workflow is dispatched below in any + # case so a fresh cache does not wait for the next merge. + - cron: "0 5 1,15 * *" + workflow_dispatch: + +permissions: + actions: write + +concurrency: + group: rust-cache-bust + cancel-in-progress: false + +jobs: + bust: + name: Drop Rust caches + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Delete Rust caches + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + # rust-cache prefixes every key it writes with `prefix-key`, which is + # left at its default. Matching on that keeps Turbo and any other + # cache in this repository untouched. + ids="$(gh api --paginate "/repos/$REPO/actions/caches?per_page=100" \ + --jq '.actions_caches[] | select(.key | startswith("v0-rust")) | .id')" + + if [ -z "$ids" ]; then + echo "No Rust caches to drop." + echo "No Rust caches to drop." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + count=0 + while read -r id; do + [ -n "$id" ] || continue + gh api -X DELETE "/repos/$REPO/actions/caches/$id" + count=$((count + 1)) + done <<< "$ids" + + echo "Dropped $count Rust cache entries." + echo "Dropped $count Rust cache entries." >> "$GITHUB_STEP_SUMMARY" + + - name: Request a fresh warm run + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + # Rebuild immediately rather than leaving every branch cold until the + # next merge or the Monday schedule. + gh workflow run turbo-cache-warm.yml --repo "$REPO" --ref main + echo "Dispatched Turbo Cache Warm on main." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d216c9af..6e76a8a84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,6 +100,14 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: packages/browseros-agent + # Without this the key embeds GITHUB_JOB, so the warm cache produced + # on main by a differently named job could never be restored here. + shared-key: browseros-agent + # Intermediate artifacts live outside the checkout so worktrees can + # share them, and rust-cache only saves workspace target dirs plus the + # registry and git caches by default. Name the build dir explicitly or + # every run would recompile the dependency graph from scratch. + cache-directories: ~/.cargo/build/browseros - name: Install dependencies run: bun ci diff --git a/.github/workflows/turbo-cache-warm.yml b/.github/workflows/turbo-cache-warm.yml index 07b9b76d3..1686177d7 100644 --- a/.github/workflows/turbo-cache-warm.yml +++ b/.github/workflows/turbo-cache-warm.yml @@ -1,14 +1,16 @@ name: Turbo Cache Warm -# Populates the Turbo cache on `main` so PR branches inherit warm cache. -# GitHub Actions caches restore only from the same branch or the base/default -# branch, so without a run on `main` every PR would start cold. Neither the -# Tests nor Code Quality workflows run on `main`, so this is that run. +# Populates the Turbo and Rust caches on `main` so PR branches inherit warm +# cache. GitHub Actions caches restore only from the same branch or the +# base/default branch, so without a run on `main` every PR would start cold. +# Neither the Tests nor Code Quality workflows run on `main`, so this is that +# run. on: push: branches: [main] paths: - .github/workflows/turbo-cache-warm.yml + - .cargo/config.toml - packages/browseros-agent/** schedule: # Weekly full warm to backstop GitHub's 7-day unused-cache eviction for @@ -63,3 +65,38 @@ jobs: # Per-merge: warm only what this merge changed. bunx turbo run typecheck --affected fi + + warm-rust: + name: Warm Rust cache + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Rust + # Pinned to match the Tests workflow. A different toolchain hashes into + # a different cache key, which would warm a cache nothing else reads. + uses: dtolnay/rust-toolchain@1.95.0 + with: + components: clippy,rustfmt + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/browseros-agent + # Must match the Tests workflow exactly. Without it the key embeds + # GITHUB_JOB and this cache would be unreadable there. + shared-key: browseros-agent + cache-directories: ~/.cargo/build/browseros + # Save even though nothing here is a pass/fail gate; warming is the + # entire point of the job. + save-if: "true" + + - name: Warm Rust cache + # Mirrors what the Rust suites compile: test binaries and clippy's + # separate artifacts. -D warnings is deliberately omitted, since this + # job exists to populate a cache, not to gate on lints. + run: | + cargo test --workspace --locked --no-run + cargo clippy --workspace --all-targets --locked