From 439f2f7dab77e60ebbe31489268438f623a4fed7 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Sun, 18 Oct 2026 18:06:30 +0000 Subject: [PATCH] CI: approved fork PRs run heavy jobs on self-hosted-fast in private directories An approved-for-ci label on a fork pull request sends Clippy, Build tests, the test shards and Doc tests to the trusted runners, with private copies of the build directory, Cargo home and rustup home that are deleted after the job. Only the labeling event qualifies; later pushes use the old fork routing. The skipped run of an unrelated label names its ci job differently. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++++++---- scripts/ci/fork-scratch.sh | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100755 scripts/ci/fork-scratch.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f88664fb53..361a93d49a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,11 @@ jobs: full: ${{ steps.paths.outputs.full }} # Code changed on a pull request that has no approved-for-ci label yet: only Quick compile. quick: ${{ steps.paths.outputs.quick }} + # An approved fork pull request whose approval is this very run: its heavy jobs run on the + # trusted self-hosted-fast runners, in private copies of the warm directories (fork-scratch.sh). + # Only the `labeled` event qualifies, so a later push to the fork's branch (unreviewed code) + # falls back to the fork routing above until a maintainer labels it again. `local` stays false. + fork_heavy: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'approved-for-ci' && github.event.pull_request.head.repo.full_name != github.repository }} packages: ${{ steps.paths.outputs.packages }} oracle: ${{ steps.paths.outputs.oracle }} # Whether the other Linux jobs use the self-hosted runners too. @@ -282,10 +287,13 @@ jobs: name: Native Linux Clippy needs: changes if: needs.changes.outputs.native == 'true' - runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'ubuntu-24.04' }} + runs-on: ${{ needs.changes.outputs.fork_heavy == 'true' && 'self-hosted-fast' || needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'ubuntu-24.04' }} timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Private build, Cargo and rustup directories (approved fork pull request) + if: needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh clippy - name: Warm build directory (self-hosted runner) if: runner.environment == 'self-hosted' && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) run: scripts/ci/warm-target.sh clippy @@ -308,12 +316,15 @@ jobs: scope=(--workspace) if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi cargo clippy "${scope[@]}" --all-targets --profile ci --locked -- -D warnings + - name: Delete the private directories + if: always() && needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh --clean build-tests: name: Build tests needs: changes if: needs.changes.outputs.native == 'true' - runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 'self-hosted-untrusted' || 'ubuntu-24.04' }} + runs-on: ${{ needs.changes.outputs.fork_heavy == 'true' && 'self-hosted-fast' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 'self-hosted-untrusted' || 'ubuntu-24.04' }} # Fork builds compile inside gVisor on shared boxes; give them room to finish and upload. timeout-minutes: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 90 || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 60 || 45 }} env: @@ -323,6 +334,9 @@ jobs: SCCACHE_IGNORE_SERVER_IO_ERROR: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Private build, Cargo and rustup directories (approved fork pull request) + if: needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh - name: Size parallel compiles to the job's memory run: scripts/ci/build-jobs.sh - uses: dtolnay/rust-toolchain@stable @@ -355,11 +369,14 @@ jobs: - name: Compiler cache statistics if: always() run: sccache --show-stats + - name: Delete the private directories + if: always() && needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh --clean tests: name: Native Linux tests (${{ matrix.shard }}/4) needs: [changes, build-tests] - runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 'self-hosted-untrusted' || 'ubuntu-24.04') }} + runs-on: ${{ needs.changes.outputs.fork_heavy == 'true' && 'self-hosted-fast' || needs.changes.outputs.local == 'true' && 'self-hosted-fast' || ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 'self-hosted-untrusted' || 'ubuntu-24.04') }} timeout-minutes: 60 strategy: fail-fast: false @@ -374,6 +391,9 @@ jobs: SOLIDCRAFT_REQUIRE_GPU: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Private build, Cargo and rustup directories (approved fork pull request) + if: needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh - name: Install a software Vulkan driver (lavapipe) # Preinstalled on the self-hosted runner, which has no sudo. if: runner.environment == 'github-hosted' @@ -425,6 +445,9 @@ jobs: name: junit-${{ matrix.shard }} path: target/nextest/ci/junit.xml retention-days: 7 + - name: Delete the private directories + if: always() && needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh --clean action-matrix: # Every ordinary action on every face, edge, vertex and body of the design corpus, against @@ -522,7 +545,7 @@ jobs: name: Doc tests needs: changes if: needs.changes.outputs.native == 'true' - runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'ubuntu-24.04' }} + runs-on: ${{ needs.changes.outputs.fork_heavy == 'true' && 'self-hosted-fast' || needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'ubuntu-24.04' }} timeout-minutes: 45 env: CARGO_INCREMENTAL: "0" @@ -531,6 +554,9 @@ jobs: SCCACHE_IGNORE_SERVER_IO_ERROR: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Private build, Cargo and rustup directories (approved fork pull request) + if: needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh doc-tests - name: Size parallel compiles to the job's memory run: scripts/ci/build-jobs.sh - name: Warm build directory (self-hosted runner) @@ -570,6 +596,9 @@ jobs: exit 0 fi cargo test "${scope[@]}" --profile ci --locked --doc + - name: Delete the private directories + if: always() && needs.changes.outputs.fork_heavy == 'true' + run: scripts/ci/fork-scratch.sh --clean oracle: name: Reference tests @@ -671,6 +700,9 @@ jobs: # Preserve the existing check name for branch protection, including docs-only PRs. ci: + # On the skipped run another label starts, this job is named differently, so that skipped job + # can never stand in for the required `ci` check of the pull request's real run. + name: ${{ github.event.action == 'labeled' && github.event.label.name != 'approved-for-ci' && 'ci (ignored label)' || 'ci' }} needs: [changes, checks, quick-compile, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup] if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci') runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-light' || 'ubuntu-24.04' }} diff --git a/scripts/ci/fork-scratch.sh b/scripts/ci/fork-scratch.sh new file mode 100755 index 0000000000..a430d551e2 --- /dev/null +++ b/scripts/ci/fork-scratch.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Self-hosted runner, approved fork pull requests only: give the job private copies of the build +# directory, Cargo home and rustup home, so the job never writes to the shared warm directories +# that main and merge-queue runs reuse (a build script or test from a fork could otherwise leave +# files there). The copies start as the warm state (reflinks where the filesystem has them) and +# `fork-scratch.sh --clean` deletes them at the end of the job. +# Usage: fork-scratch.sh [NAME] NAME = the warm-target.sh directory to start the build from +# fork-scratch.sh --clean +# This keeps shared state out of reach of ordinary writes. It is not a sandbox: fork code still +# runs as the runner's user, which is why the workflow starts it only from a `labeled` event a +# maintainer triggered, never from a push. +set -euo pipefail +base=${RUNNER_TEMP:?}/fork-scratch +if [ "${1:-}" = --clean ]; then + rm -rf "$base" + exit 0 +fi +if [ "${GITHUB_EVENT_NAME:-}" != pull_request ]; then + echo "fork-scratch: only for pull request runs" >&2 + exit 1 +fi +name=${1:-} +rm -rf "$base" +mkdir -p "$base" +if [ -n "$name" ]; then + warm=/home/runner/work/cargo-target/$name + if [ -d "$warm" ]; then + cp -a --reflink=auto "$warm" "$base/target" + else + mkdir -p "$base/target" + fi + echo "CARGO_TARGET_DIR=$base/target" >> "$GITHUB_ENV" +fi +cargo_home=${CARGO_HOME:-$HOME/.cargo} +rustup_home=${RUSTUP_HOME:-$HOME/.rustup} +mkdir -p "$base/cargo-home" "$base/rustup-home" +[ -d "$cargo_home" ] && cp -a --reflink=auto "$cargo_home/." "$base/cargo-home/" +[ -d "$rustup_home" ] && cp -a --reflink=auto "$rustup_home/." "$base/rustup-home/" +{ + echo "CARGO_HOME=$base/cargo-home" + echo "RUSTUP_HOME=$base/rustup-home" +} >> "$GITHUB_ENV" +echo "$base/cargo-home/bin" >> "$GITHUB_PATH" +echo "fork-scratch: private copies in $base ($(du -sh "$base" | cut -f1))"