Rust CI best practices — multi-job design, gate pattern, caching, SHA pinning
From gh-guardnpx claudepluginhub anthropics/claude-plugins-community --plugin gh-guardThis skill is limited to using the following tools:
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
This skill covers the design patterns for a robust Rust CI pipeline, based on production experience with multi-job workflows.
lint ──┐
msrv ──┤
test ──┼──→ ci (gate)
deny ──┤
audit ─┘
| Job | Purpose | Blocking? |
|---|---|---|
lint | cargo fmt --check + cargo clippy (default + all features) | Yes |
msrv | cargo check --locked with pinned MSRV toolchain | Yes |
test | cargo test across OS matrix (Linux/macOS/Windows) + beta | Yes |
deny | cargo-deny for advisories, bans, licenses, sources | Mixed* |
audit | cargo audit for known vulnerabilities | Yes |
ci | Gate job — single required status check | Always runs |
*deny uses continue-on-error for advisories (informational) but blocks on bans/licenses/sources.
The ci gate job is the only required status check in branch protection settings:
ci:
name: CI
if: always()
needs: [lint, msrv, test, deny, audit]
runs-on: ubuntu-latest
steps:
- name: Evaluate results
run: |
result="${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}"
[[ "$result" == "false" ]] || exit 1
Why this pattern:
if: always() ensures the gate runs even if upstream jobs are cancelledAll actions MUST be pinned to full commit SHA with a version comment:
# v6.0.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
Why SHA not tag:
Real-world incident — Trivy tag hijacking (March 2026):
Attackers with write access to aquasecurity/trivy-action force-pushed 75 of 76 version tags to point to malicious commits. The poisoned entrypoint.sh harvested secrets from CI runner process memory and exfiltrated them to a typosquatting C2 domain (scan.aquasecurtiy[.]org). Over 10,000 workflow files on GitHub referenced aquasecurity/trivy-action by tag. Any pipeline that ran a poisoned tag had all accessible secrets stolen. SHA-pinned workflows were completely unaffected — the immutable commit reference couldn't be redirected. This incident is the single strongest argument for SHA pinning.
Finding the SHA for a tag:
# Get the commit SHA for a specific tag
gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha'
# Or browse to the tag on GitHub and copy the full SHA
Exception: SLSA generator must use @tag — see slsa-provenance skill.
- name: Cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }} # Only save on main
shared-key: msrv # Share across MSRV jobs
Key decisions:
save-if: main — PRs read the cache but don't write, preventing cache pollutionshared-key — MSRV and stable builds share caches when possibleworkspaces: fuzz -> target and per-target keysAlways set permissions: read-all at the workflow level:
permissions: read-all
Override per-job only where needed:
jobs:
publish:
permissions:
contents: read
id-token: write # For OIDC Trusted Publishing
This satisfies the Scorecard Token-Permissions check and follows least privilege.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Advisories (CVE disclosures in dependencies) should NOT block PRs:
deny:
strategy:
matrix:
checks:
- advisories
- bans licenses sources
continue-on-error: ${{ matrix.checks == 'advisories' }}
Why: A new advisory can appear at any time, blocking all PRs until a dependency is updated. The advisory check runs but failures are informational. The bans/licenses/sources check is strict.
- name: Install cargo-audit
run: cargo install cargo-audit --version 0.22.1 --locked
Gotcha: Without --locked, transitive dependencies (e.g., smol_str) may require a newer Rust version than CI provides, causing build failures. Always use --locked. Pin --version to avoid pulling untested new releases in CI — update intentionally when ready.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable]
include:
- os: ubuntu-latest
toolchain: beta
fail-fast: false — don't cancel other OS tests if one fails--locked to ensure reproducible buildsmsrv:
name: MSRV (1.82)
steps:
- uses: dtolnay/rust-toolchain@SHA
with:
toolchain: "1.82"
- run: cargo check --locked
- run: cargo check --locked --all-features
cargo check (not cargo test) — faster, only verifies compilationrust-toolchain.toml for contributor consistencySee templates/workflows/ci.yml for the complete implementation.