diff --git a/.github/workflows/cache-warm.yml b/.github/workflows/cache-warm.yml new file mode 100644 index 000000000..4688a08f7 --- /dev/null +++ b/.github/workflows/cache-warm.yml @@ -0,0 +1,196 @@ +# Copyright 2026 RustFS Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Sole writer of the Rust dependency caches that ci.yml restores. +# +# Why this is a separate workflow rather than steps inside ci.yml: ci.yml's +# concurrency group cancels in-progress runs on main pushes, and merges land far +# faster than its 70-minute pipeline. Measured over 15 consecutive main pushes: +# 12 cancelled, 2 failed, 0 succeeded. A cancelled run never reaches +# Swatinem/rust-cache's post step (cache-on-failure does not cover cancellation), +# so the writer lanes were saving nothing and every PR paid a cold restore — +# 11.8-20.9 minutes of "Setup Rust environment" against 0.7-3.4 warm. +# +# Splitting cache writing out of the test pipeline lets ci.yml keep cancelling +# superseded runs (which is correct — nobody needs test results for a commit +# that is already three merges behind) while the caches still get written. +# +# The group below deliberately does NOT cancel in progress. GitHub keeps at most +# one running plus one pending run per group, so a burst of merges collapses +# into "current run finishes, newest queued run follows" rather than a pile-up. +# That also bounds this workflow to one self-hosted runner at a time. +# +# Each job below owns exactly one shared-key and is the only place that sets +# cache-save-if to anything but 'false' for it; every lane in ci.yml reads. +# scripts/security/check_cache_save_if.sh keeps the declarations explicit. +# +# The builds are supersets of what the reading lanes compile, because a reader +# restores only what the writer saved. Feature resolution matters here: a lane +# built with e2e-test-hooks resolves dependency features differently, which +# changes -Cmetadata, so the plain build does not cover it. See +# rustfs/backlog#1600. + +name: Cache Warm + +on: + push: + branches: [ main ] + # Mirrors ci.yml's push paths-ignore: if a commit cannot change what ci.yml + # compiles, it cannot change what ci.yml needs restored either. + paths-ignore: + - "**.md" + - "docs/**" + - "deploy/**" + - "scripts/dev_*.sh" + - "scripts/probe.sh" + - "LICENSE*" + - ".gitignore" + - ".dockerignore" + - "README*" + - "**/*.png" + - "**/*.jpg" + - "**/*.svg" + - ".github/workflows/build.yml" + - ".github/workflows/docker.yml" + - ".github/workflows/audit.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cache-warm + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + +jobs: + # Readers: test-and-lint, test-ilm-integration-serial, build-rustfs-debug-binary, + # e2e-tests, e2e-full. + warm-ci-dev: + name: Warm ci-dev + runs-on: sm-standard-4 + timeout-minutes: 90 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Rust environment + uses: ./.github/actions/setup + with: + rust-version: stable + cache-shared-key: ci-dev + github-token: ${{ secrets.GITHUB_TOKEN }} + cache-save-if: 'true' + + # --all-targets covers the test binaries nextest builds, including + # e2e_test, which test-and-lint's own run excludes. The second build adds + # the e2e-test-hooks feature resolution that build-rustfs-debug-binary uses + # and that no lint lane enables. + - name: Build ci-dev superset + env: + # Same limit ci.yml puts on its nextest step: this builds the same + # ~100 workspace test binaries, and three concurrent links saturate the + # self-hosted runner's overlay I/O and can wedge Cargo (#5394). + CARGO_BUILD_JOBS: "2" + run: | + cargo build --workspace --all-targets + cargo build -p rustfs --bins --features e2e-test-hooks + + # Readers: test-and-lint-rio-v2, build-rustfs-debug-binary-rio-v2. + warm-ci-feat-rio: + name: Warm ci-feat-rio + runs-on: sm-standard-4 + timeout-minutes: 90 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Rust environment + uses: ./.github/actions/setup + with: + rust-version: stable + cache-shared-key: ci-feat-rio + github-token: ${{ secrets.GITHUB_TOKEN }} + cache-save-if: 'true' + + - name: Build ci-feat-rio superset + run: | + cargo build -p rustfs -p rustfs-ecstore --all-targets --features rio-v2 + cargo build -p rustfs --bins --features rio-v2,e2e-test-hooks + + # Readers: the swift and sftp legs of test-and-lint-protocols. Built in + # sequence rather than as `--features swift,sftp`, which is a combination no + # lane actually compiles; running both leaves the union in target/. + warm-ci-feat-proto: + name: Warm ci-feat-proto + runs-on: sm-standard-4 + timeout-minutes: 90 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Rust environment + uses: ./.github/actions/setup + with: + rust-version: stable + cache-shared-key: ci-feat-proto + github-token: ${{ secrets.GITHUB_TOKEN }} + cache-save-if: 'true' + + - name: Build ci-feat-proto superset + run: | + cargo build -p rustfs -p rustfs-protocols --all-targets --features swift + cargo build -p rustfs -p rustfs-protocols --all-targets --features sftp + + # Reader: uring-integration. Runs on ubuntu-latest to match it: rust-cache's + # key covers runner.os and arch but not the runner label or image, so a cache + # written on sm-standard-4 would be restored by the hosted runner as if it + # belonged to it. + warm-ci-uring: + name: Warm ci-uring + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Rust environment + uses: ./.github/actions/setup + with: + rust-version: stable + cache-shared-key: ci-uring + github-token: ${{ secrets.GITHUB_TOKEN }} + cache-save-if: 'true' + + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Build ci-uring superset + run: cargo build -p rustfs-ecstore --all-targets diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d663451e3..f5567744d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,9 +169,14 @@ jobs: uses: ./.github/actions/setup with: rust-version: stable + # Every lane in this workflow reads its cache and none writes it. + # cache-warm.yml is the sole writer for all four keys: this workflow + # cancels superseded runs on main, and a cancelled run never reaches + # rust-cache's post step, so writing from here saved nothing (12 of 15 + # consecutive main-push runs were cancelled). See rustfs/backlog#1600. cache-shared-key: ci-dev github-token: ${{ secrets.GITHUB_TOKEN }} - cache-save-if: ${{ github.ref == 'refs/heads/main' }} + cache-save-if: 'false' - name: Prepare test evidence run: | @@ -183,19 +188,6 @@ jobs: echo "started_at=$(date --utc --iso-8601=seconds)" } > artifacts/test-and-lint/run-metadata.txt - # This job is the sole writer of the ci-dev cache, so what it builds is - # what every reader restores. Its own steps are not a superset of theirs: - # clippy only emits metadata, the nextest pass excludes e2e_test, and - # nothing here enables e2e-test-hooks (a different feature set means a - # different -Cmetadata). Without these two builds the readers restore a - # cache that is missing exactly the dependencies they need. Runs on main - # only, so it costs nothing on the PR critical path. - - name: Warm shared cache superset (main only) - if: github.ref == 'refs/heads/main' - run: | - cargo build --tests -p e2e_test - cargo build -p rustfs --bins --features e2e-test-hooks - # Clippy runs before the test pass: lint failures are the most common # CI-only breakage and should surface in minutes, not after 20+ minutes # of tests. @@ -415,14 +407,7 @@ jobs: rust-version: stable cache-shared-key: ci-feat-rio github-token: ${{ secrets.GITHUB_TOKEN }} - cache-save-if: ${{ github.ref == 'refs/heads/main' }} - - # Sole writer of ci-feat-rio. The reader (build-rustfs-debug-binary-rio-v2) - # builds with rio-v2,e2e-test-hooks, which this job never enables, so warm - # that combination here or the reader restores a cache without it. - - name: Warm shared cache superset (main only) - if: github.ref == 'refs/heads/main' - run: cargo build -p rustfs --bins --features rio-v2,e2e-test-hooks + cache-save-if: 'false' - name: Run rio-v2 clippy lints run: cargo clippy -p rustfs -p rustfs-ecstore --all-targets --features rio-v2 -- -D warnings @@ -464,15 +449,7 @@ jobs: rust-version: stable cache-shared-key: ci-feat-proto github-token: ${{ secrets.GITHUB_TOKEN }} - cache-save-if: ${{ github.ref == 'refs/heads/main' && matrix.features.name == 'swift' }} - - # The swift leg is the sole writer of ci-feat-proto, shared with the sftp - # leg. Build the other leg's feature set too so the saved target/ holds - # the union; running them in sequence is safer than `--features - # swift,sftp`, which is a combination no lane actually compiles. - - name: Warm shared cache superset (main only) - if: github.ref == 'refs/heads/main' && matrix.features.name == 'swift' - run: cargo clippy -p rustfs -p rustfs-protocols --all-targets --features sftp -- -D warnings + cache-save-if: 'false' - name: Run clippy with ${{ matrix.features.name }} run: | @@ -575,10 +552,11 @@ jobs: # sm-standard-4 are therefore indistinguishable to it, so sharing a key # would let two different system images overwrite each other's # artifacts, and would make a 2-core hosted runner unpack ci-dev's ~3GB - # instead of this lane's ~1.3GB. + # instead of this lane's ~1.3GB. cache-warm.yml warms this key on + # ubuntu-latest for the same reason. cache-shared-key: ci-uring github-token: ${{ secrets.GITHUB_TOKEN }} - cache-save-if: ${{ github.ref == 'refs/heads/main' }} + cache-save-if: 'false' - name: Install build dependencies run: sudo apt-get update && sudo apt-get install -y protobuf-compiler