diff --git a/.config/migration-gate-floor.txt b/.config/migration-gate-floor.txt new file mode 100644 index 000000000..1da7a3af2 --- /dev/null +++ b/.config/migration-gate-floor.txt @@ -0,0 +1,10 @@ +# Committed floor for the number of tests selected by the migration-critical +# CI gate (see scripts/check_migration_gate_count.sh, backlog#1153 infra-12). +# +# The floor equals the exact count of rustfs-ecstore --lib tests matching the +# gate filter (name substrings: data_movement, rebalance, decommission, +# source_cleanup, delete_marker) at the time this file was last updated. +# CI fails if the selected count drops below this number, so renames or +# removals that thin the gate must update this file in the same PR. +# Adding tests does not require a bump, but bumping keeps the guard tight. +571 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b55082482..6a0950e32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,13 +170,18 @@ jobs: # Explicit gate for migration-critical suites. These tests already ran in # the full nextest pass above; a single filtered nextest invocation keeps # the named gate without rebuilding or re-running them one package at a time. + # + # The gate selects tests by name substring (data_movement / rebalance / + # decommission / source_cleanup / delete_marker), so renames can silently + # thin it. The script owns the filter expression and first verifies the + # selected-test count against the committed floor in + # .config/migration-gate-floor.txt before running the gate; renames or + # removals must update that file consciously (see the script header). # Kept on the default profile (no --profile ci): a second --profile ci run # would clobber target/nextest/ci/junit.xml, and none of these tests are # quarantined so they gain nothing from the ci profile's retry overrides. - name: Run rebalance/decommission migration proofs - run: | - cargo nextest run -p rustfs-ecstore --lib \ - -E 'test(data_movement) or test(rebalance) or test(decommission) or test(source_cleanup) or test(delete_marker)' + run: ./scripts/check_migration_gate_count.sh test-and-lint-rio-v2: name: Test and Lint (rio-v2) diff --git a/scripts/check_migration_gate_count.sh b/scripts/check_migration_gate_count.sh new file mode 100755 index 000000000..22c853e71 --- /dev/null +++ b/scripts/check_migration_gate_count.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Count-floor guard for the migration-critical test gate (backlog#1153 infra-12). +# +# ci.yml's migration gate selects tests BY NAME SUBSTRING. A rename that drops +# a test out of the filter silently thins the gate — potentially to zero — +# without any CI signal (this is how the layered gate died when #878 closed). +# This script owns the filter expression so the count check and the test run +# cannot drift, and fails fast when the number of selected tests drops below +# the committed floor in .config/migration-gate-floor.txt. +# +# NAMING CONVENTION DEPENDENCY: migration-gate tests are matched by these +# reserved name substrings: +# +# data_movement, rebalance, decommission, source_cleanup, delete_marker +# +# Renaming a test away from all of these substrings removes it from the gate. +# The floor equals the exact selected-test count at the time it was last +# committed, so ANY reduction fails CI. To shrink the gate intentionally +# (test removed or consciously renamed), update the floor file in the same PR +# so the reduction is visible in the diff. Adding tests never requires a floor +# bump (the floor is a lower bound), but bumping it keeps the guard tight. +# +# Per-pattern floors are deliberately omitted: with an exact-count total floor, +# a single test dropping out of any pattern already fails the check, so +# per-pattern bookkeeping would add churn without detection power. +# +# Usage: +# scripts/check_migration_gate_count.sh # count check + run the gate +# scripts/check_migration_gate_count.sh check # count check only +# scripts/check_migration_gate_count.sh run # run the gate only + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Single source of truth for the migration-gate filter. ci.yml must invoke +# this script instead of inlining the expression. +MIGRATION_GATE_FILTER='test(data_movement) or test(rebalance) or test(decommission) or test(source_cleanup) or test(delete_marker)' +FLOOR_FILE=".config/migration-gate-floor.txt" + +mode="${1:-all}" +case "$mode" in + all | check | run) ;; + *) + echo "usage: $0 [all|check|run]" >&2 + exit 2 + ;; +esac + +if [[ "$mode" == "all" || "$mode" == "check" ]]; then + floor="$(grep -Ev '^[[:space:]]*(#|$)' "$FLOOR_FILE" | head -n1 | tr -d '[:space:]')" + if ! [[ "$floor" =~ ^[0-9]+$ ]]; then + echo "error: $FLOOR_FILE does not contain a numeric floor (got: '$floor')" >&2 + exit 1 + fi + + # Human-format list: binary headers start at column 0, one test per + # 4-space-indented line. A nextest failure aborts via set -e with its + # stderr visible; an output-format change collapses the count to 0 and + # fails loudly rather than silently passing. + list_output="$(cargo nextest list -p rustfs-ecstore --lib -E "$MIGRATION_GATE_FILTER")" + count="$(printf '%s\n' "$list_output" | grep -c '^ ' || true)" + + if ((count < floor)); then + echo "error: migration gate selects $count tests, below the committed floor of $floor." >&2 + echo "" >&2 + echo "The gate matches tests by name substring (data_movement / rebalance /" >&2 + echo "decommission / source_cleanup / delete_marker). A rename or removal has" >&2 + echo "thinned the selection. Either restore the test names, or — if the" >&2 + echo "reduction is intentional — update $FLOOR_FILE in this PR so the" >&2 + echo "gate change is explicit and reviewable." >&2 + exit 1 + fi + echo "migration gate count OK: $count selected tests (floor: $floor)" +fi + +if [[ "$mode" == "all" || "$mode" == "run" ]]; then + cargo nextest run -p rustfs-ecstore --lib -E "$MIGRATION_GATE_FILTER" +fi