name: PR Check — Rust # Rust half of the former check.yml (split in #232). # # Runs on PRs that change Rust, plus once a week on main to refresh the shared # cache (see the note on the triggers). The runner has capacity 1 — jobs queue # instead of running in parallel, so every minute spent here is a minute the # next PR waits. # # No `branches:` filter on purpose. `branches: [main]` never matched a PR # stacked on top of another feature branch, which is what /autopilot produces: # 4 of the 5 PRs in the feature-gating milestone ran no CI at all. on: pull_request: paths: - 'src-tauri/**' - '.forgejo/workflows/check-rust.yml' # Whole directory, not just audit.toml: a later .cargo/config.toml # (rustflags, linker, target dir) would change the Rust build, and an # exact-path entry would let it skip Rust CI unnoticed. - '.cargo/**' # The weekly run is what feeds the cache to pull requests (#234). The runner # (forgejo-runner v12.7.0) isolates what a pull_request run writes to that # PR's ref: later pushes to the same PR read it back, other PRs never see it. # A PR run falls back to the shared, non-isolated entries only when nothing # matches in its own scope. Scheduled and dispatched runs write to the shared # scope, which makes them this workflow's only writers of it (a merged PR's # `closed` event would be another, but this workflow does not subscribe to # it). # # The runner deletes an entry unused for 7 days or created more than 30 days # ago, and keeps only the newest entry per key, whatever its scope; the next # Monday run saves a fresh one. Cold runs that do not mean #234 is back (a # timeout would): # - a new stable Rust (the toolchain floats on `stable`) or a change to this # job's `env:`: the key hashes rustc and the CARGO*, RUST*, CC*, CFLAGS*, # CXX* and CMAKE* variables. Cold until the next Monday; # - a PR run that saves under the shared entry's key evicts it for every PR # until the next Monday. It saves when its own scope holds an older entry # for another Cargo.lock, served before the shared one: a PR that bumped a # dependency, then went back to main's lockfile. Two open PRs on the same # lockfile, different from main's, evict each other the same way; # - PR runs once an entry that only got exact hits, hence never re-saved, # reaches 30 days; # - after a week without any cache request, the Monday restore can fail with # a 404: the lookup does not refresh the entry, and the cleanup it triggers # may delete it (forgejo/runner#1741). The run then saves a fresh entry. # workflow_dispatch re-seeds by hand. schedule: # Mondays 06:30 UTC, after audit.yml (06:00 UTC). - cron: '30 6 * * 1' workflow_dispatch: # Cancel obsolete runs (e.g. on force-push) so only the latest commit runs. # Distinct from the frontend group: a shared group would make the two # workflows cancel each other on a PR that touches both. concurrency: group: ci-rust-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: rust: runs-on: ubuntu container: ubuntu:22.04 env: PATH: /root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CARGO_TERM_COLOR: always # rust-cache keeps the dependencies and drops this crate's own artifacts # before saving, so incremental data would never be reused — pure # overhead — and test debug info would only make the cached target/ # bigger. CARGO_INCREMENTAL: 0 CARGO_PROFILE_TEST_DEBUG: 0 steps: - name: Install system dependencies, Node.js and Rust run: | apt-get update # zstd: the cache archives are compressed with it when present, # faster and smaller than the gzip fallback. apt-get install -y --no-install-recommends \ curl wget git ca-certificates build-essential pkg-config \ libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libssl-dev \ libdbus-1-dev zstd # Node.js runs the JavaScript actions: actions/checkout (node20) and # Swatinem/rust-cache (node24 since v2.9). The runner starts them # with whatever `node` the container has, so install the newest # runtime they declare. taiki-e/install-action is a composite action. curl -fsSL https://deb.nodesource.com/setup_24.x | bash - apt-get install -y nodejs # Rust toolchain curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal node --version rustc --version cargo --version - name: Checkout uses: https://github.com/actions/checkout@v4 # Caches ~/.cargo (registry, git, bin) and src-tauri/target, keyed on the # rustc version, the job's CARGO*/RUST* environment, the dependencies # declared in the Cargo manifests and lockfile (not the app's own # version), and this job. Saved only when the job succeeds and the key # was not an exact hit. Served by # the runner's cache proxy on port 28100, which ufw opens to the Docker # job networks only; until 2026-10-03 every restore and save timed out # there (#234, la-compagnie-maximus docs/forgejo-ops.md). What a PR run # saves stays private to that PR: see the note on the triggers. - name: Rust cache uses: https://github.com/Swatinem/rust-cache@v2 with: workspaces: "src-tauri -> target" - name: cargo check run: cargo check --manifest-path src-tauri/Cargo.toml --all-targets # Anti-rot guard for the suppressions in .cargo/audit.toml (#310). Each # entry there is justified by its crate being absent from every shipped # target's graph — a property of today's resolved graph, not a permanent # one. rsa is currently reachable from nothing: its only parent in the # lockfile is sqlx-mysql, which this SQLite project never compiles. If a # dependency change ever pulls it into a shipped target, the suppression # would silently hide a live advisory and the daily audit would stay # green: the inverse of the permanent red #310 exists to kill. # # Such a change would itself touch src-tauri, which is exactly what # triggers this workflow. Runs after cargo check so the registry index is # already warm, and --locked so cargo tree cannot rewrite the lockfile the # audit was taken against. # # CRATES must mirror the crates named in .cargo/audit.toml. Adding an # entry there without adding its crate here leaves it unguarded. # # Every check echoes its result, including the passing ones. A guard that # is silent on success cannot be told apart in the log from a guard that # never ran — which is the same silent-skip failure mode this step exists # to catch, one level up. - name: Verify suppressed advisories are still unreachable run: | set -u CRATES="rsa" TARGETS="x86_64-unknown-linux-gnu x86_64-pc-windows-msvc" rc=0 checks=0 for crate in $CRATES; do for target in $TARGETS; do # An absent crate exits 0 with empty stdout ("nothing to print" # goes to stderr). A non-zero exit means cargo tree itself failed # — treat that as a failure rather than as proof of absence. out=$(cargo tree --manifest-path src-tauri/Cargo.toml --locked \ -i "$crate" --target "$target" 2>/dev/null) || { echo "cargo tree failed for $crate / $target — cannot verify the suppression" rc=1 continue } if [ -n "$out" ]; then echo "FAIL: $crate is now compiled for $target — its .cargo/audit.toml suppression is no longer justified (see #310)." rc=1 else echo "ok: $crate absent from $target" fi checks=$((checks + 1)) done done # Canary: a crate known to be present. If this stops being found, the # loop above is broken and its silence means nothing. canary=$(cargo tree --manifest-path src-tauri/Cargo.toml --locked \ -i tar --target x86_64-unknown-linux-gnu 2>/dev/null) || true if [ -z "$canary" ]; then echo "FAIL: canary 'tar' was not found although it is a known dependency. The guard is not proving anything." rc=1 else echo "ok: canary 'tar' found for x86_64-unknown-linux-gnu" fi echo "Suppression guard: $checks checks, exit $rc" exit $rc - name: cargo test run: cargo test --manifest-path src-tauri/Cargo.toml --all-targets # Prebuilt binary. `cargo install --locked cargo-audit` recompiled the # tool from source on every single run (~4m40s). No continue-on-error: # a tooling failure should fail the job rather than be swallowed. - name: Install cargo-audit uses: https://github.com/taiki-e/install-action@v2 with: tool: cargo-audit # Advisories are informational — they can land on unrelated crates and # would otherwise stall unrelated work — so the step is non-blocking. # It no longer hides real failures behind `|| true` though. Daily # RustSec coverage outside Rust PRs lives in audit.yml. - name: cargo audit continue-on-error: true run: cargo audit --file src-tauri/Cargo.lock