test(scanner): add G09 upgrade evidence runner (#7524)

* fix(replication): close the GA blocker set from backlog#2366 (#7503)

* fix(replication): close GA blockers from backlog#2366

Implements the P1 set from the pre-GA replication audit:

- Replication rule tag filters now require every And.Tag to match, replacing
  the s3s OR semantics with a local AND matcher that fails closed on a
  malformed tag.
- A replicated group membership change no longer writes the group status, so
  a membership update carrying the default Enabled status cannot silently
  re-enable a disabled group on the peer.
- A successful IAM import schedules one collapsed full-IAM snapshot per remote
  peer instead of leaving the imported entities local-only.
- A pending endpoint refresh is redriven by the heavyweight reconcile tick,
  carries its own ilm-expiry override, and no longer blocks a remove that
  drops every unacknowledged peer.
- Site metrics expose local replication failure totals and rolling windows;
  node-level counters no longer report a constructed zero.
- set/remove-remote-target notify peer metadata caches before returning, so a
  follow-up put-bucket-replication on another node sees the target.
- Adds the site-replication operations runbook, a docs index, a replication
  support boundary section, and the Replication changelog section.

* fix(site-replication): resume only a locally driven endpoint refresh

The peer-side edit handler journals a pending endpoint refresh with an empty
`remote_peers` map and commits it inside the same request through
`apply_internal_peer_edit`. The reconcile tick could not tell that journal
from the coordinator's own: with no required peers it reads as complete on
sight, so the tick committed it with `edit_state` - losing the local-name
sync - and cleared it under the request that owned it, whose commit then
reported the refresh as changed and denied the coordinator the peer
acknowledgement it was waiting for.

Resume now runs only for a journal that carries the fan-out topology. A
receiver's journal stays for the coordinator to redrive with the same
refresh id, which is the path that already recovers it.

* fix(site-replication): keep an explicit disabled group status on a snapshot

Skipping the group-status write whenever an item carries members stopped a
membership change from re-enabling a disabled group, but it also silenced the
full-IAM snapshot, which always sends members together with the sender's real
status. A peer that did not have the group yet created it through
`GroupInfo::new` - enabled - so a bootstrap, a repair, or the snapshot an IAM
import now schedules handed every member of a frozen group live access there.

The madmin wire maps an unset `groupStatus` to Enabled, so only Enabled can be
a default. Disabled is always explicit and is applied again.

* fix(site-replication): schedule the import snapshot without recording a failure

`import-iam` reused the failure-recording path to queue its full-IAM
snapshot. That raises `retry_count` on every call, so three imports - the
normal shape of a bulk migration done one archive at a time - escalated a
healthy peer to `retryStats.failed` with the scheduling note shown as
`lastError`, which is exactly the signal the runbook tells operators to
repair. A full retry queue also turned a completed import into a 503.

Scheduling now only ensures the collapsed entry exists, and a failure to
schedule is logged instead of failing the request: the entities are already
imported and the reconcile pass still closes the gap.

* fix(admin): stop reporting replication failures as retries

`retries` is the minio-go counter for redeliveries, and mc prints it as such.
Filling it with the failure count claimed a redelivery that never happens: a
failed object is not retried by an event today, it waits for the scanner heal
pass. `errors` keeps the failure counters; `retries` stays zero until there is
a real redelivery to count, and the runbook now says so.

* perf(site-replication): aggregate failure windows without cloning bucket stats

`site_metrics_snapshot` went through `get_all`, which clones every bucket's
stats, and then scanned each target's sample deque twice. That deque is
bounded only by the one-hour window, so an unreachable target under load -
the case an operator polls this endpoint for - made every
`mc admin replicate status` copy the whole backlog and hold the read lock
against the failure path while doing it.

It now folds under the read lock and takes both windows in one walk. The
`max` against the serialized `last_minute` / `last_hour` snapshots is dropped:
those are stamped onto per-bucket clones elsewhere and are always zero in this
node-local cache.

* fix(site-replication): reject a conflicting ilm-expiry override on a re-run

The commit now reads the ilm-expiry override back out of the pending refresh
journal, so a second edit that asks for a different value had it dropped while
the request still reported success. Re-running without the flag keeps pinning
the recorded value - that is the documented way to redrive a stuck refresh -
but an explicit different value is now rejected instead of ignored.

* fix(admin): do not fail a remote-target write on a peer reload error

set/remove-remote-target propagated the peer metadata reload error, so a
target that was already persisted and live on this node reported a 5xx to the
client whenever one peer could not be reached. Every S3 bucket-config write
path treats that reload as best effort and only warns; these two admin
handlers now do the same, and the reason is logged with the bucket and action.

* fix(site-replication): undo every bucket a cut-short refresh rewrote

When a remove accepted on another node clears the refresh journal mid-pass,
only the bucket holding the lock at that moment had its restored target
undone. The buckets rewritten earlier in the same pass kept a target pointing
at the removed peer whenever the remove's own cleanup had already walked past
them. The undo now covers every bucket this pass rewrote, attempting all of
them so one failure does not strand the rest.

* fix(site-replication): keep replay running while an endpoint refresh is pending

A pending endpoint refresh took the whole heavyweight pass with it, so a peer
that never came back froze IAM and bucket replay to every healthy peer too -
the stall this journal's resume path was meant to end. The refresh arm now
drains the retry queue before returning; it replays per-peer deliveries
against the endpoints currently committed in state, so it is unaffected by the
edit in flight. Bucket wiring reconciliation still waits, because it rewrites
the very targets the refresh is changing, and the runbook now says so.

* test(e2e): cover the AND semantics of a two-tag replication filter

The acceptance matrix only had a single-tag rule, which matches under both AND
and OR semantics and therefore proved nothing about the filter this fix
changed. It now also carries a two-tag `And` rule - the shape
`mc replicate add --tags "k1=v1&k2=v2"` writes - and asserts that an object
with one of the two tags is not admitted while an object with both is.

No new test function, so the nightly selection digest is unchanged.

* refactor(site-replication): fold the refresh state-change error into one constructor

The endpoint-refresh work added three `s3_error!` invocation lines, which the
s3s footprint ratchet is meant to prevent. Five copies of the same
concurrent-change error now share one constructor, so the surface nets one
line smaller than main; the baseline is retightened to match.

* fix(site-replication): report a peer whose IAM snapshot waits for a repair

An escalated snapshot entry records a deletion a snapshot cannot replay, so
only a repair settles it and the marker must survive. Scheduling an import
snapshot therefore leaves that peer's entry alone - and now says so, instead
of returning success while nothing was scheduled for it.

* docs(operations): state the group-status and escalation convergence limits

Two boundaries the fixes in this branch make load-bearing: a membership change
never carries an enable, so a group disabled on one site only has to be
re-enabled there explicitly; and a peer holding an escalated IAM entry does
not receive a scheduled snapshot, including the one a bulk import schedules,
until a repair settles it.

* fix(ci): bind performance runs to selected inputs (#7512)

* test(scanner): add G09 upgrade evidence runner

Add a reusable Scanner/Heal G09 runner for Linux PR-head validation. The script downloads the pinned previous release, builds the current checkout, runs the mixed-version and rollback upgrade E2E lanes, and validates the measured raw artifacts before they can be consumed by the release bundle gate.

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
houseme
2026-09-08 23:50:02 +08:00
committed by GitHub
parent cfda9b451c
commit 857584b3c2
3 changed files with 363 additions and 128 deletions
+1 -1
View File
@@ -51,12 +51,12 @@ their issue closes.
| `e2e-run.sh` | ci-gate | Boots a rustfs server and runs the `s3s-e2e` black-box conformance tool against it | ci.yml `e2e-tests` jobs; `docs/testing/README.md` |
| `run_ecstore_validation_suite.sh` | dev-tool | ecstore black-box validation suite (`quick`/`full`/`destructive`/`fuzz` profiles) | `docs/testing/README.md`, `docs/testing/ecstore-validation-suite-design.md` |
| `run_e2e_tests.sh` | dev-tool | Local `e2e_test` crate runner (starts a server, applies filters, cleans up) | `crates/e2e_test/README.md` |
| `run_scanner_heal_g09_upgrade_evidence.sh` | dev-tool | Runs the Scanner/Heal G09 mixed-version and rollback upgrade evidence lanes against the pinned previous Linux release binary | e2e-upgrade workflow constants; backlog#2269 |
| `run.sh` | dev-tool | Local rustfs startup wrapper | `make e2e-server`; Justfile |
| `run.ps1` | dev-tool | Windows counterpart of `run.sh` | — |
| `probe.sh` | dev-tool | Probe-style e2e run | `make probe-e2e` |
| `run_scanner_validation_harness.sh` | dev-tool | Scanner validation harness | `docs/operations/scanner-benchmark-runbook.md` |
| `run_scanner_heal_evidence_case.sh` | dev-tool | Runs one Scanner/Heal release-evidence registry case and checks the produced receipt/oracle | `.config/scanner-heal-required-tests.json`; `check_test_wiring.py --check-scanner-heal` |
| `run_scanner_heal_g09_upgrade_evidence.sh` | dev-tool | Runs the G09 mixed-version and rollback upgrade E2E lanes against a pinned previous release and verifies the raw evidence artifacts | `docs/testing/ci-gates.md`; `.github/workflows/e2e-upgrade.yml` |
| `test_scanner_validation_harness.sh` | dev-tool | Self-test for the scanner validation harness | — |
| `scanner_abba.py` | dev-tool | Scanner/heal ABBA orchestration and evidence gates via `run_scanner_validation_harness.sh --abba` | `docs/operations/scanner-benchmark-runbook.md` |
| `test_scanner_abba.py` | dev-tool | Synthetic ABBA adapter and failure-path tests | `test_scanner_validation_harness.sh` |
+349 -127
View File
@@ -1,159 +1,381 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
UPGRADE_WORKFLOW="${PROJECT_ROOT}/.github/workflows/e2e-upgrade.yml"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN="${RUSTFS_PYTHON_BIN:-python3}"
SOURCE_REPOSITORY="${RUSTFS_UPGRADE_SOURCE_REPOSITORY:-rustfs/rustfs}"
SOURCE_VERSION="${RUSTFS_UPGRADE_SOURCE_VERSION:-1.0.0-rc.5}"
SOURCE_ASSET="${RUSTFS_UPGRADE_SOURCE_ASSET:-rustfs-linux-x86_64-gnu-v1.0.0-rc.5.zip}"
SOURCE_SHA256="${RUSTFS_UPGRADE_SOURCE_SHA256:-3ee8df71e8edcfada533be452c4135868f697bc515460ae97b027313eade7a3d}"
RUN_DIR=""
SOURCE_DIR=""
OUT_DIR=""
SKIP_BUILD="false"
SOURCE_BINARY="${RUSTFS_UPGRADE_SOURCE_BINARY:-}"
TEST_SELECTION="all"
PLAN_ONLY=0
ALLOW_DIRTY=0
SKIP_BUILD=0
VERBOSE=0
usage() {
cat <<'USAGE'
Usage:
scripts/run_scanner_heal_g09_upgrade_evidence.sh [--source-dir <dir>] [--out-dir <dir>] [--skip-build]
cat <<'USAGE'
Usage: scripts/run_scanner_heal_g09_upgrade_evidence.sh [OPTIONS]
Runs the Scanner/Heal G09 upgrade evidence lanes against the pinned previous
Linux x86_64 RustFS release used by the e2e-upgrade workflow:
Build the current checkout, run the Scanner/Heal G09 upgrade compatibility
lanes, and validate the raw mixed-version/rollback evidence artifacts.
- rolling mixed-version reader/writer evidence
- rollback payload replay evidence
Options:
--run-dir DIR New evidence directory (default: target/scanner-heal-g09-evidence/TIMESTAMP)
--out-dir DIR Alias for --run-dir
--source-dir DIR Cache directory for the pinned previous release binary
--source-binary BIN Use an existing previous-release rustfs binary
--test NAME all, mixed-version, or rollback (default: all)
--allow-dirty Allow tracked source changes while collecting evidence
--skip-build Reuse an existing target/debug/rustfs binary
--plan-only Print the resolved plan without building or running tests
--self-test Run lightweight CLI and artifact-validator checks
--verbose Stream command output instead of storing it under the run directory
-h, --help Show this help
The script builds the current PR head by default, downloads and verifies the
pinned previous release binary, runs the ignored e2e tests, and fails unless all
G09 JSON evidence artifacts are present and non-empty.
The default pinned release asset is Linux x86_64. Use --source-binary when
running against a custom previous-release binary on another platform.
USAGE
}
die() {
echo "ERROR: $*" >&2
exit 1
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--source-dir)
[[ $# -ge 2 ]] || die "--source-dir requires a value"
SOURCE_DIR="$2"
shift 2
;;
--out-dir)
[[ $# -ge 2 ]] || die "--out-dir requires a value"
OUT_DIR="$2"
shift 2
;;
--skip-build)
SKIP_BUILD="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown arg: $1"
;;
case_names() {
case "$TEST_SELECTION" in
all)
printf '%s\n' mixed-version rollback
;;
mixed-version|rollback)
printf '%s\n' "$TEST_SELECTION"
;;
*)
echo "unknown test selection: $TEST_SELECTION" >&2
exit 2
;;
esac
done
}
workflow_env_value() {
local key="$1"
awk -v key="${key}:" '$1 == key { gsub(/["'\'']/, "", $2); print $2; exit }' "$UPGRADE_WORKFLOW"
artifact_for() {
case "$1" in
mixed-version)
echo "mixed-version-upgrade"
;;
rollback)
echo "bucket-config-rollback"
;;
*)
echo "unknown G09 case: $1" >&2
exit 2
;;
esac
}
require_linux_x86_64() {
local kernel machine
kernel="$(uname -s)"
machine="$(uname -m)"
[[ "$kernel" == "Linux" ]] || die "G09 upgrade evidence uses the pinned Linux release binary; run this on Linux"
[[ "$machine" == "x86_64" || "$machine" == "amd64" ]] || die "G09 upgrade evidence requires x86_64/amd64"
test_filter_for() {
case "$1" in
mixed-version)
echo "upgrade_compatibility_test::rolling_upgrade_from_rc2_preserves_mixed_version_contracts"
;;
rollback)
echo "upgrade_compatibility_test::rollback_to_previous_release_reads_current_bucket_metadata"
;;
*)
echo "unknown G09 case: $1" >&2
exit 2
;;
esac
}
source_binary_path() {
printf '%s/rustfs\n' "$SOURCE_DIR"
normalize_path() {
local path="$1"
if [[ "$path" == /* ]]; then
echo "$path"
else
echo "$ROOT/$path"
fi
}
ensure_previous_release_binary() {
local version asset sha archive url binary
version="${UPGRADE_SOURCE_VERSION:-$(workflow_env_value UPGRADE_SOURCE_VERSION)}"
asset="${UPGRADE_SOURCE_ASSET:-$(workflow_env_value UPGRADE_SOURCE_ASSET)}"
sha="${UPGRADE_SOURCE_SHA256:-$(workflow_env_value UPGRADE_SOURCE_SHA256)}"
[[ -n "$version" ]] || die "UPGRADE_SOURCE_VERSION is missing"
[[ -n "$asset" ]] || die "UPGRADE_SOURCE_ASSET is missing"
[[ -n "$sha" ]] || die "UPGRADE_SOURCE_SHA256 is missing"
SOURCE_DIR="${SOURCE_DIR:-${PROJECT_ROOT}/target/scanner-heal-g09-source/${version}}"
mkdir -p "$SOURCE_DIR"
binary="$(source_binary_path)"
if [[ -x "$binary" ]]; then
return
fi
archive="${SOURCE_DIR}/${asset}"
url="https://github.com/rustfs/rustfs/releases/download/${version}/${asset}"
curl --fail --location --retry 3 --output "$archive" "$url"
echo "${sha} ${archive}" | sha256sum --check --strict
unzip -q -o "$archive" -d "$SOURCE_DIR"
chmod +x "$binary"
[[ -x "$binary" ]] || die "downloaded archive did not provide an executable rustfs binary"
ensure_default_asset_platform() {
if [[ -n "$SOURCE_BINARY" ]]; then
return
fi
local os arch
os="$(uname -s)"
arch="$(uname -m)"
if [[ "$os" != "Linux" || ( "$arch" != "x86_64" && "$arch" != "amd64" ) ]]; then
echo "default previous-release asset requires Linux x86_64; pass --source-binary for this platform" >&2
exit 2
fi
}
build_current_rustfs() {
if [[ "$SKIP_BUILD" == "true" ]]; then
return
fi
"${CARGO:-cargo}" build --locked -p rustfs --bin rustfs
: > "${PROJECT_ROOT}/target/debug/rustfs.features"
verify_sha256() {
local archive="$1"
if command -v sha256sum >/dev/null 2>&1; then
printf '%s %s\n' "$SOURCE_SHA256" "$archive" | sha256sum --check --strict
elif command -v shasum >/dev/null 2>&1; then
printf '%s %s\n' "$SOURCE_SHA256" "$archive" | shasum -a 256 --check
else
echo "sha256sum or shasum is required to verify $SOURCE_ASSET" >&2
exit 1
fi
}
prepare_output_dir() {
local stamp
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
OUT_DIR="${OUT_DIR:-${PROJECT_ROOT}/target/scanner-heal-g09-evidence/${stamp}}"
mkdir -p "$OUT_DIR"
resolve_source_binary() {
if [[ -n "$SOURCE_BINARY" ]]; then
SOURCE_BINARY="$(normalize_path "$SOURCE_BINARY")"
test -x "$SOURCE_BINARY"
echo "$SOURCE_BINARY"
return
fi
ensure_default_asset_platform
if [[ -z "$SOURCE_DIR" ]]; then
SOURCE_DIR="$ROOT/target/scanner-heal-g09-source/$SOURCE_VERSION"
else
SOURCE_DIR="$(normalize_path "$SOURCE_DIR")"
fi
local binary="$SOURCE_DIR/rustfs"
if [[ -x "$binary" ]]; then
echo "$binary"
return
fi
mkdir -p "$SOURCE_DIR"
local archive="$SOURCE_DIR/$SOURCE_ASSET"
curl --fail --location --retry 3 --output "$archive" \
"https://github.com/$SOURCE_REPOSITORY/releases/download/$SOURCE_VERSION/$SOURCE_ASSET"
verify_sha256 "$archive"
unzip -q "$archive" -d "$SOURCE_DIR"
chmod +x "$binary"
test -x "$binary"
echo "$binary"
}
run_case() {
local artifact="$1"
local test_name="$2"
shift 2
local case_dir="${OUT_DIR}/${artifact}"
mkdir -p "$case_dir"
RUSTFS_UPGRADE_SOURCE_BINARY="$(source_binary_path)" \
RUSTFS_E2E_LOG_DIR="${OUT_DIR}/server-logs/${artifact}" \
RUSTFS_SCANNER_HEAL_G09_EVIDENCE_DIR="$case_dir" \
"${CARGO:-cargo}" test --locked -p e2e_test \
"upgrade_compatibility_test::${test_name}" \
-- --ignored --exact --nocapture
local expected
for expected in "$@"; do
[[ -s "${case_dir}/${expected}" ]] || die "missing non-empty ${artifact}/${expected}"
done
run_logged() {
local label="$1"
shift
local log="$RUN_DIR/logs/$label.log"
mkdir -p "$(dirname "$log")"
if [[ "$VERBOSE" == 1 ]]; then
"$@"
return
fi
if ! "$@" >"$log" 2>&1; then
echo "$label failed; log: $log" >&2
tail -80 "$log" >&2 || true
return 1
fi
echo "PASS: $label"
}
main() {
parse_args "$@"
[[ -f "$UPGRADE_WORKFLOW" ]] || die "missing e2e-upgrade workflow"
require_linux_x86_64
cd "$PROJECT_ROOT"
ensure_previous_release_binary
build_current_rustfs
prepare_output_dir
run_case \
"mixed-version-upgrade" \
"rolling_upgrade_from_rc2_preserves_mixed_version_contracts" \
"G09-mixed_version_reader_evidence.json" \
"G09-mixed_version_writer_evidence.json"
run_case \
"bucket-config-rollback" \
"rollback_to_previous_release_reads_current_bucket_metadata" \
"G09-rollback_payload_evidence.json"
echo "PASS: Scanner/Heal G09 upgrade evidence written under ${OUT_DIR}"
validate_artifacts() {
local source_revision="$1"
"$PYTHON_BIN" - "$RUN_DIR" "$source_revision" "$TEST_SELECTION" <<'PY'
import json
import pathlib
import sys
run_dir = pathlib.Path(sys.argv[1])
source_revision = sys.argv[2]
selection = sys.argv[3]
expected = {
"mixed-version": [
("mixed-version-upgrade/G09-mixed_version_reader_evidence.json",
"mixed_version_reader_evidence", "mixed-version-reader"),
("mixed-version-upgrade/G09-mixed_version_writer_evidence.json",
"mixed_version_writer_evidence", "mixed-version-writer"),
],
"rollback": [
("bucket-config-rollback/G09-rollback_payload_evidence.json",
"rollback_payload_evidence", "rollback-payload"),
],
}
main "$@"
if selection != "all":
expected = {selection: expected[selection]}
for _, artifacts in expected.items():
for relative, field, role in artifacts:
path = run_dir / relative
if not path.is_file():
raise SystemExit(f"missing G09 evidence artifact: {relative}")
evidence = json.loads(path.read_text())
if evidence.get("schema") != 1:
raise SystemExit(f"{relative}: expected schema 1")
if evidence.get("evidence_type") != "measured":
raise SystemExit(f"{relative}: expected measured evidence")
if evidence.get("artifact_kind") != "upgrade-compatibility-e2e":
raise SystemExit(f"{relative}: unexpected artifact kind")
if evidence.get("gate") != "G09":
raise SystemExit(f"{relative}: unexpected gate")
if evidence.get("field") != field:
raise SystemExit(f"{relative}: expected field {field}")
if evidence.get("mixed_version_role") != role:
raise SystemExit(f"{relative}: expected role {role}")
if evidence.get("current_revision") != source_revision:
raise SystemExit(f"{relative}: current revision does not match this checkout")
versions = evidence.get("versions")
if not isinstance(versions, list) or len(versions) != 2:
raise SystemExit(f"{relative}: versions must contain previous and current revisions")
if versions[0] == versions[1]:
raise SystemExit(f"{relative}: previous and current revisions must differ")
if field == "rollback_payload_evidence" and evidence.get("rollback_payload_replayed") is not True:
raise SystemExit(f"{relative}: rollback payload was not marked replayed")
print("PASS: G09 raw evidence artifacts verified")
PY
}
run_self_test() {
local tmp
tmp="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-g09-evidence-self-test.XXXXXX")"
trap "rm -rf '$tmp'" EXIT
local plan
plan="$("$0" --plan-only --run-dir "$tmp/run" --source-binary "$tmp/rustfs-prev" --test all)"
[[ "$plan" == *"tests=mixed-version rollback"* ]]
[[ "$plan" == *"run_dir=$tmp/run"* ]]
if "$0" --plan-only --test not-a-case >/dev/null 2>&1; then
echo "self-test failed: invalid test selection was accepted" >&2
return 1
fi
mkdir -p "$tmp/run/mixed-version-upgrade" "$tmp/run/bucket-config-rollback"
local current previous
current="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
previous="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
cat >"$tmp/run/mixed-version-upgrade/G09-mixed_version_reader_evidence.json" <<JSON
{"schema":1,"evidence_type":"measured","artifact_kind":"upgrade-compatibility-e2e","gate":"G09","field":"mixed_version_reader_evidence","mixed_version_role":"mixed-version-reader","current_revision":"$current","previous_revision":"$previous","versions":["$previous","$current"]}
JSON
cat >"$tmp/run/mixed-version-upgrade/G09-mixed_version_writer_evidence.json" <<JSON
{"schema":1,"evidence_type":"measured","artifact_kind":"upgrade-compatibility-e2e","gate":"G09","field":"mixed_version_writer_evidence","mixed_version_role":"mixed-version-writer","current_revision":"$current","previous_revision":"$previous","versions":["$previous","$current"]}
JSON
cat >"$tmp/run/bucket-config-rollback/G09-rollback_payload_evidence.json" <<JSON
{"schema":1,"evidence_type":"measured","artifact_kind":"upgrade-compatibility-e2e","gate":"G09","field":"rollback_payload_evidence","mixed_version_role":"rollback-payload","current_revision":"$current","previous_revision":"$previous","versions":["$previous","$current"],"rollback_payload_replayed":true}
JSON
RUN_DIR="$tmp/run" TEST_SELECTION="all" validate_artifacts "$current" >/dev/null
}
while [[ $# -gt 0 ]]; do
case "$1" in
--run-dir|--out-dir)
RUN_DIR="$2"
shift 2
;;
--source-dir)
SOURCE_DIR="$2"
shift 2
;;
--source-binary)
SOURCE_BINARY="$2"
shift 2
;;
--test)
TEST_SELECTION="$2"
shift 2
;;
--allow-dirty)
ALLOW_DIRTY=1
shift
;;
--skip-build)
SKIP_BUILD=1
shift
;;
--plan-only)
PLAN_ONLY=1
shift
;;
--self-test)
run_self_test
exit $?
;;
--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
CASES=()
while IFS= read -r case_name; do
CASES+=("$case_name")
done < <(case_names)
if [[ -z "$RUN_DIR" ]]; then
RUN_DIR="$ROOT/target/scanner-heal-g09-evidence/$(date -u +%Y%m%dT%H%M%SZ)"
else
RUN_DIR="$(normalize_path "$RUN_DIR")"
fi
if [[ "$PLAN_ONLY" == 1 ]]; then
echo "run_dir=$RUN_DIR"
echo "tests=${CASES[*]}"
echo "source_repository=$SOURCE_REPOSITORY"
echo "source_version=$SOURCE_VERSION"
if [[ -n "$SOURCE_BINARY" ]]; then
echo "source_binary=$(normalize_path "$SOURCE_BINARY")"
else
if [[ -z "$SOURCE_DIR" ]]; then
echo "source_dir=$ROOT/target/scanner-heal-g09-source/$SOURCE_VERSION"
else
echo "source_dir=$(normalize_path "$SOURCE_DIR")"
fi
echo "source_asset=$SOURCE_ASSET"
fi
exit 0
fi
cd "$ROOT"
if [[ "$ALLOW_DIRTY" != 1 && -n "$(git status --porcelain --untracked-files=no)" ]]; then
echo "commit tracked source changes before creating release evidence, or pass --allow-dirty for local diagnostics" >&2
exit 1
fi
if [[ -e "$RUN_DIR" ]]; then
echo "evidence run directory already exists: $RUN_DIR" >&2
exit 1
fi
mkdir -p "$RUN_DIR/logs"
SOURCE_BINARY="$(resolve_source_binary)"
export RUSTFS_UPGRADE_SOURCE_BINARY="$SOURCE_BINARY"
export RUSTFS_E2E_LOG_DIR="${RUSTFS_E2E_LOG_DIR:-$RUN_DIR/server-logs}"
mkdir -p "$RUSTFS_E2E_LOG_DIR"
if [[ "$SKIP_BUILD" != 1 ]]; then
run_logged build-current cargo build --locked -p rustfs --bin rustfs
: > "$ROOT/target/debug/rustfs.features"
fi
SOURCE_REVISION="$(git rev-parse HEAD)"
printf '%s\n' "$SOURCE_REVISION" >"$RUN_DIR/source-revision.txt"
printf '%s\n' "$SOURCE_VERSION" >"$RUN_DIR/previous-release-version.txt"
for case_name in "${CASES[@]}"; do
artifact="$(artifact_for "$case_name")"
test_filter="$(test_filter_for "$case_name")"
evidence_dir="$RUN_DIR/$artifact"
mkdir -p "$evidence_dir"
run_logged "$case_name" env \
NO_PROXY="${NO_PROXY:-127.0.0.1,localhost}" \
HTTP_PROXY= \
HTTPS_PROXY= \
RUSTFS_SCANNER_HEAL_G09_EVIDENCE_DIR="$evidence_dir" \
cargo test --locked -p e2e_test "$test_filter" -- --ignored --exact --nocapture
done
validate_artifacts "$SOURCE_REVISION"
echo "Scanner/Heal G09 evidence verified: $RUN_DIR"