mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-31 16:22:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4480d7efca | |||
| dcf63411f0 | |||
| 8adc1a3082 |
@@ -96,7 +96,7 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
# Readers: test-and-lint (workspace and ILM), build-rustfs-debug-binary,
|
||||
# Readers: workspace-lint-and-build, workspace-tests (workspace and ILM), build-rustfs-debug-binary,
|
||||
# e2e-tests, e2e-full.
|
||||
warm-ci-dev:
|
||||
name: Warm ci-dev
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
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
|
||||
# e2e_test, which the workspace 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
|
||||
|
||||
+36
-34
@@ -113,7 +113,7 @@ jobs:
|
||||
printf '%s\n' 'mode=full' >> "$GITHUB_OUTPUT"
|
||||
echo "Base CI policy unavailable; running the full matrix."
|
||||
fi
|
||||
if ! grep -q '^plan=' "$GITHUB_OUTPUT" || ! grep -q '^rehearse_shards=' "$GITHUB_OUTPUT"; then
|
||||
if ! grep -q '^plan=' "$GITHUB_OUTPUT" || ! grep -q '^plan_contract=2$' "$GITHUB_OUTPUT"; then
|
||||
printf '%s\n' 'mode=full' 'connect=true' 'full_validation=true' 'rehearse_shards=false' 'packages=' 'plan=' >> "$GITHUB_OUTPUT"
|
||||
echo "Base policy does not cover this workflow contract; retaining the full matrix."
|
||||
fi
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
- name: Run shared quick checks
|
||||
uses: ./.github/actions/quick-checks
|
||||
|
||||
test-and-lint:
|
||||
workspace-lint-and-build:
|
||||
name: Workspace Lint and Build
|
||||
if: contains(fromJSON('["full", "e2e"]'), needs.classify-changes.outputs.mode) && (github.event_name != 'pull_request' || github.event.action != 'closed')
|
||||
needs: [ quick-checks, classify-changes ]
|
||||
@@ -193,13 +193,13 @@ jobs:
|
||||
|
||||
- name: Prepare test evidence
|
||||
run: |
|
||||
mkdir -p artifacts/test-and-lint
|
||||
mkdir -p artifacts/workspace
|
||||
{
|
||||
echo "run_id=${GITHUB_RUN_ID}"
|
||||
echo "job=${GITHUB_JOB}"
|
||||
echo "runner=${RUNNER_NAME}"
|
||||
echo "started_at=$(date --utc --iso-8601=seconds)"
|
||||
} > artifacts/test-and-lint/run-metadata.txt
|
||||
} > artifacts/workspace/run-metadata.txt
|
||||
|
||||
# Clippy gates every event, pull requests included. When it ran only after
|
||||
# merge, a lint regression merged freely and then failed every main run
|
||||
@@ -233,11 +233,11 @@ jobs:
|
||||
|
||||
- name: Run documentation tests
|
||||
run: |
|
||||
mkdir -p artifacts/test-and-lint
|
||||
mkdir -p artifacts/workspace
|
||||
set +e
|
||||
timeout --verbose --signal=TERM --kill-after=30s 15m \
|
||||
python3 scripts/ci_test_artifact.py cargo doc --configuration workspace \
|
||||
2>&1 | tee artifacts/test-and-lint/doctest.log
|
||||
2>&1 | tee artifacts/workspace/doctest.log
|
||||
status=${PIPESTATUS[0]}
|
||||
{
|
||||
echo "command=cargo test --all --exclude e2e_test --doc"
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
echo
|
||||
echo "Remaining test-related processes:"
|
||||
pgrep -af 'cargo|rustdoc|target/.*/deps/' || true
|
||||
} > artifacts/test-and-lint/doctest-diagnostics.txt
|
||||
} > artifacts/workspace/doctest-diagnostics.txt
|
||||
exit "${status}"
|
||||
|
||||
- name: Upload test reports and diagnostics
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
with:
|
||||
name: junit-test-and-lint-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
artifacts/test-and-lint
|
||||
artifacts/workspace
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
|
||||
workspace-tests:
|
||||
name: Workspace tests
|
||||
needs: [ test-and-lint, classify-changes ]
|
||||
needs: [ workspace-lint-and-build, classify-changes ]
|
||||
# The archive retains compile-time fixture paths and the large test set
|
||||
# requires the same runner image until the hosted-runtime pilot passes.
|
||||
runs-on: sm-standard-4
|
||||
@@ -293,14 +293,14 @@ jobs:
|
||||
|
||||
- name: Extract and list workspace tests
|
||||
run: |
|
||||
mkdir -p artifacts/test-and-lint
|
||||
mkdir -p artifacts/workspace
|
||||
cargo nextest list --profile ci --archive-file "$RUNNER_TEMP/workspace-tests.tar.zst" --workspace-remap "$GITHUB_WORKSPACE" --extract-to "$GITHUB_WORKSPACE" --extract-overwrite --message-format json \
|
||||
> artifacts/test-and-lint/core-test-listing.json
|
||||
python3 scripts/check_test_wiring.py --check-core artifacts/test-and-lint/core-test-listing.json
|
||||
> artifacts/workspace/core-test-listing.json
|
||||
python3 scripts/check_test_wiring.py --check-core artifacts/workspace/core-test-listing.json
|
||||
|
||||
- name: Run nextest tests
|
||||
run: |
|
||||
mkdir -p artifacts/test-and-lint
|
||||
mkdir -p artifacts/workspace
|
||||
rm -f target/nextest/ci/junit.xml
|
||||
./scripts/ci/resource_sampler.sh start nextest
|
||||
trap './scripts/ci/resource_sampler.sh stop' EXIT
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
cargo nextest run --profile ci --cargo-metadata target/nextest/cargo-metadata.json --binaries-metadata target/nextest/binaries-metadata.json \
|
||||
--workspace-remap "$GITHUB_WORKSPACE" --target-dir-remap "$GITHUB_WORKSPACE/target" \
|
||||
--status-level all --final-status-level all \
|
||||
2>&1 | tee artifacts/test-and-lint/nextest.log
|
||||
2>&1 | tee artifacts/workspace/nextest.log
|
||||
status=${PIPESTATUS[0]}
|
||||
{
|
||||
echo "command=workspace archive execution"
|
||||
@@ -321,13 +321,13 @@ jobs:
|
||||
echo
|
||||
echo "Kernel OOM / kill events:"
|
||||
dmesg -T 2>/dev/null | grep -iE 'oom|out of memory|killed process' | tail -20 || true
|
||||
} > artifacts/test-and-lint/nextest-diagnostics.txt
|
||||
} > artifacts/workspace/nextest-diagnostics.txt
|
||||
exit "${status}"
|
||||
|
||||
- name: Verify workspace execution completeness
|
||||
run: |
|
||||
python3 scripts/ci_test_artifact.py evidence artifacts/test-and-lint/core-test-listing.json target/nextest/ci/junit.xml
|
||||
./scripts/check_migration_gate_count.sh evidence artifacts/test-and-lint/core-test-listing.json target/nextest/ci/junit.xml
|
||||
python3 scripts/ci_test_artifact.py evidence artifacts/workspace/core-test-listing.json target/nextest/ci/junit.xml
|
||||
./scripts/check_migration_gate_count.sh evidence artifacts/workspace/core-test-listing.json target/nextest/ci/junit.xml
|
||||
|
||||
- name: Run ignored ILM integration tests serially
|
||||
if: needs.classify-changes.outputs.mode == 'full'
|
||||
@@ -358,7 +358,7 @@ jobs:
|
||||
name: workspace-test-evidence-${{ github.run_attempt }}
|
||||
path: |
|
||||
target/nextest/ci/junit.xml
|
||||
artifacts/test-and-lint
|
||||
artifacts/workspace
|
||||
artifacts/ilm-integration
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
@@ -368,7 +368,7 @@ jobs:
|
||||
if: needs.classify-changes.outputs.rehearse_shards == 'true'
|
||||
run: |
|
||||
python3 scripts/ci_test_shards.py prepare \
|
||||
--listing artifacts/test-and-lint/core-test-listing.json \
|
||||
--listing artifacts/workspace/core-test-listing.json \
|
||||
--junit target/nextest/ci/junit.xml --output artifacts/workspace-shard-plan
|
||||
- name: Upload same-run shard plan
|
||||
if: needs.classify-changes.outputs.rehearse_shards == 'true'
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
|
||||
connect-feature-boundaries:
|
||||
name: Connect Feature Boundaries
|
||||
if: needs.classify-changes.outputs.mode == 'full' && needs.classify-changes.outputs.connect != 'false' && (github.event_name != 'pull_request' || github.event.action != 'closed')
|
||||
if: needs.classify-changes.outputs.mode == 'full' && needs.classify-changes.outputs.connect != 'false' && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name)
|
||||
needs: [ quick-checks, classify-changes ]
|
||||
runs-on: sm-standard-4
|
||||
timeout-minutes: 90
|
||||
@@ -499,11 +499,13 @@ jobs:
|
||||
|
||||
test-and-lint-rio-v2:
|
||||
name: Test and Lint (rio-v2)
|
||||
# Alternate feature configurations run on the nightly schedule, merge
|
||||
# groups and manual dispatch. The default workspace remains the PR gate and
|
||||
# the per-push main baseline: repeating four feature builds on every merge
|
||||
# bought nothing the nightly run does not report within a day.
|
||||
if: needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('["merge_group", "schedule", "workflow_dispatch"]'), github.event_name)
|
||||
# Specialized boundary, hardware and alternate-feature lanes run only on
|
||||
# the nightly schedule and manual dispatch. They do not gate a merge: the
|
||||
# nightly run reports a regression within a day, and repeating these builds
|
||||
# on every PR revision and merge group bought no earlier signal worth the
|
||||
# runner occupancy. Workspace lint and tests, the core contract, the debug
|
||||
# build, E2E smoke and the S3 lanes remain the pull-request gate.
|
||||
if: needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name)
|
||||
needs: [ quick-checks, classify-changes ]
|
||||
runs-on: sm-standard-4
|
||||
timeout-minutes: 90
|
||||
@@ -540,12 +542,12 @@ jobs:
|
||||
# --profile ci so the quarantine list (and its junit flaky markers)
|
||||
# covers this leg too; the default profile is the local no-retry
|
||||
# profile and silently ignored quarantined flakes here (rustfs#6703).
|
||||
mkdir -p artifacts/test-and-lint
|
||||
mkdir -p artifacts/workspace
|
||||
rm -f target/nextest/ci/junit.xml
|
||||
./scripts/ci/resource_sampler.sh start nextest-rio-v2
|
||||
trap './scripts/ci/resource_sampler.sh stop' EXIT
|
||||
cargo nextest run --profile ci -p rustfs -p rustfs-ecstore --features rio-v2 \
|
||||
2>&1 | tee artifacts/test-and-lint/nextest.log
|
||||
2>&1 | tee artifacts/workspace/nextest.log
|
||||
cargo test -p rustfs --doc --features rio-v2
|
||||
|
||||
- name: Upload rio-v2 test reports and diagnostics
|
||||
@@ -555,14 +557,14 @@ jobs:
|
||||
name: junit-test-and-lint-rio-v2-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
target/nextest/ci/junit.xml
|
||||
artifacts/test-and-lint
|
||||
artifacts/workspace
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
test-and-lint-protocols:
|
||||
name: "Test and Lint (${{ matrix.features.name }})"
|
||||
# Same cadence as test-and-lint-rio-v2 above.
|
||||
if: needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('["merge_group", "schedule", "workflow_dispatch"]'), github.event_name)
|
||||
if: needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name)
|
||||
needs: [ quick-checks, classify-changes ]
|
||||
runs-on: sm-standard-4
|
||||
timeout-minutes: 90
|
||||
@@ -631,7 +633,7 @@ jobs:
|
||||
path: |
|
||||
target/nextest/ci/junit.xml
|
||||
artifacts/protocol-tests
|
||||
artifacts/test-and-lint
|
||||
artifacts/workspace
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -783,7 +785,7 @@ jobs:
|
||||
# job had neither, so each closed/merged PR really ran the whole io_uring
|
||||
# suite (measured 4m17s / 7m19s / 7m31s on runs 30678272341 / 30678117601 /
|
||||
# 30662728539) and kept the cancellation run in progress for minutes.
|
||||
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
|
||||
if: needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name)
|
||||
needs: [ quick-checks, classify-changes ]
|
||||
# GitHub-hosted ubuntu-latest runs a recent kernel with io_uring and, unlike
|
||||
# a container, applies no seccomp filter that would block io_uring_setup — so
|
||||
@@ -978,7 +980,7 @@ jobs:
|
||||
path: |
|
||||
${{ runner.temp }}/rustfs-e2e-smoke-logs/
|
||||
${{ runner.temp }}/rustfs-e2e-smoke-list.json
|
||||
artifacts/test-and-lint
|
||||
artifacts/workspace
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload e2e smoke JUnit report
|
||||
@@ -1389,7 +1391,7 @@ jobs:
|
||||
- classify-changes
|
||||
- typos
|
||||
- quick-checks
|
||||
- test-and-lint
|
||||
- workspace-lint-and-build
|
||||
- workspace-tests
|
||||
- workspace-shards
|
||||
- workspace-shard-evidence
|
||||
@@ -1425,7 +1427,7 @@ jobs:
|
||||
- required-checks
|
||||
- typos
|
||||
- quick-checks
|
||||
- test-and-lint
|
||||
- workspace-lint-and-build
|
||||
- workspace-tests
|
||||
- core-contract
|
||||
- test-and-lint-rio-v2
|
||||
|
||||
@@ -18,7 +18,7 @@ Every PR enters `ci.yml`. The `classify-changes` job uses the base revision of `
|
||||
|
||||
PRs changing only Rust sources under `crates/e2e_test/src/` and known E2E selection digests (optionally with those documentation paths) use the `e2e` scope. They keep workspace validation, the server build, E2E smoke, and both S3 lanes; independent workflows retain their path selection. Connect boundary, serial ILM, optional protocol/rio-v2, and real io_uring jobs are skipped because their sources and test inputs are unchanged. Manifests, fixtures, shared nextest configuration, scripts, production sources, and unknown paths still select `full`. A dependency on `e2e_test` from another workspace member or reachable local dependency disables this shortcut. All non-PR events retain their full selection. The structured planner enforces affected-package and Connect selection from the trusted base policy, retaining fixed core coverage and full validation for shared or unknown inputs. See [verification plans and artifacts](ci-verification-pipeline.md) for identity, rollout and queue-switch contracts.
|
||||
|
||||
Alternate-feature unit tests and Clippy for rio-v2, Swift, SFTP, and HTTP/3 run on merge groups and on scheduled (nightly) or manual validation. PRs and main pushes run the default workspace and retain ILM, S3, real io_uring, and Connect security boundaries in their applicable scopes. The aggregate requires the alternate-feature jobs to be skipped on PRs and main pushes and successful on merge-group, scheduled, and manual events. This avoids repeating four feature builds on every PR revision and on every merge; feature-specific regressions are detected by the nightly run when the merge queue is not used. For changes to these features, dispatch `ci.yml` against the candidate branch before merging when that signal is needed, including PRs targeting `gateway/integration`, which has no automatic push trigger in this workflow. Scheduled results supplement the main baseline and do not replace it.
|
||||
Specialized boundary, hardware and alternate-feature lanes run only on the scheduled (nightly) and manual full-validation runs: `Connect Feature Boundaries`, `io_uring Integration (real)`, and the rio-v2, Swift, SFTP and HTTP/3 feature lanes. They do not gate a merge. The nightly run reports their regressions within a day, and repeating these builds on every PR revision and merge group bought no earlier signal worth the runner occupancy (Connect alone measured 53.5 median minutes per run and occasionally set the wall). PRs, merge groups and main pushes run the default workspace and retain ILM, S3, E2E smoke, the short core contract and the debug build. The aggregate requires the specialized lanes to be skipped on pull-request, merge-group and push events and successful on scheduled and manual events. For changes to these features, dispatch `ci.yml` against the candidate branch before merging when that signal is needed, including PRs targeting `gateway/integration`, which has no automatic push trigger in this workflow. Scheduled results supplement the main baseline and do not replace it.
|
||||
|
||||
Workspace Clippy runs on every event with a selected workspace Rust job, pull requests included, in `Workspace Lint and Build`. That job also creates the exact test archive and runs doctests; `Workspace tests` consumes the archive without rebuilding it. `cache-warm.yml` warms Clippy's check artifacts under `ci-dev` with the same invocation to reuse compatible dependency artifacts. `make pre-commit` does not run Clippy. Select local Clippy targets, features, and documentation exemptions from the root `AGENTS.md` verification tier; the workspace command below reproduces the CI lane and is not a universal pre-push requirement.
|
||||
|
||||
@@ -50,13 +50,13 @@ The four site-replication state-writer concurrency proofs reserve the available
|
||||
| Event | Check name | Workflow / job | Merge status | Reproduce |
|
||||
|---|---|---|---|---|
|
||||
| PR, non-doc change | `Quick Checks` | `ci.yml` `quick-checks` | Required | `make pre-commit` |
|
||||
| PR, non-doc change, and every non-PR event | `Workspace Lint and Build`, `Workspace tests` | `ci.yml` `test-and-lint` / `workspace-tests` | Via aggregate | `cargo clippy --all-targets -- -D warnings`; `cargo nextest run --profile ci --all --exclude e2e_test`; `cargo test --all --exclude e2e_test --doc`; migration evidence check described below |
|
||||
| PR, non-doc change, and every non-PR event | `Workspace Lint and Build`, `Workspace tests` | `ci.yml` `workspace-lint-and-build` / `workspace-tests` | Via aggregate | `cargo clippy --all-targets -- -D warnings`; `cargo nextest run --profile ci --all --exclude e2e_test`; `cargo test --all --exclude e2e_test --doc`; migration evidence check described below |
|
||||
| PR, non-doc change | `Typos` | `ci.yml` `typos` | Via aggregate | `typos` |
|
||||
| Full selection | `Workspace tests` (serial ILM step) | `ci.yml` `workspace-tests` | Via aggregate | exact `Run ignored ILM integration tests serially` command in the job |
|
||||
| Merge group, nightly schedule, manual dispatch | `Test and Lint (rio-v2)`, `Test and Lint (swift)`, `Test and Lint (sftp)`, `Test and Lint (http3)` | `ci.yml` `test-and-lint-rio-v2`, `test-and-lint-protocols` | Via aggregate on these events; skipped on PRs and main pushes | `cargo nextest run` with the job's `--features` |
|
||||
| PR, full selection | `Connect Feature Boundaries` | `ci.yml` `connect-feature-boundaries` | Via aggregate | `cargo test -p rustfs --test connect_registration --features connect-e2e-short-credentials`; `cargo check -p rustfs --release --features connect-e2e-short-credentials` must fail; `scripts/check_offline_enrollment_e2e.sh` |
|
||||
| Nightly schedule, manual dispatch | `Test and Lint (rio-v2)`, `Test and Lint (swift)`, `Test and Lint (sftp)`, `Test and Lint (http3)` | `ci.yml` `test-and-lint-rio-v2`, `test-and-lint-protocols` | Via aggregate on these events; skipped on PRs, merge groups and main pushes | `cargo nextest run` with the job's `--features` |
|
||||
| Nightly schedule, manual dispatch, full selection | `Connect Feature Boundaries` | `ci.yml` `connect-feature-boundaries` | Via aggregate on these events; skipped on PRs, merge groups and main pushes | `cargo test -p rustfs --test connect_registration --features connect-e2e-short-credentials`; `cargo check -p rustfs --release --features connect-e2e-short-credentials` must fail; `scripts/check_offline_enrollment_e2e.sh` |
|
||||
| PR, non-doc change | `Build RustFS Debug Binary` | `ci.yml` `build-rustfs-debug-binary` | Via aggregate; prerequisite for black-box jobs | `python3 scripts/e2e_binary.py build --bins --features e2e-test-hooks` (binary plus its `rustfs.e2e.json` sidecar) |
|
||||
| PR, full selection | `io_uring Integration (real)` | `ci.yml` `uring-integration` | Via aggregate | `cargo test -p rustfs-ecstore --lib uring_ -- --test-threads=1 --nocapture` |
|
||||
| Nightly schedule, manual dispatch, full selection | `io_uring Integration (real)` | `ci.yml` `uring-integration` | Via aggregate on these events; skipped on PRs, merge groups and main pushes | `cargo test -p rustfs-ecstore --lib uring_ -- --test-threads=1 --nocapture` |
|
||||
| PR, non-doc change | `End-to-End Tests` | `ci.yml` `e2e-tests` | Via aggregate | `python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke -p e2e_test`, then `./scripts/e2e-run.sh ./target/debug/rustfs <data-dir>` under the same wrapper; membership guards `scripts/check_test_wiring.py --check-profile e2e-smoke <listing.json>` and `scripts/check_security_smoke_count.sh check <listing.json>` |
|
||||
| PR, non-doc change | `Core contract` | `ci.yml` `core-contract` | Via aggregate | existing write/read, anonymous denial and restart-byte tests via `core-contract` profile and the E2E archive |
|
||||
| PR, non-doc change | `S3 Implemented Tests` | `ci.yml` `s3-implemented-tests` | Via aggregate | build `rustfs`, then `scripts/s3-tests/run.sh` with the job's `DEPLOY_MODE` / `TEST_MODE` / `MAXFAIL` env |
|
||||
@@ -136,7 +136,7 @@ Update this file in the same PR when a job or check name changes, a workflow gai
|
||||
|
||||
## ECStore invariant selection
|
||||
|
||||
The existing `ci.yml` test-and-lint job runs the ordinary ECStore and filemeta tests. After that run, `scripts/check_test_wiring.py --check-core` checks the same nextest profile and package selection against `.config/ecstore-required-tests.json`. Every named test must exist, match the filter, and be non-ignored; the job also requires a nonempty JUnit report. This checks membership without running the tests twice. `core-test-listing.json`, JUnit, and the run log are retained in the existing test-and-lint artifact.
|
||||
The existing `ci.yml` `workspace-tests` job runs the ordinary ECStore and filemeta tests. After that run, `scripts/check_test_wiring.py --check-core` checks the same nextest profile and package selection against `.config/ecstore-required-tests.json`. Every named test must exist, match the filter, and be non-ignored; the job also requires a nonempty JUnit report. This checks membership without running the tests twice. `core-test-listing.json`, JUnit, and the run log are retained in the existing `artifacts/workspace` evidence directory.
|
||||
|
||||
The manifest records a minimum set of invariants: write quorum, metadata rollback, stale-writer lock loss, plaintext Range content, multipart cancellation, hiding uncommitted LIST versions, real MinIO metadata, corrupt part arrays, and the shared on-demand-migration source-backend contract for each provider dialect (S3, Azure, native GCS). The three contract entries live in the `rustfs` suite and reach the lane through that package's default features, so dropping `gcs` from `rustfs`'s defaults fails this check instead of silently deselecting the GCS contract (rustfs/backlog#2323). Renaming or moving a required test must update the manifest in the same change after checking the compiled listing. Extend this list as new deterministic regressions land; it is not a claim that all storage invariants are covered.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Periodic and manual suites use the responsibility registry and retain the existi
|
||||
|
||||
## Merge-queue preparation and rollback
|
||||
|
||||
The merge-group event plans and validates its actual combined SHA/tree with the full Linux and feature configuration matrix. Main pushes keep complete validation; no reuse is inferred from tree equality because commit identity and build inputs can matter. CLA remains an independent queue-entry requirement.
|
||||
The merge-group event plans and validates its actual combined SHA/tree with the default workspace matrix: workspace lint and tests, the short core contract, the debug build, E2E smoke, both S3 lanes and the full merge-gate E2E. The specialized boundary, hardware and alternate-feature lanes (Connect Feature Boundaries, real io_uring, rio-v2, Swift, SFTP, HTTP/3) run only in the scheduled and manual full-validation runs and are required to be skipped here. Main pushes keep complete validation; no reuse is inferred from tree equality because commit identity and build inputs can matter. CLA remains an independent queue-entry requirement.
|
||||
|
||||
`scripts/ci_merge_queue.py` prepares a bounded queue proposal from a live ruleset snapshot without calling a mutation API. It preserves all existing checks, reviews, bypass actors and branch conditions, adds an `ALLGREEN` squash queue with at most two concurrent builds/two merged entries, and produces the original rollback payload. Its timeout accommodates the existing full gate while measurements are collected. API fields follow [GitHub's ruleset contract](https://docs.github.com/en/rest/repos/rules).
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ filtered and malformed listing cases, and missing or altered required fixtures.
|
||||
Those cases must fail; an optional legacy fixture test that returns early is
|
||||
not evidence for a required fixture.
|
||||
|
||||
The `test-and-lint` artifact contains the native log, listing, JUnit and
|
||||
The `artifacts/workspace` evidence directory contains the native log, listing, JUnit and
|
||||
execution receipt. A native test that reopens a disk in the same process does
|
||||
not establish process-crash or power-loss durability.
|
||||
|
||||
|
||||
@@ -705,7 +705,7 @@ def check_ilm_build_budget(root: Path) -> list[str]:
|
||||
"--workspace-remap": "$GITHUB_WORKSPACE",
|
||||
"--target-dir-remap": "$GITHUB_WORKSPACE/target",
|
||||
}
|
||||
if ("test-and-lint" not in job.get("needs", []) or "--archive-file" in tokens
|
||||
if ("workspace-lint-and-build" not in job.get("needs", []) or "--archive-file" in tokens
|
||||
or any(tokens.count(flag) != 1 or tokens[tokens.index(flag) + 1:tokens.index(flag) + 2] != [value]
|
||||
for flag, value in reuse_options.items())):
|
||||
return ["ILM integration must consume the workspace producer archive"]
|
||||
@@ -729,7 +729,7 @@ def check_ilm_build_budget(root: Path) -> list[str]:
|
||||
return ["ILM integration must propagate failures"]
|
||||
if 'status=${PIPESTATUS[0]}' not in command or 'exit "${status}"' not in command or "|| true" in command.split('status=${PIPESTATUS[0]}')[0]:
|
||||
return ["ILM integration must retain the actual test exit status"]
|
||||
producer_steps = jobs.get("test-and-lint", {}).get("steps", [])
|
||||
producer_steps = jobs.get("workspace-lint-and-build", {}).get("steps", [])
|
||||
builds = [item for item in producer_steps if "ci_test_artifact.py build --configuration workspace" in item.get("run", "")]
|
||||
if len(builds) != 1:
|
||||
return ["ILM integration requires one workspace archive producer"]
|
||||
|
||||
@@ -33,7 +33,7 @@ set -uo pipefail
|
||||
# GNU date on the runners; the fallback keeps local smoke tests on macOS quiet.
|
||||
now() { date --utc --iso-8601=seconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S+00:00; }
|
||||
|
||||
LOG_DIR="artifacts/test-and-lint"
|
||||
LOG_DIR="artifacts/workspace"
|
||||
LOG_FILE="${LOG_DIR}/sampler.log"
|
||||
PID_FILE="${LOG_DIR}/.sampler.pid"
|
||||
INTERVAL="${SAMPLER_INTERVAL_SECS:-60}"
|
||||
|
||||
+25
-16
@@ -16,20 +16,29 @@ import unittest
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ALWAYS_JOBS = ("classify-changes", "typos", "quick-checks")
|
||||
CODE_JOBS = (
|
||||
"test-and-lint", "workspace-tests", "core-contract", "connect-feature-boundaries", "test-and-lint-rio-v2",
|
||||
"workspace-lint-and-build", "workspace-tests", "core-contract", "connect-feature-boundaries", "test-and-lint-rio-v2",
|
||||
"test-and-lint-protocols", "build-rustfs-debug-binary", "uring-integration",
|
||||
"e2e-tests", "s3-implemented-tests", "s3-lifecycle-behavior-tests",
|
||||
)
|
||||
# Full alternate-feature tests run on merge groups and on scheduled/manual
|
||||
# runs. Pull requests and main pushes skip them: the nightly run reports a
|
||||
# feature regression within a day, and repeating four builds per PR revision
|
||||
# or per merge bought no earlier signal.
|
||||
EXTENDED_FEATURE_JOBS = ("test-and-lint-rio-v2", "test-and-lint-protocols")
|
||||
# Specialized boundary, hardware and alternate-feature lanes do not gate a
|
||||
# merge. They run only in the scheduled (nightly) and manual full-validation
|
||||
# runs, which report a regression within a day; repeating them on every PR
|
||||
# revision and merge group bought no earlier signal worth the runner occupancy
|
||||
# (Connect alone measured 53.5 median minutes per run and occasionally set the
|
||||
# wall). Workspace lint and tests, the short core contract, the debug build,
|
||||
# E2E smoke and the S3 lanes stay on the pull-request path because they cover
|
||||
# the product's core correctness and S3 compatibility surface.
|
||||
NIGHTLY_JOBS = ("connect-feature-boundaries", "uring-integration", "test-and-lint-rio-v2", "test-and-lint-protocols")
|
||||
# Bump when a plan produced by an older `select` can no longer be checked
|
||||
# against this revision's expected job contract. The workflow refuses a plan
|
||||
# that does not carry the current marker and falls back to the full matrix, so
|
||||
# a base planner predating the contract cannot authorize a narrower selection.
|
||||
PLAN_CONTRACT = "2"
|
||||
# These jobs still exercise the server and its black-box harness when only E2E
|
||||
# Rust sources change. Production code, manifests and shared test configuration
|
||||
# always select the full matrix. No production package depends on e2e_test.
|
||||
E2E_JOBS = (
|
||||
"test-and-lint", "workspace-tests", "core-contract", "build-rustfs-debug-binary", "e2e-tests",
|
||||
"workspace-lint-and-build", "workspace-tests", "core-contract", "build-rustfs-debug-binary", "e2e-tests",
|
||||
"s3-implemented-tests", "s3-lifecycle-behavior-tests",
|
||||
)
|
||||
E2E_SELECTION_FILES = {
|
||||
@@ -167,11 +176,10 @@ def expected_results(mode: str, event: str, ref: str, connect: bool = True, acti
|
||||
raise ValueError("shard rehearsal requires an explicit manual event")
|
||||
expected = {job: "success" for job in ALWAYS_JOBS}
|
||||
expected.update({job: "success" if mode == "full" or (mode == "e2e" and job in E2E_JOBS) else "skipped" for job in CODE_JOBS})
|
||||
expected["connect-feature-boundaries"] = "success" if mode == "full" and connect else "skipped"
|
||||
if event in ("pull_request", "push"):
|
||||
expected.update({job: "skipped" for job in EXTENDED_FEATURE_JOBS})
|
||||
rio = mode == "full" and event in ("schedule", "workflow_dispatch")
|
||||
expected.update({job: "success" if rio else "skipped" for job in OPTIONAL_JOBS[:2]})
|
||||
nightly = mode == "full" and event in ("schedule", "workflow_dispatch")
|
||||
expected.update({job: "success" if nightly else "skipped" for job in NIGHTLY_JOBS})
|
||||
expected["connect-feature-boundaries"] = "success" if nightly and connect else "skipped"
|
||||
expected.update({job: "success" if nightly else "skipped" for job in OPTIONAL_JOBS[:2]})
|
||||
full = mode == "full" and (event in ("merge_group", "workflow_dispatch") or (event == "push" and ref == "refs/heads/main"))
|
||||
expected["e2e-full"] = "success" if full else "skipped"
|
||||
expected.update({job: "success" if rehearse_shards else "skipped" for job in SHARD_JOBS})
|
||||
@@ -192,8 +200,8 @@ def read_policy(root: Path, revision: str) -> dict:
|
||||
|
||||
def configurations(jobs: dict[str, str]) -> list[dict]:
|
||||
values = [
|
||||
("workspace-lint", "test-and-lint", "dev", []),
|
||||
("workspace-tests", "test-and-lint", "test", []),
|
||||
("workspace-lint", "workspace-lint-and-build", "dev", []),
|
||||
("workspace-tests", "workspace-lint-and-build", "test", []),
|
||||
("e2e-harness", "build-rustfs-debug-binary", "test", []),
|
||||
("uring-tests", "uring-integration", "test", []),
|
||||
("server-e2e-hooks", "build-rustfs-debug-binary", "dev", ["e2e-test-hooks"]),
|
||||
@@ -540,7 +548,7 @@ def check_workflow(root: Path) -> list[str]:
|
||||
or verifiers[0].get("shell") != "bash" or "if" in verifiers[0]):
|
||||
errors.append("required-checks must unconditionally verify actual needs and preserve its exit status")
|
||||
lint_commands = ("cargo clippy --all-targets -- -D warnings", "python3 scripts/ci_test_artifact.py cargo lint --configuration workspace")
|
||||
clippy = [step for step in jobs.get("test-and-lint", {}).get("steps", [])
|
||||
clippy = [step for step in jobs.get("workspace-lint-and-build", {}).get("steps", [])
|
||||
if any(command in step.get("run", "") for command in lint_commands)]
|
||||
if not clippy or any("if" in step for step in clippy):
|
||||
errors.append("workspace Clippy must run on every code event, pull requests included")
|
||||
@@ -620,7 +628,8 @@ def main() -> int:
|
||||
for name, value in (("mode", plan["mode"]), ("plan", json.dumps(plan, separators=(",", ":"))),
|
||||
("connect", str(plan["connect"]).lower()), ("packages", json.dumps(plan["packages"], separators=(",", ":"))),
|
||||
("full_validation", str(plan["full_validation"]).lower()),
|
||||
("rehearse_shards", str(plan["rehearse_shards"]).lower())):
|
||||
("rehearse_shards", str(plan["rehearse_shards"]).lower()),
|
||||
("plan_contract", PLAN_CONTRACT)):
|
||||
output.write(f"{name}={value}\n")
|
||||
print(json.dumps(plan, indent=2))
|
||||
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||
|
||||
+29
-9
@@ -116,7 +116,7 @@ class PlannerTests(unittest.TestCase):
|
||||
plan = self.repo.plan(self.repo.commit(), base=baseline)
|
||||
self.assertEqual(plan['packages'], ['consumer', 'core', 'leaf', 'rustfs'])
|
||||
self.assertEqual(plan['jobs']['connect-feature-boundaries'], 'skipped')
|
||||
for job in ('test-and-lint', 'workspace-tests', 'core-contract', 'e2e-tests',
|
||||
for job in ('workspace-lint-and-build', 'workspace-tests', 'core-contract', 'e2e-tests',
|
||||
's3-implemented-tests', 's3-lifecycle-behavior-tests'):
|
||||
self.assertEqual(plan['jobs'][job], 'success')
|
||||
|
||||
@@ -233,7 +233,8 @@ class PlannerTests(unittest.TestCase):
|
||||
result = subprocess.run([sys.executable, '-I', str(script), 'select'], cwd=self.repo.root, env=env, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
values = dict(line.split('=', 1) for line in output.read_text().splitlines())
|
||||
self.assertEqual(set(values), {'mode', 'plan', 'connect', 'packages', 'full_validation', 'rehearse_shards'})
|
||||
self.assertEqual(set(values), {'mode', 'plan', 'connect', 'packages', 'full_validation', 'rehearse_shards', 'plan_contract'})
|
||||
self.assertEqual(values['plan_contract'], '2')
|
||||
plan = json.loads(values['plan'])
|
||||
self.assertEqual(plan['policy']['revision'], self.repo.base)
|
||||
self.assertEqual(plan['policy']['rollout'], 'shadow')
|
||||
@@ -295,29 +296,48 @@ class GateTests(unittest.TestCase):
|
||||
needs['classify-changes']['outputs'] = {'mode': 'full', 'plan': '{'}
|
||||
self.assertTrue(gate.verify_results(needs, 'pull_request', 'refs/heads/main'))
|
||||
|
||||
def test_plan_from_a_previous_contract_cannot_authorize_the_new_job_set(self):
|
||||
# A base planner predating the NIGHTLY_JOBS contract still emits
|
||||
# connect-feature-boundaries as required success on a pull request. The
|
||||
# current verifier must reject that plan instead of accepting the old
|
||||
# responsibility, so the workflow's contract-marker fallback is the only
|
||||
# path that can widen or narrow the required set.
|
||||
stale = self.plan()
|
||||
stale['jobs'] = dict(stale['jobs'], **{'connect-feature-boundaries': 'success',
|
||||
'uring-integration': 'success',
|
||||
'test-and-lint-rio-v2': 'success',
|
||||
'test-and-lint-protocols': 'success'})
|
||||
stale['not_applicable'] = {job: 'fixture' for job, state in stale['jobs'].items() if state == 'skipped'}
|
||||
expected = gate.expected_results('full', 'pull_request', 'refs/heads/main')
|
||||
self.assertEqual(expected['connect-feature-boundaries'], 'skipped')
|
||||
needs = {job: {'result': result} for job, result in expected.items()}
|
||||
needs['classify-changes']['outputs'] = {'mode': 'full', 'plan': json.dumps(stale, separators=(',', ':')),
|
||||
'connect': 'true', 'full_validation': 'true', 'rehearse_shards': 'false'}
|
||||
self.assertTrue(gate.verify_results(needs, 'pull_request', 'refs/heads/main'))
|
||||
|
||||
def workflow(self):
|
||||
jobs = {}
|
||||
for job in gate.ALWAYS_JOBS + gate.CODE_JOBS + gate.OPTIONAL_JOBS:
|
||||
# A complete plan lookup is represented by equivalent explicit expressions here.
|
||||
if job in gate.ALWAYS_JOBS:
|
||||
expression = "true"
|
||||
elif job in gate.EXTENDED_FEATURE_JOBS:
|
||||
expression = "needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('[\"merge_group\",\"schedule\",\"workflow_dispatch\"]'), github.event_name)"
|
||||
elif job == 'connect-feature-boundaries':
|
||||
expression = "needs.classify-changes.outputs.mode == 'full' && needs.classify-changes.outputs.connect == 'true' && contains(fromJSON('[\"schedule\",\"workflow_dispatch\"]'), github.event_name)"
|
||||
elif job in gate.NIGHTLY_JOBS:
|
||||
expression = "needs.classify-changes.outputs.mode == 'full' && contains(fromJSON('[\"schedule\",\"workflow_dispatch\"]'), github.event_name)"
|
||||
elif job in gate.OPTIONAL_JOBS[:2]:
|
||||
expression = "contains(fromJSON('[\"schedule\",\"workflow_dispatch\"]'), github.event_name)"
|
||||
elif job == 'e2e-full':
|
||||
expression = "github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')"
|
||||
elif job in gate.SHARD_JOBS:
|
||||
expression = "needs.classify-changes.outputs.rehearse_shards == 'true'"
|
||||
elif job == 'connect-feature-boundaries':
|
||||
expression = "needs.classify-changes.outputs.mode == 'full' && needs.classify-changes.outputs.connect == 'true'"
|
||||
elif job in gate.E2E_JOBS:
|
||||
expression = "needs.classify-changes.outputs.mode != 'docs'"
|
||||
else:
|
||||
expression = "needs.classify-changes.outputs.mode == 'full'"
|
||||
expression = f"({expression}) && (github.event_name != 'pull_request' || github.event.action != 'closed')"
|
||||
jobs[job] = {'name': job, 'if': expression, 'steps': []}
|
||||
jobs['test-and-lint']['steps'] = [{'run': 'cargo clippy --all-targets -- -D warnings'}]
|
||||
jobs['workspace-lint-and-build']['steps'] = [{'run': 'cargo clippy --all-targets -- -D warnings'}]
|
||||
jobs['test-and-lint-protocols']['strategy'] = {'matrix': {'features': [
|
||||
{'name': 'swift', 'flags': '--features swift'}, {'name': 'sftp', 'flags': '--features sftp'}, {'name': 'http3', 'flags': '--features http3,swift'}]}}
|
||||
jobs['required-checks'] = {'name': 'Test and Lint', 'if': "always() && (github.event_name != 'pull_request' || github.event.action != 'closed')",
|
||||
@@ -387,7 +407,7 @@ class GateTests(unittest.TestCase):
|
||||
workflow['jobs']['required-checks']['needs'].reverse()
|
||||
for job in ('workspace-tests', 'core-contract', 'e2e-tests', 's3-implemented-tests', 's3-lifecycle-behavior-tests'):
|
||||
del workflow['jobs'][job]['if']
|
||||
workflow['jobs'][job]['needs'] = ['test-and-lint' if job == 'workspace-tests' else 'build-rustfs-debug-binary']
|
||||
workflow['jobs'][job]['needs'] = ['workspace-lint-and-build' if job == 'workspace-tests' else 'build-rustfs-debug-binary']
|
||||
self.assertEqual(self.check(workflow), [])
|
||||
for mutate in (
|
||||
lambda w: w['jobs']['required-checks']['needs'].pop(),
|
||||
@@ -395,7 +415,7 @@ class GateTests(unittest.TestCase):
|
||||
lambda w: w['jobs']['required-checks'].update({'if': "'always()' == 'always()'"}),
|
||||
lambda w: w['jobs']['required-checks']['steps'][0].update({'run': 'python3 scripts/ci_gate.py verify || true'}),
|
||||
lambda w: w['jobs']['typos'].update({'continue-on-error': 'true'}),
|
||||
lambda w: w['jobs']['test-and-lint']['steps'][0].update({'if': 'false'}),
|
||||
lambda w: w['jobs']['workspace-lint-and-build']['steps'][0].update({'if': 'false'}),
|
||||
lambda w: w['jobs']['test-and-lint-protocols']['strategy']['matrix']['features'].pop(),
|
||||
lambda w: w['jobs'].update({'unaccounted': {'name': 'New'}}),
|
||||
lambda w: w['on']['pull_request'].update({'paths': ['README.md']}),
|
||||
|
||||
Reference in New Issue
Block a user