# 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; see the comment on
# it for how that bounds concurrency and why it is scoped by event.
#
# 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:
    inputs:
      emit_timings:
        description: >-
          Also emit cargo --timings for the ci-dev build and upload it. Used to
          decide whether sccache is worth adopting (rustfs/backlog#1601 gate).
        required: false
        default: false
        type: boolean

permissions:
  contents: read

# Scoped by event. A push run and a dispatch run do not compete: GitHub keeps
# one running plus one pending per group, so with a single shared group a
# manually dispatched run was displaced as pending by the next merge and
# cancelled — observed three times in a row, which made the --timings gate in
# rustfs/backlog#1601 effectively impossible to trigger while main was busy.
#
# Still no cancel-in-progress: a burst of merges collapses into "current run
# finishes, newest queued run follows" rather than a pile-up, which is what
# bounds this workflow to one self-hosted runner per event type.
#
# The two paths can now overlap and race to save the same key. That is benign:
# the loser finds the key already present and skips, and both builds produce the
# same artifacts from the same commit.
concurrency:
  group: cache-warm-${{ github.event_name }}
  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
          cache-save-if: 'true'
          install-build-packaging-tools: 'false'

      # rustfs/backlog#1601 gate. sccache can only cache compilation units whose
      # --emit includes link, so it covers workspace rlibs and nothing else:
      # clippy is metadata-only, and the ~100 test binaries, the rustfs bin and
      # every build script invoke the system linker. Before spending a bucket,
      # credentials and a supply-chain boundary on it, measure how much of the
      # build is actually rlib codegen.
      #
      # Read from the report: workspace lib codegen as a share of the build, and
      # s3select-query's own rlib as a share. The plan adopts sccache only above
      # 50% and 25% respectively; if linking dominates instead, the answer is
      # mold/lld plus split-debuginfo, which is exactly the part sccache cannot
      # touch. Off by default — this doubles the ci-dev build.
      - name: Build ci-dev superset (with --timings)
        if: inputs.emit_timings
        env:
          CARGO_BUILD_JOBS: "2"
        run: cargo build --workspace --all-targets --timings

      - name: Upload cargo timings report
        if: inputs.emit_timings
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: cargo-timings-ci-dev
          path: target/cargo-timings/
          retention-days: 30
          if-no-files-found: error

      # --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

      # Runs before rust-cache's post step, so these are the sizes it is about
      # to archive. Reported so the cache-all-crates decision stays evidence-led:
      # registry/src is what that flag prunes, registry/cache is what the pruned
      # sources are re-unpacked from. See rustfs/backlog#1600.
      - name: Report cache input sizes
        if: always()
        run: |
          # tee, not a plain redirect: sent only to $GITHUB_STEP_SUMMARY these
          # numbers are readable in the UI but absent from the job log, and the
          # REST API exposes the log, not the summary — which made the figures
          # unreachable for exactly the scripted comparison they exist for.
          sizes="$(du -sh ~/.cargo/registry/src ~/.cargo/registry/cache \
                          ~/.cargo/registry/index ~/.cargo/git target 2>/dev/null || true)"
          echo "cache-input-sizes-begin"
          printf '%s\n' "$sizes"
          echo "cache-input-sizes-end"
          {
            echo "### Cache input sizes (ci-dev)"
            echo '```'
            printf '%s\n' "$sizes"
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

  # Readers: test-and-lint-rio-v2 (per-PR), build-rustfs-debug-binary-rio-v2
  # (weekly schedule / manual dispatch only — dormant rio-v2 variant, see
  # rustfs/backlog#1835 and docs/architecture/minio-file-format-compat.md).
  # The second build below stays despite the reduced cadence: it warms the
  # rio-v2,e2e-test-hooks feature resolution the scheduled build restores,
  # which keeps that lane inside its 30-minute timeout.
  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
          cache-save-if: 'true'
          install-build-packaging-tools: 'false'

      - 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
          cache-save-if: 'true'
          install-build-packaging-tools: 'false'

      - 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
          cache-save-if: 'true'
          install-build-packaging-tools: 'false'

      - 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
