Compare commits

..

721 Commits

Author SHA1 Message Date
Chris d9c3eeea0c fix(ecstore): hide and reclaim ancestors of delete residue in prefix listings (#7955)
* fix(ecstore): hide ancestors of delete residue in prefix listings

Since #7342 the never-versioned fast path hides a directory whose children are all deleted-version data dirs, but only that one level: the date-style ancestors above a deleted key (`metrics/<stream>/2026/08/28/23/`) still surfaced as empty folders that nothing could remove, which is what #6898 keeps reporting on rc.6 and 1.0.0. The residue probe now walks down through metadata-less ancestors depth-first, ending at the first file met outside a UUID data dir, with a total read budget so a large residue tree surfaces and is hidden one level down instead of costing an unbounded walk. An empty delimiter listing of the ancestor then reclaims the whole committed residue tree in one pass.

Refs #6898

* fix(ecstore): sample before full reads in the residue probe and purge orphan subtrees independently

Adversarial pass 1 findings: the probe read a wide directory in full at every level of a genuine prefix once its 8-entry batch was full, so a leaf holding thousands of objects was materialised per listed prefix; the probe now descends into the sampled children first and only reads the remainder once every sample proved unlistable, and a test pins one bounded read per level and zero complete reads on a wide genuine prefix. Hiding ancestors also made purge_orphan_dir_object fail closed on a whole ancestor when committed residue shared it with residue an older build left without markers, which 1.0.0 users could still reclaim by browsing one level lower; the purge now blocks only the ancestor chain of unpurgeable data and reclaims purgeable sibling subtrees. A UUID directory holding a subdirectory surfaces again as before.

Refs #6898

* fix(ecstore): back off repeated orphan purge scans of unpurgeable trees

Adversarial pass 2: with ancestors hidden, every empty listing of a phantom prefix scanned the whole residue subtree on every disk even when nothing under it can be purged (no committed marker), and the scan no longer stops at the first blocked directory. Remember such prefixes per set for 60 s and skip the rescan. Build committed residue file paths like directory paths so the blocked filter matches under keys containing repeated slashes.

Refs #6898
2026-09-17 10:11:45 +08:00
Aria Stewart 892d3ba3f7 Allow 127.0.0.0/8 as loopback addresses for binding (#7943)
This is particularly useful for integration testing of services that use
an S3 API, as it lets many different instances bind a port on different
IPs.

Co-authored-by: Chris <anzhengchao@gmail.com>
Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-17 10:11:36 +08:00
Chris 706ffdc9ac fix(ecstore): keep the single-block bound on every inline candidate (#7954) 2026-09-17 08:57:49 +08:00
Chris f018628374 fix(ilm): keep expiry pending gauge balanced and drain overwrite tails (#7944) 2026-09-17 00:38:02 +00:00
Jason Kossis cd10981132 fix(ecstore): reuse metadata read guards during writes (#7941) 2026-09-17 06:39:25 +08:00
Hauser 5a353a6f6f refactor(obs): depend on dial9 crate (#7949) 2026-09-17 06:38:56 +08:00
GatewayJ e3be247b40 feat(tables): support staged table creation (#7919) 2026-09-17 06:38:41 +08:00
Chris 14081c41a4 fix(ecstore): forward quota admission on CopyObject destination writes (#7947) 2026-09-17 06:38:16 +08:00
Chris 58a3c599ba chore(release): prepare 1.0.1 (#7946) 2026-09-16 19:06:40 +00:00
Chris f7431ce747 fix(ci): bound the fault-tolerance dashboard upload step (#7948) 2026-09-17 02:02:44 +08:00
GatewayJ 87ab95770c perf(s3select): accelerate ScanRange record scanning (#7934) 2026-09-16 23:42:07 +08:00
Hauser 69974a1c4c build(deps): update dependency versions (#7942) 2026-09-16 23:34:13 +08:00
Chris 4a4121c74d ci: drop dead references to the deleted release branch (#7945) 2026-09-16 23:33:24 +08:00
hector e9f8beb69e ci(ft): upload the fault-tolerance report to the dashboard (#7939)
The fault-tolerance suite was the only one without a dashboard upload
step - its reports never reached functional-reports/fault-tolerance/
without manual uploads. Add the standard upload step (continue-on-error,
same shape as the other ten suites), placed before the artifact upload.

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-16 22:42:44 +08:00
cxymds 225026f657 fix(heal): certify metadata health and marker repair receipts (#7932)
* fix(heal): certify metadata health and marker repair receipts

* fix(heal): match nil selectors to absent null metadata

* test(heal): satisfy clippy for nil selector proof

* test(heal): align normal-scan receipt with nil selector proof

A nil or omitted selector now matches only a nil or absent metadata
version, so a normal scan of the latest UUID version leaves
metadata_verified unset and issues no receipt.

Assert metadata health through the exact latest version id instead, and
pin the omitted-selector path to fail closed.

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-16 22:41:51 +08:00
cxymds 58d1296a27 fix(heal): match legacy null version during replacement readback (#7933) 2026-09-16 20:44:36 +08:00
cxymds 3e7a6aa637 fix(ecstore): keep decommission multipart on reserved target (#7937)
Route capacity-owned unversioned multipart uploads through the durable decommission target reservation and fail closed when the reservation lease has expired.
2026-09-16 19:53:56 +08:00
Chris 1240490789 chore(release): merge release into main after 1.0.0 (#7935)
* test(scanner): derive evidence runner profile from registry (#7484)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require mixed-version evidence roles (#7485)

Tighten the Scanner/Heal release bundle checker so mixed-version, scoped-ACK, and rollback fields cannot reuse a generic versions list without proving the expected evidence role.

Require version lists to use source revision identities and include the tested source revision. Also require profile evidence fields to name the core profiling artifacts before release approval.

The release gate remains blocked until measured field evidence is present.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): publish committed MRF runtime checkpoints (#7490)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind profile artifacts in release evidence (#7487)

Require Scanner/Heal release bundles to attach every required profiling artifact to P1 profile evidence with relative paths, artifact formats, non-empty files, hashes, and optional per-artifact measurement-window checks.

Document the tightened release bundle profile contract and cover missing, tampered, and mismatched-window profile artifact regressions in the existing checker self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden measured ABBA evidence claims (#7491)

Reject measured Scanner/Heal ABBA manifests whose mixed-version evidence uses the same baseline and candidate source revision or binary hash.

Require crash fault modes and profile artifact names to match the exact supported sets, rejecting missing, duplicate, and unknown values.

Update harness fixtures and regression coverage for same-build mixed-version claims and exact-set release evidence fields.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): group scanner heal evidence payload (#7494)

Group the EC8+4 Scanner/Heal evidence writer inputs into a typed payload so the distributed e2e crate stays within the clippy argument limit without weakening the lint.

The evidence writer still validates the same S3 bodies, physical shard census, process restart PIDs, and node listings.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF replay bundle fields (#7486)

Bind Scanner/Heal release bundle evidence for MRF durable replay to replay counts, retained responsibility anchors, and successor snapshot publication evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): profile EC84 evidence case runs (#7495)

Give Scanner/Heal evidence cases explicit runtime profiles so EC8+4 background restart and crash cases use their own object count, object size, and partial-progress timeout defaults instead of inheriting the legacy 4x1 case assumptions.

Expose the runtime profile in plan-only output and cover every registry case in the script self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): reject empty release evidence artifacts (#7496)

Require Scanner/Heal release bundle artifact paths to resolve to non-empty files before hashing them.

Cover empty hard-gate artifacts in the existing release bundle checker self-test and keep profile artifact size checking on the shared artifact boundary.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require G14 same-window field coverage (#7489)

Tighten the Scanner/Heal release bundle checker so G14 same-window evidence must name the EC8+4, multi-set, and multi-pool fields covered in that measurement window.

Keep the release gate blocked when same-window evidence omits one of the required G14 fields, without changing production runtime behavior.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require two-hour measured ABBA windows (#7493)

Reject measured Scanner/Heal release ABBA manifests and summaries whose evidence window is shorter than the W21 two-hour release requirement.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF idle checkpoint cleanup (#7497)

Add a runtime cleanup regression test that publishes both retained replay and runtime committed checkpoints, writes scoped and legacy journals, and verifies idle cleanup removes every recovery anchor from the registered local disks.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): ignore untracked files in build identity (#7498)

Align e2e_test build provenance with Scanner/Heal evidence receipts and server binary provenance by treating only tracked source changes as dirty.

This prevents unrelated untracked worktrees or evidence directories from causing compiled test identity mismatches before real evidence cases can run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF disk-full evidence fields (#7499)

Require the Scanner/Heal release registry and bundle checker to carry explicit G08 MRF capacity, disk-full, and replica-loss evidence fields before release approval can pass.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind release JSON artifact provenance (#7500)

Require scanner/heal release evidence JSON artifacts to repeat their measured source revision, run identity, measurement window, gate, and field identity inside the artifact payload. This keeps a refreshed outer bundle hash from accepting stale summary or profile JSON from another run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF rollback mirror filtering (#7501)

Add a regression oracle that keeps scoped-only MRF responsibilities in the authoritative runtime snapshot while omitting them from the v1 legacy rollback mirror.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require segment activation evidence (#7504)

* test(scanner): require segment activation evidence

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

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

* test(scanner): cover activation proof inputs

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF crash matrix cases (#7505)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require release bundle domain evidence (#7502)

Reject scanner/heal release bundles that omit field-level domain evidence for scoped ACK, durable intent, mixed-version, scheduler pressure, profile cost, and two-hour pressure lanes.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF retention GC evidence (#7506)

Require P4 retained-responsibility release bundle evidence to list the retained replay anchor and idle cleanup cases, prove a two-hour retention window, and record both idle cleanup and verified-proof discharge observations.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup GC soak evidence (#7507)

Require the P4 release bundle to carry measured cleanup/GC soak evidence for retained MRF replay responsibilities. The bundle now needs a two-hour cleanup window, exact cleanup case coverage, observed verified idle GC, and zero pending responsibilities or stale journals after GC.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF disk-full commit anchors (#7509)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): report port allocator bind errors (#7510)

Surface the scanned port window, attempt count, and last bind error when the e2e port allocator cannot reserve a localhost port. This keeps Scanner/Heal evidence failures actionable when the environment blocks binds before business assertions run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(sse): resolve 1.0.0 SSE/KMS blockers and P1 findings (#7511)

* fix(sse): resolve bucket default encryption per request

PUT and the POST-object/extract path resolved a bucket's default
encryption with a hard-coded "no explicit SSE-C" flag, so the default was
layered onto a request that already carried an SSE-C header triple and
then tripped that request's own mutual-exclusion check. Every bucket with
default encryption refused SSE-C single PUTs with 400 InvalidArgument,
while CreateMultipartUpload on the same bucket succeeded because it
resolves SSE elsewhere. Both call sites now derive the flag from the
request headers, as COPY already did.

The bucket default's KMS key id was also inherited independently of the
effective algorithm, so an explicit AES256 request against an aws:kms
default bucket produced a self-contradictory algorithm/key-id pair and
was rejected. The key id is now inherited only when the effective
algorithm is aws:kms, matching the storage-layer resolver.

Refs backlog#2368 B1, B2.

* fix(sse): refuse SSE-KMS without a running KMS service

A write requesting aws:kms on a node with no KMS service fell back to the
node-local SSE-S3 provider: the data key was wrapped with
RUSTFS_SSE_S3_MASTER_KEY while the object metadata still recorded
aws:kms and the requested KMS key id. The stored object claimed a KMS
protection it never had, under a key that was never consulted, and no
signal distinguished it from a genuine SSE-KMS object.

The managed-encryption path now asks the resolved DEK provider whether it
wraps with a node-local master key and refuses SSE-KMS in that case:
InvalidRequest when KMS was never configured, ServiceUnavailable when a
configured service is not running. The check sits after the per-key
authorization gate so an unauthorized caller still receives AccessDenied
whatever the KMS runtime state is, and asks the provider rather than a
parallel availability signal because the provider is what actually wraps
the key. A missing master key no longer answers an SSE-KMS request with
an SSE-S3-worded configuration error.

The SSE-S3 local fallback is unchanged.

Refs backlog#2368 B4.

* fix(ecstore): restore and archive tiers in stored coordinates

Multipart restore addressed the remote tier in plaintext coordinates
while the copy-back reads the stored representation. Each part received a
misaligned slice of the remote object whose length still satisfied the
range, the hash reader and the completion size check, so the restore
reported success and silently replaced the object's bytes. Encrypted and
compressed multipart objects were both affected. Restore now accumulates
stored part sizes, passes the stored length to the hash reader alongside
the plaintext length, and validates against the stored size.

The copy-back digests stored bytes, so its computed MD5 is not the
object's public ETag. Restore now preserves the object ETag on both the
single-part and multipart paths, and gives each restored part its own
recorded part ETag rather than the object-level value.

Transition also handed the tier the object's SSE headers and its
RustFS-wrapped data key as request headers. Any S3 target rejected an
SSE-C archive outright, an SSE-KMS archive asked the target to encrypt a
second time under a key id it does not own, and the wrapped DEK left the
cluster. The archive request now strips every SSE header and encryption
marker with the predicate the replication path already uses; the local
xl.meta keeps all of it, so read-through and restore are unaffected.

Objects restored by an affected release are not detected or repaired
retroactively and must be re-restored from the tier.

Refs backlog#2368 B3, B5; backlog#2369 P7.1.

* fix(rio): lock the v1 nonce layout within a segment

Decrypting a v1 segment tried three historical nonce layouts per frame,
independently for every frame. The last of them exists for streams
written before 1.0.0-alpha.91, which reused a segment's part nonce for
every block in it; because block zero's derived nonce equals that base
nonce, a frame encrypted at index zero authenticated at any position. An
attacker able to rewrite the underlying shards could replay it and have
the forged plaintext returned with 200 and an unchanged length. Shard
integrity uses a keyed-hash-free checksum, which such an attacker can
recompute, so it is not a barrier.

A segment now locks onto whichever layout decoded its first non-zero-index
frame and rejects any later frame needing a different one. That leaves one
residual shape: a stream built purely from repeats of frame zero has no
later frame to disagree. New RUSTFS_ENCRYPTION_LEGACY_NONCE_FALLBACK
(default true, so pre-alpha.91 objects keep decrypting) drops the third
layout entirely when set to false, which closes it. Turning it off refuses
pre-alpha.91 objects, so migrate them first by rewriting in place.

Refs backlog#2369 P2.

* fix(kms): reload a service that failed to start

POST /rustfs/admin/v3/kms/reload short-circuited whenever the persisted
configuration matched the in-memory one byte for byte. A node whose KMS
failed to start keeps that configuration and sits in Error, so the
documented recovery call returned "reloaded successfully" while leaving
the node down. Peers reached the same path through the reload broadcast,
so a cluster that lost Vault during a rolling restart had no working
recovery route other than the node-local start endpoint. Reload now
short-circuits only for a service that is actually running, and otherwise
reconfigures, which starts a service that is not running.

The AWS backend also advertised key-version enumeration through
kms/status, which its own documentation says it cannot do; the capability
and its golden snapshot now say false.

Refs backlog#2369 P1, P7.3.

* docs: record the SSE and KMS changes for 1.0.0

The Unreleased changelog section carried no entry for any encryption work
merged since 1.0.0-rc.5, including three items with operational impact:
the config-secret variable whose absence persists secrets in cleartext
with only a warning, the v2 frame write switch and its rolling-upgrade
constraint, and per-key authorization making a public bucket incompatible
with SSE-KMS objects. Adds those plus this batch, including the SSE-KMS
refusal as a breaking change with both routes out.

Also corrects four places where documentation contradicted the code: the
cleanup register still called encrypted range seek opt-in after its
default flipped, the Helm README claimed vault_mount_path only applies to
Transit while the template also feeds the KV2 mount, the disaster-recovery
drill listed bundle contents for backends whose export is refused with
501, and the Chinese README capability table predated most of the feature
set. Documents the SSE-S3 local master key as a first-class operational
mode with its rotation dead end, and what the v1 frame layout does and
does not authenticate.

Refs backlog#2369 P5.

* fix(kms): classify data-path KMS failures by what the caller can do

Only "key not found" and a backend outage were classified; every other
KMS failure that reached the S3 data path fell through to
500 InternalError with a generic message. A disabled or pending-deletion
key, a denied KMS grant, an encryption-context mismatch, an unsupported
algorithm, a credential or timeout failure, and a capability the
configured backend does not have all looked identical to a server fault.
SDKs therefore applied exponential backoff to configuration errors no
retry can fix, and monitoring counted every one of them against the
server's own error rate.

Unusable-key and request-side failures now answer 400, a denied grant
403, transient backend failures 503, and a missing backend capability
501. Damaged, unreadable, or unknown-format key material keeps its 500:
it is a server-side integrity fault, and existing tests pin it.

The classifier is deliberately separate from the admin lifecycle
mapping, which answers 404 for a missing key because there a key id is
the resource being addressed; on the data path it arrives inside a
request header or a bucket default. Messages either name what the caller
asked for or stay generic, with deployment-side detail left on the error
source the way the storage-IO mapping already does.

Refs backlog#2368 B6.

* fix(kms): track and renew static Vault tokens

Token authentication hard-coded "this token carries no lease", so the
renewal task never started, the remaining-TTL gauge was never published,
and nothing looked wrong. `vault token create` grants a 768-hour TTL by
default, so a cluster that had been healthy for a month turned every KMS
call into a 403 and could not recover without a restart or a
reconfigure. Production configuration validation only rejects the
literal dev-token, so an ordinary expiring token reaches a whole cluster.

The source now reads `auth/token/lookup-self` at login and adopts what
Vault reports. A token with no expiry behaves exactly as before. An
expiring renewable one is picked up by the existing renewal loop and
renewed at half TTL like every other auth method. An expiring
non-renewable one warns with its remaining lifetime and publishes the
gauge, so the fail-closed window is visible before it arrives.

The probe never fails the login: a policy that omits lookup-self, or a
Vault that is briefly unreachable, warns and falls back to exactly the
previous behaviour rather than taking down a deployment that works
today. The scripted Vault test double answers the lookup out of band so
existing scripts keep describing only the protocol under test.

Refs backlog#2369 P3.

* feat(sse): report SSE-C requests that arrive without TLS

An SSE-C request carries the customer's AES key in a request header, so
AWS S3 and MinIO both refuse one that did not arrive over TLS. RustFS
accepted them on any transport: a plaintext hop hands the key to anyone
on the path, and since the object cannot be read without that same key,
the exposure lasts as long as the object does.

Refusing outright is the correct end state but not a safe default to
adopt inside a release window, because the project's own s3-tests and
e2e lanes and most staging deployments speak plain HTTP. This release
reports instead: each such request increments
rustfs_ssec_plaintext_requests_total and logs one warning per process, so
an operator can confirm nothing would break before the default flips.
RUSTFS_SSE_C_REQUIRE_TLS=true opts into the AWS 400 now.

The verdict is per connection rather than per deployment: the layer is
built with whether this listener terminated TLS, and additionally accepts
an https protocol forwarded by a proxy the trusted-proxy configuration
already vetted. It sits beside the rate limiter, after the layer that
makes a forwarded protocol trustworthy and after the request context, so
a rejection can echo the request id.

Refs backlog#2369 P7.2.

* fix(kms): say what a node-local backend means for a cluster

The Local backend keeps key material on each node's own disk and
generates its Argon2id salt per node, so two nodes derive different keys
from the same master_key and an object encrypted on one node cannot be
decrypted on another. Behind a load balancer that surfaces as
intermittent 500s on reads that succeeded moments earlier, with nothing
tying the symptom to the cause: the only signal was a generic
"development, testing and demos only" positioning warning that says
nothing about what actually breaks.

Configuring or reconfiguring Local while the deployment is distributed
now logs a dedicated event and appends the consequence to the configure
response, so the operator who made the change sees it. The product
decision to warn rather than refuse is unchanged.

Refs backlog#2369 P7.4.

* docs: record the remaining SSE and KMS changes for 1.0.0

Adds changelog entries for the KMS data-path status classification, the
Vault static-token lease probe, the SSE-C plaintext-transport report and
its switch, and the node-local backend warning.

Documents two things the backend security guide never stated: that SSE-C
belongs on a secure transport, with the counter and switch to plan the
change around, and that the Local backend cannot be shared by a
multi-node deployment because each node derives different keys from the
same master key.

Refs backlog#2368 B6; backlog#2369 P3, P5, P7.2, P7.4.

* fix(kms): report an unreadable key store as an outage on the S3 path

A backend now distinguishes a key store it could not read from a key
that is genuinely absent, but the S3 boundary collapsed the first one
back onto 500 InternalError through the fallthrough for integrity
faults. The distinction was therefore invisible to the client: a
temporary key-directory outage looked exactly like a permanently damaged
key record, and neither the status nor the metric said the request was
worth retrying.

An unreadable key store joins the retryable class and answers 503, next
to a backend error and a credential failure. Damaged, unreadable or
unknown-format key material keeps its 500.

Refs backlog#2368 B6; builds on rustfs/rustfs#7470.

* test(storage): cover MRF cleanup delete ENOSPC anchor (#7513)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require heal retry stats evidence (#7514)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add release bundle dry-run fixture (#7515)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): confirm recovery intent accept readback (#7516)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup delete ENOSPC evidence (#7518)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require bounded retry window evidence (#7517)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): emit scanner heal G09 upgrade evidence (#7519)

Record measured Scanner/Heal G09 evidence artifacts from the upgrade compatibility lanes when a fresh evidence directory is provided.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): persist MRF rollback mirror boundary (#7520)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add G09 upgrade evidence runner (#7522)

* 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 Linux x86_64 runner for the Scanner/Heal G09 mixed-version and rollback upgrade evidence lanes.

The helper reads the pinned previous-release asset metadata from the upgrade workflow, verifies the downloaded binary, builds the current head, runs both ignored E2E tests, and fails unless the expected G09 JSON artifacts exist.

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>

* feat(scanner): implement V2 evidence preflights (#7523)

* feat(scanner): wire dirty usage producer identities

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

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

* test(scanner): add segment activation preflight proof

Keep scanner segment reuse behind a structured activation preflight so release evidence can prove the production gate remains disabled until every producer, generation, overflow, cold-oracle, and distributed invalidation check is satisfied.

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

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

* feat(scanner): expose distributed invalidation evidence

Record an explicit distributed segment invalidation evidence summary when remote dirty usage snapshots are bound to the current activity window and the authenticated scoped ACK capability probe succeeds.

Reject peer dirty usage snapshots that contradict the peer activity pending bit so scoped ACKs fail closed instead of clearing an unadvertised remote mutation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): assemble release evidence bundles (#7526)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

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

* fix(scanner): replay recovery intents while disabled (#7521)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): preflight G09 evidence disk space (#7527)

* 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)

* fix(targets): reject trailing batch items (#7508)

* test(scanner): preflight G09 evidence disk space

Fail the Scanner/Heal G09 upgrade evidence runner before downloading or building when the validation host does not have enough free space for a full raw evidence pass.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): respect cargo target dir in G09 runner (#7528)

* 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)

* fix(targets): reject trailing batch items (#7508)

* fix(scanner): respect cargo target dir in G09 runner

Write the RustFS feature stamp under the resolved Cargo target directory so remote validation hosts with CARGO_TARGET_DIR set can reuse the built PR-head binary.

Extend the runner plan/self-test path to cover relative target-dir resolution.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): add G09 upgrade evidence runner (#7525)

* 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(e2e): add G09 upgrade evidence runner

Add a Linux x86_64 runner that downloads the pinned previous release, builds the current RustFS binary, runs the mixed-version and rollback upgrade compatibility lanes, and verifies the required Scanner/Heal G09 raw evidence artifacts.

Document the runner and add a shell self-test for help, dry-run, SHA validation, and non-empty artifact directory guards.

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>

* test(scanner): emit G09 release bundle gate evidence (#7530)

* 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)

* fix(targets): reject trailing batch items (#7508)

* test(scanner): emit G09 release bundle gate evidence

Write a bundle-ready G09 gate descriptor from the Linux upgrade evidence runner and validate the single G09 gate with the shared release-bundle rules without approving the full Scanner/Heal release.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): reject invalid G09 runner test selection (#7531)

Validate the selected G09 evidence lane before process-substitution case expansion so --plan-only cannot turn an unknown --test value into an empty successful plan.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): keep G09 source binary path clean (#7532)

Keep checksum verification output out of the command substitution that resolves the previous-release binary for the G09 runner.

Also tolerate non-GNU sha256sum in local self-tests by falling back to shasum when GNU --check support is unavailable.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF replay proof cleanup (#7534)

Exercise the committed MRF replay checkpoint path with a real ECStore-backed bucket incarnation, retain the replay anchor until an exact verified proof arrives, then delete the proof-discharged checkpoint and prove a subsequent restart does not resurrect the replayed work.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): carry segment activation preflight evidence (#7535)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): surface cold segment reuse oracle (#7536)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require hard evidence artifact payloads (#7537)

Require Scanner/Heal release bundle JSON artifacts to carry hard-domain evidence fields for mixed-version, crash, capacity, disk-full, replica-loss, and MRF cleanup gates. This prevents a descriptor from approving a hard gate while pointing at a generic measured artifact that lacks the boundary-specific oracle fields.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): bind producer evidence to dirty generations (#7538)

Record scanner segment producer identities with the dirty usage generation they invalidated, then expose a cycle-local producer evidence snapshot for segment reuse activation preflight.

Production activation remains fail-closed until durable producer identity and restart-gap proof are available.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): persist segment invalidation proof metadata (#7539)

* test(scanner): wait for EC8+4 heal control readiness (#7540)

* test(scanner): list full P4 release evidence fields (#7542)

Align the Scanner/Heal release requirement registry with the release bundle gate for P4 so the closure checklist advertises MRF scale, replay cost, retained responsibility, and cleanup/GC soak evidence together.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add W16 release evidence runner (#7543)

Add a W16 Scanner/Heal evidence runner that executes the recovery-intent crash-boundary and quota-authority lanes, emits measured G04/G12 JSON artifacts, and validates the resulting single-gate release descriptors.

Wire its shell self-test into script-tests and document the release evidence entry point.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix: address release acceptance regressions (#7541)

* feat(scanner): admit durable segment proof replay (#7544)

* feat(scanner): admit durable segment proof evidence

Validate complete set snapshot segment invalidation proof metadata against the current dirty usage generation window and scanner process epoch before clearing durable producer and restart-gap activation blockers.

Production segment reuse remains gated by the explicit activation flag.

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

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

* feat(scanner): replay segment proof from root snapshots

Carry segment invalidation proof metadata into root snapshot set states so a complete published baseline can replay the durable producer evidence recorded by each set cache.

Keep the field additive for older readers and leave incomplete or LKG set states unproven.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind MRF release artifacts to evidence kinds (#7545)

Require Scanner/Heal G07/G08/P4 JSON artifacts to declare the exact MRF evidence kind expected for each release-bundle field. This keeps generic measured JSON from satisfying durable replay, ENOSPC, replica-loss, scale, replay-cost, retained-responsibility, or cleanup-GC soak gates.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): enable segment reuse activation gate (#7546)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): run scoped fallback cases on dedicated stack (#7550)

Linux libtest defaults can abort these deep EC-store futures before the assertions run. Wrap the async scoped fallback cases in the repository's dedicated-stack test pattern so the default cargo test path exercises the oracle directly.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): roll out distributed e2e selection (#7549)

Refresh the committed e2e-distributed selection digest after the EC8+4 Scanner/Heal evidence case joined the profile, and document the registry-aware runner entry for the distributed case.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add W13 MRF evidence runner (#7547)

Add a W13 durable MRF evidence runner that emits measured G07, G08, and P4 JSON artifacts and release descriptors through the existing scanner/heal bundle gate.

The runner now executes the ignored MRF replay evidence test with an exact full test path, validates raw artifact kinds and gate decisions, prepares Linux tmpfs-backed ENOSPC roots for G08, and documents the Linux/long-soak boundaries.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): fix segment proof fixtures

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

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

* test(heal): reduce W13 evidence helper arguments

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

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

* ci(test): enable local KMS for s3 compatibility tests (#7553)

Configure the s3-tests harness with a local KMS key so SSE-KMS cases run in CI without relying on an external KMS service.

Also move the anonymous POST default SSE-KMS regression onto the shared local KMS test environment.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind segment proof to typed dirty suffix (#7564)

Track producer identity completeness per pending bucket and retain process coverage across durable acknowledgements. Admit a prior same-process segment proof only for an exact or strictly earlier fully typed generation window.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): validate scoped ACK clear receipts (#7563)

Fail closed when a signed scoped ACK response claims more cleared entries than its request contained, while preserving partial-clear semantics and zero-mutation capability probes.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind cold scoped cache reuse (#7565)

Record an explicit cold bucket reuse proof for scoped set scans and treat fully cold sets as valid cold-zero-walk reuse when the baseline and bucket incarnations are bound.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(s3): preserve delete marker read error headers (#7558)

* chore(release): sync configurable nightly build workflow from main (#7567)

feat(nightly): make the build branch configurable (NIGHTLY_BRANCH var + dispatch input) (#7557)

* docs(obs): correct metric export interval and explain alert timing (#7571)

* fix(heal): persist scoped admin admission handoff (#7569)

* fix(heal): persist root admission handoff

Persist administrator root heal ownership before queue publication so crash restart can replay an admitted control-plane start without relying on process-local replay cache state.

Protect queued durable root owners from priority displacement; callers must use forceStart cancellation semantics to replace them.

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

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

* fix(heal): persist admin heal handoff types

Extend durable admin heal admission handoff beyond cluster roots while preserving legacy root recovery records. Persist the heal type in schema v2, replay typed admin requests after restart, keep non-admin requests out of the durable control-plane handoff, and allow path cancellation to clear durable-only non-root records.

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

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

* fix(heal): preserve disjoint durable admin owners

Limit admin forceStart durable-owner cleanup to overlapping heal requests after the admin recovery journal begins storing non-root work. Add fail-closed and displacement tests so durable bucket owners cannot be silently dropped by unrelated replacement or priority pressure.

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

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

* fix(heal): persist admin terminal receipts

Persist terminal receipts for durable administrator heal starts before retiring their pending intent so restart can answer completed or cancelled scoped tasks without replaying them.

Retain pending work when terminal publication fails and keep timeout owners on checkpoint-only replay semantics.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): mark committed replication and tier mutations (#7568)

* fix(scanner): mark committed replication and tier mutations

Bridge ECStore committed replication status writebacks and lifecycle tier expiration cleanup into scanner dirty-usage producer identity tracking. Only successful replication metadata updates mark Replication; validate-only, superseded, and retry paths remain unmarked.

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

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

* feat(scanner): replay durable dirty producer records

Add a strict scanner dirty-usage producer replay schema that can restore typed producer coverage after restart without treating ordinary process-local mutations as durable evidence. Replay records validate schema, cache key format, generations, scopes, producer identities, duplicate buckets, byte limits, and entry limits before modifying dirty state.

Keep segment activation fail-closed unless the restored bucket state came from durable replay; subsequent local mutations clear the durable authority bit for that bucket. Also fix the release scanner clippy lint in scoped cold-reuse proof creation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): recover expansion buckets and resume retirement (#7574)

* fix(ecstore): recover partially created expansion bucket volumes

* fix(ecstore): resume retirement after startup safety checks

* fix(ci): align S3 KMS fixtures and default key expectations (#7573)

* fix(heal): prune retained root terminal receipts (#7576)

Bound durable root-heal terminal receipt retention to the completed-status TTL while keeping expired receipts authoritative enough to retire stale pending intents before deleting the receipt itself.

Add a budgeted background GC pass and fail closed on corrupt or non-terminal receipt records.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(storage): preserve retryable publication guard errors (#7561)

* fix(storage): preserve retryable publication guard errors

* fix(scanner): satisfy scoped cache clippy lint

Use bool::then_some for the scoped cold bucket reuse proof construction after merging the latest release branch.

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(health): report storage quorum in node readiness (#7566)

* fix: preserve scanner backlog during pool retirement (#7555)

* fix: hand off native scanner backlog before pool retirement

* test: initialize optional proof in data usage fixtures

* perf: bound parallel V3 pool metadata phase writes

* test: keep native backlog fault errors typed

* test: skip unchanged pool metadata in crash barriers

---------

Co-authored-by: cxymds <cxymds@gmail.com>

* test(scanner): bind profile artifact provenance (#7577)

* fix(scanner): persist dirty producer journal (#7580)

Persist scanner dirty producer replay records from committed replication and tier mutations, hydrate journal state on restart, and clear durable records only after acknowledged dirty generations.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: add scanner heal scheduler pressure evidence (#7584)

Add measured G10/P1/P3 scheduler-pressure release descriptor generation, ABBA scheduler/profile provenance, and focused self-tests for the release bundle gate.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: add scanner heal G14 multiset evidence (#7583)

Add EC8+4 multi-set and multi-pool scanner/heal release evidence coverage, including e2e registry cases, oracle checks, and a G14 descriptor assembler for measured case artifacts.

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(nightly): push assets with plain git on release — the build fleet has no gh CLI (#7586)

fix(nightly): push assets with plain git — the build fleet has no gh CLI

The sm-standard-4 runners used by the nightly build have no gh binary
(functional-chain workflows run elsewhere, on the jumpbox). The assets
publish step died on 'gh: command not found' at the credential-helper
setup, and the preceding clone failure had been masked by 2>/dev/null,
misleading the step into the orphan path. Swap clone and remote setup
to plain git with the token embedded in the URL; push semantics are
unchanged.

* fix(scanner): stabilize retired backlog survivor commits (#7585)

Treat an authoritative scanner pause-backlog commit as stable when every member in that commit stores the matching stable and committed record. Retired source replicas can remain missing in the observed topology while survivor membership is already stable.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(metrics): prevent duplicate drive reporting across nodes (#7582)

Co-authored-by: houseme <housemecn@gmail.com>

* fix(ecstore): avoid Windows batch-delete cleanup sharing conflicts (#7579)

Co-authored-by: houseme <housemecn@gmail.com>

* feat(ilm): inspect legacy transition version state (#7581)

* test(scanner): add release evidence ingestion gates (#7588)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix: reuse admin error boundary for legacy tier reconciliation (#7587)

* fix(test): stabilize release CI heal fixtures (#7589)

Keep heal admission-only tests off durable root recovery so mock storage does not require local metadata disks. Align forceStart retry assertions with replacement semantics and fix the e2e metric clippy collapse.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).

* test(scanner): add release bundle evidence descriptors (#7595)

* test(scanner): merge release evidence descriptors

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

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

* test(scanner): add authority and legacy release descriptors

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

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

* test(scanner): add scoped ack release descriptor

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

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

* test(scanner): require all release evidence fields

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

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

* test(scanner): add mrf release descriptor producer

Add a standalone W13 MRF descriptor producer for measured G07/G08/P4 raw artifacts, with fail-closed self-tests for missing responsibility cases, disk-full ENOSPC observations, synthetic artifacts, and short P4 cleanup soaks.

Also bind G12 quota path evidence to explicit case lists so W16 descriptors cannot pass without reset and settlement coverage.

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

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

* test(scanner): reuse mrf release descriptor producer

Route the W13 runner through the shared MRF descriptor producer so measured runs and offline re-packaging use the same fail-closed validation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(ilm): backfill proven legacy transition state (#7591)

* feat(ilm): backfill proven legacy transition state

* fix(ilm): remove redundant reconcile test clone

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): remove redundant transition clone (#7597)

Drop an unnecessary clone in the legacy transition state digest regression so release CI clippy lanes can run with warnings denied.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): seed current backlog members before committing (#7594)

* fix(scanner): seed current backlog members before committing

* test(app): run recursive force delete test on large stack

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind raw profile artifacts (#7598)

Require Scanner/Heal release profile wrappers to carry bundled raw profile artifacts plus the matching profile cost metrics.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): surface first failed object in status (#7599)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover retry budget across attempts (#7600)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden durable producer evidence gate (#7601)

Require Scanner/Heal G11 maintenance evidence to prove durable dirty producer journal replay, replay-state hydration, generation-bound ACK clearing, and invalid journal fail-closed cases before segment reuse can be activated.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden scoped ack evidence proof (#7602)

Require Scanner/Heal G03 scoped ACK evidence fields to carry their own measured provenance and concrete ACK, capability, and mixed-peer observations before release-bundle gate verification.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind status outcome raw provenance (#7603)

Require measured G05/G06/R-D raw status, compatibility, and disposition artifacts to share run identity before producing release bundle descriptors.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): retry fully recoverable batch failures (#7604)

Allow recursive bucket and cluster heal attempts whose retained batch failure is entirely retryable to use the existing bounded manager retry budget. Permanent or mixed batch failures remain terminal so heal outcome semantics stay strict.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): avoid movement debt for stale remote lease (#7605)

Classify a final remote scanner publication lease validation failure from a fresh activity snapshot instead of treating every validation error as data movement. If the peer session and movement generation still match the granted leases and publication is otherwise allowed, keep the publication rejected as an activity-baseline miss without creating pause backlog movement debt. Preserve DataMovement for expired leases, peer restarts, movement generation changes, and active publication blocks.

Tests cover namespace-only validation invalidation and the remote fence-loss cases that must still defer as DataMovement.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* chore(deps): refresh workspace dependencies (#7606)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind G14 case artifacts (#7611)

Require Scanner/Heal G14 release bundle JSON wrappers to mirror their outer evidence and carry self-contained case artifact provenance.

Copy proof-json case artifacts into the generated G14 descriptor bundle so assembled release bundles can validate case file hashes after relocation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): retry contended decommission cancel target locks (#7612)

* test: align transition fixtures and KMS context expectations (#7610)

* test(scanner): stabilize W13 release gate evidence (#7613)

* feat(nightly): publish packages as assets of the rolling 'nightly' release (sync from release) (#7593)

feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).

* test(scanner): stabilize W13 release gate evidence

Treat only real raw-entry windows as replayed scanner enumeration in the restart diagnostic, so final completion rounds without raw entries are not fail-closed as raw replays.

Allow the EC8:4 multi-pool heal evidence case to select an outage object key that routes to an online pool while an entire target pool is down, preserving strict behavior for single-pool cases.

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

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

---------

Co-authored-by: hector <42570491+majinghe@users.noreply.github.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bound native backlog fixture resources (#7609)

* fix(scanner): bound native backlog fixture resources

Keep native scanner backlog restart fixtures within low file descriptor limits by reducing the native-only disk layout, making fixture shutdown drain background work, and avoiding long-lived ECStore retention from background loops.

Convert ECStore-backed background refresh/recovery/monitor tasks to upgrade weak owners only while doing work so completed test stores release their disk graph before the next native fixture starts.

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

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

* test(tier): refresh release-merged failure fixtures

Reuse the raw legacy-transition fixture helper so delete-all expiry actually exercises Unknown transitioned history metadata after the release merge.

Rewrite restore-failure disk fixtures through per-disk xl.meta snapshots so stale destination identities can be persisted without tripping ordinary metadata update guards.

Update the SSE KMS mismatch expectation to the InvalidRequest/context_mismatch behavior now returned by the rio-v2 path.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): align release E2E gate and repair regression tests (#7607)

* fix(scanner): harden heal release gates (#7614)

* test(scanner): reject synthetic heal evidence inputs (#7617)

Harden the Scanner/Heal checkpoint restart and G14 EC evidence producers so release descriptors cannot be assembled from marked fixture, dry-run, or synthetic case inputs.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): collect status outcome raw evidence (#7618)

Normalize live Scanner/Heal status/outcome observations into the measured raw artifacts consumed by the G05/G06/R-D release descriptor producer.

Reject synthetic observations, incomplete required cases, and reused run/window identities before raw artifacts can enter the release bundle flow.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind scoped ack confirmation to generation (#7619)

Require lost scoped dirty-usage ACK reconciliation to observe a clean peer activity generation that covers the requested ACK generation. Apply the same guard in the scanner aggregation path so a stale clean activity snapshot cannot discharge pending maintenance after an uncertain ACK response.

Refs rustfs/backlog#2427

Refs rustfs/backlog#2281

Refs rustfs/backlog#2240

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: report tier reference proof setup failures without hanging (#7608)

* test(scanner): plan Linux evidence validation (#7620)

Add a Scanner/Heal Linux release-evidence planner that emits a machine-readable execution manifest for the remaining measured validation lanes.

The planner can run only lightweight preflight checks and keeps plan-only output distinct from measured release evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): select G14 replacement drive by evidence (#7622)

Choose the replacement disk from the target node by the presence of complete pool metadata instead of assuming the first configured drive is the scanner metadata holder. This keeps the G14 multi-set harness aligned with multi-drive EC layouts and adds clearer diagnostics when multi-pool outage writes fail closed.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): report Linux evidence plan status (#7624)

Add a status mode for the Scanner/Heal Linux evidence planner so operators can identify missing or incomplete artifacts before final bundle assembly.

The status output stays plan-only and never reports release approval.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add G14 multi-pool background heal evidence (#7628)

test(scanner): align G14 multi-pool outage evidence

Treat the localhost multi-pool topology as whole-pool loss when the target node is down. If that topology cannot admit the outage object while the pool is offline, defer that object write until the pool rejoins and record the oracle marker instead of failing before the real crash/restart evidence runs.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): verify deferred multipool outage evidence (#7629)

Validate the G14 multi-pool oracle field that records down-window outage PUT refusal and requires the deferred post-rejoin outage object to be accepted and checked through S3.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): allow metadata snapshots at read quorum (#7627)

fix(ecstore): admit metadata snapshots at read quorum

Allow guarded bucket metadata snapshot existence checks to use read quorum so degraded erasure sets can continue Object Lock snapshot reads without weakening bucket mutation or object write quorum.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* chore(deps): refresh release dependencies (#7632)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): use pool lock quorum for set writes (#7630)

Use the pool-wide namespace lock client domain for every set in a pool so degraded EC8+4 multi-set writes are gated by node-level lock quorum instead of the narrower per-set endpoint host slice.

Keep namespace-lock domain deduplication tied to both the pool namespace and shared clients, and add regression coverage for three-locker degraded writes plus cross-pool domain separation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): keep G14 multi-set restart graceful (#7633)

test(e2e): keep multi-set restart graceful

Include the EC8+4 multi-set restart scenario in the graceful interruption lane so the unclean-shutdown marker assertion matches the scenario semantics.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): record deferred G14 outage writes (#7637)

Include optional outage-write diagnostics in G14 evidence oracles so deferred multi-pool outage writes bind their down-window refusals and post-rejoin acceptance.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bound G14 multi-pool evidence size (#7640)

Keep the multi-pool evidence runner within the registry object budget while retaining deferred outage-write diagnostics for release-gate validation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): find nextest junit fallback for evidence cases (#7643)

Handle cargo-nextest writing JUnit reports under the workspace target directory even when the Rust build uses CARGO_TARGET_DIR. This keeps measured Scanner/Heal evidence cases from passing the real test but failing final receipt packaging.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ilm): preserve cleanup ownership on tiered overwrites (#7639)

* fix(ilm): preserve cleanup ownership on tiered overwrites

* fix(ci): refresh E2E selection for tier overwrite regression

* test(ilm): restore noncurrent compensation tests to serial CI (#7645)

* fix(heal): preserve null-version tags and encoded object paths (#7644)

* fix(e2e): record EC8+4 drive restart set size (#7648)

* fix(e2e): record EC8+4 drive restart set size

Include the erasure set drive count in the distributed EC8+4 drive restart oracle so Scanner/Heal release evidence matches the registry contract.

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

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

* fix(s3): tighten s3s footprint ratchet

Replace release-merge s3_error! macro calls with equivalent S3Error constructors so the s3gate migration ratchet does not grow on the PR merge tree.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): target multi-set outage heal candidate (#7653)

* test(e2e): target multi-set outage heal candidate

Require the outage write used by EC8+4 multi-set root-heal evidence to miss the same erasure index owned by the selected replacement drive. This avoids accepting a candidate from a different set and turning a valid heal into a false negative.

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

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

* fix(e2e): satisfy G14 heal lint gates

Remove clippy-only noise from the G14 multi-set heal evidence test and align the admin route policy inventory with the registered heal catch-all route.

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

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

* update

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): reuse API error boundary for decoding failures (#7647)

* fix(heal): reuse API error boundary for decoding failures

* Update heal.rs

Signed-off-by: houseme <housemecn@gmail.com>

* test(ecstore): synchronize batch cleanup metadata reads

Acquire object read locks while observing unversioned and explicit-version batch cleanup, then release them before waiting for progress. This prevents snapshots from spanning per-disk marker removal while retaining the existing quorum, timeout, and remote delete count assertions.

Validation: cargo fmt --all --check and git diff --check passed. The focused nextest test passed 20 stress iterations each with test-util and test-util,rio-v2, with retries disabled.

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>

* fix(scanner): wait for ABBA telemetry window (#7660)

Size the scanner collector wait budget from the requested sample count and interval instead of using a fixed 120 second timeout. This preserves the full duration/60+1 telemetry sample set for measured ABBA runs.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(ci): add fault-tolerance degradation suite to the functional chain

Adds a fault-tolerance suite that verifies read/write behavior under
drive and node loss against the erasure-coding contract and snapshots
health-endpoint responses at every degradation tier. Scenarios are
derived from product source (default_parity_count, erasure set sizing):

- A: single-node 4 drives (EC:2, read quorum 2): hide 1/2/3 drives
- B: multi-node 4x1 (one set of 4, EC:2): stop 1/2/3 nodes
- C: multi-node 4x4 (one set of 16, EC:4, read quorum 12): 1 node down
  lands exactly on the read-quorum boundary; 2 nodes down breaks it
- C2: multi-node 4x4 with RUSTFS_STORAGE_CLASS_STANDARD=EC:8 (read
  quorum 8, write quorum 9, lock majority 9): 2 nodes down puts reads
  inside the reported divergence window (read quorum met while the
  lock majority is broken)

By default a reads-refused-despite-met-read-quorum observation is
reported as known-divergence without failing the suite; the strict
input escalates it. Chain order becomes:
upgrade -> s3 -> kms -> tier -> storage -> heal -> pool -> security ->
replication -> fault-tolerance -> performance.

Covers the findings of the 2026-09 external degradation report
(reads at read quorum, health-endpoint readiness truthfulness,
degradedReasons capture) as automated regression probes.

* Revert "feat(ci): add fault-tolerance degradation suite to the functional chain" (#7666)

This reverts commit 6eb200022e.

* fix(ecstore): preserve multi-pool version histories (#7658)

* fix(ecstore): preserve multi-pool version histories

* fix(ecstore): satisfy clippy in rebalance test

* fix(ecstore): preserve decommission delete compatibility

* fix(heal): recover stale peer state after restart (#7665)

* fix(heal): select writable recovery intent owner (#7668)

* feat(ci): add fault-tolerance degradation suite to the functional chain (#7669)

* fix(get): reject pre-header read quorum failures (#7670)

* fix(get): reject pre-header read quorum failures

* fix(ci): unblock get pre-header quorum checks

* ci: stop defaulting suite dispatches to a pinned release (#7681)

The rustfs_version dispatch input defaulted to 1.0.0-rc.4-preview.1,
which shadowed the nightly fallback and started 404ing once that
release was deleted. Manual dispatches with no inputs now fall through
to the nightly package (same contract the pool suite already has);
passing rustfs_version or package_url still pins the build exactly as
before. Chain (repository_dispatch) runs are unaffected: the inputs
context is empty there, so they always used the nightly fallback.

* ci: manage backlog issues by signal instead of per-run filing (#7680)

* ci: manage backlog issues by signal instead of per-run filing

The suite workflows used to file one backlog issue per failed run
(dedup was by run ID, which never matched), so issues accumulated
without bound. Replace the inline filing step in every suite workflow
(s3, kms, tier, storage, heal, pool, security, replication, upgrade,
performance) with a single call to
auto-testing/scripts/issue_manager.py, which:

- dedups by signal: failing cases are searched among open issues by
  label (suite category + case ID); covered cases become a coalesced
  comment on the existing issue, only uncovered cases file a new one
- labels new issues with functional-test, the suite category, one
  label per failing case ID (lazily created), and env for
  bootstrap-class failures (no cases ran, wholesale failure, or
  404/ssh/clone/dpkg signatures in the log)
- closes open issues of the suite after a fully green run, citing the
  run as evidence; cancelled runs never file or close anything

The step is skipped cleanly when auto-testing (private checkout) does
not contain the manager, or when PF_TESTING_GH_TOKEN is unset.

* fix(ci): satisfy actionlint and workflow contract tests for the manager step

- heal and performance workflows have no rustfs_version dispatch input;
  referencing `${{ inputs.rustfs_version }}` in the manager step failed
  actionlint's expression type check. Their package source now resolves
  from package_url with the nightly fallback.
- scripts/test_security_workflow.py pinned the removed inline filing
  step. The wiring assertions now pin the manager step (manager path +
  per-suite report argument), and the evidence/stale-file tests assert
  the skip contract instead: without the private auto-testing checkout
  present, the step exits 0, publishes nothing, and leaves stale
  evidence untouched.

Verified locally: actionlint clean, shellcheck clean,
test_security_workflow.py 21/21.

* fix(storage): alias legacy meta bucket over internode rpc (#7685)

* fix(storage): alias legacy meta bucket over internode rpc

Retry read-only internode RPC metadata access from legacy .minio.sys to .rustfs.sys when mixed-version peers report missing metadata during rolling upgrades.

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

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

* test(ecstore): target durable ILM receipt quorum fixture

Use the actual durable ILM receipt object path when taking target disks offline so the test exercises receipt write quorum instead of whichever set owns the source record path.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): stabilize cancelled rename test (#7684)

* test(scanner): report Linux descriptor drift ledger (#7692)

Add a read-only descriptor ledger mode to the Scanner/Heal Linux evidence planner so release operators can separate current-head measured descriptors from old-head measured artifacts and case-level inputs before final bundle assembly.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): retry recovery readback SlowDownRead (#7695)

Allow the Scanner/Heal interruption oracle to retry transient retryable GET failures after replacement recovery has converged. The readback still verifies exact object bytes and keeps a bounded timeout, so permanently unreadable objects continue to fail the case.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(pool): default the node-endpoints fallback to the 4-node fleet (#7696)

RUSTFS_POOL_NODE_ENDPOINTS has no secret/var configured, so the suite
ran with the workflow's inline 3-endpoint fallback and the explicit
--node-endpoints flag overrode the script default fixed in
rustfs/auto-testing#61 — every dispatch died at startup with 'must
provide at least 4 direct node endpoints' (run 34677538650). Add
rustfs-node4 to the fallback; explicit secret/var still wins.

* fix(scanner): require ABBA collector credentials (#7700)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): label G14 recovery read failures (#7701)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): preserve settings in token status (#7691)

* fix(heal): preserve settings in token status

* fix(heal): group status response context

* fix(heal): decode partial status settings

* ci: green runs for case failures; red only for harness breakdowns (#7698)

A failing product case used to turn the whole workflow red, so the run
conclusion carried no signal beyond 'something failed' and the report
was suppressed. New semantics across the functional suites:

- Suite steps run with continue-on-error: the outcome is still recorded
  for the report and the backlog issue manager (security/tier already
  carried the flag).
- Generate report always publishes the full per-case table plus a
  'Product result: N passed, M failed' summary, and its exit gate is
  harness health: red only when the suite never reached case level (no
  case verdicts), failed wholesale (zero passes, >=3 failures), or was
  cancelled/skipped. performance is unchanged (parked).
- tier's structured gate no longer fails on case failures; it keeps red
  for evidence-init and missing-gate-result breakdowns.
- pool/performance keep their existing red sources (install/benchmark).

Workflow contract tests updated to the new exit semantics: the security
report matrix keys green off the suite outcome, the evidence matrix
expects green for failure outcomes with recorded case rows (except
performance), the heal staged-rerun block expects the per-step table to
always publish, and run steps are now required to carry
continue-on-error.

Verified locally: actionlint clean; test_security_workflow.py 21/21.

* test(heal): retry G14 deferred outage put (#7703)

Treat SlowDownRead as a bounded retryable deferred PUT response after the target pool rejoins, and label terminal deferred outage PUT failures with stage context.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): retry G14 outage put probes (#7704)

Treat SlowDownRead as a retryable outage PUT probe response in the G14 multi-pool runner, and label terminal candidate failures with stage context.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): remove unused G14 put helper (#7705)

Remove the superseded ServiceUnavailable-only PUT helper after the G14 outage PUT probe switched to the shared retry classifier.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(tables): authorize reserved metadata reads (#7672)

* ci: harden report steps after first full new-semantics run (#7708)

First full serial pass with the green-on-case-failure semantics
(run 34693745171 / 34695021651) exposed three report-layer defects:

security: the report step referenced LOG_FILE, which is undefined in
this workflow (set -u killed the step before writing report.md), and
the verdict greps could not match the ANSI-escaped [PASS]/[FAIL] tags
in the real suite log. Point it at the artifacts suite.log, allow any
number of color escapes before the verdict tag, and count [SKIP]
lines separately (45 passed, 6 failed, 3 skipped was reported as an
unbound-variable crash).

pool: warp is stopped early (SIGINT) at the storage threshold and
only writes its final report on a clean exit, so an empty warp.log is
the expected shape of a healthy run - require its presence, not its
size. A mid-script die() abort or a FAIL step verdict must also turn
the validator red now that the run step is continue-on-error.

tier: add the standard 'Product result: N passed, M failed' summary
line computed from the case table, matching the other suites.

The contract test fixture previously injected LOG_FILE into the
environment and wrote verdict lines without ANSI escapes, which hid
both real-world defects; the fixture now mirrors the real suite
(stdout+tee with color tags) and asserts the pass/fail/skip counters.

* ci: tolerate ANSI reset escapes in security verdicts; fix tier product counts (#7712)

The overnight run after #7708 proved the security verdict greps still
counted zero: real verdict lines are '\e[1;31m[FAIL]\e[0m STS-105 ...'
— the reset escape sits between the tag and the case id, and the
pattern only tolerated escapes before the tag. Allow escapes on both
sides; the fixture now emits the reset too, mirroring the real suite.

The tier case table is produced by rustfs_tier_report.py and its rows
lead with the topology column, so the case-ID-first table grep matched
nothing ('Product result: 0 passed, 0 failed'). Parse the PASS/FAIL
counts from the '## Case Summary' bullets the report always emits,
falling back to a topology-aware table grep.

* fix(heal): reject overlapping admin heal owners (#7723)

* fix(heal): reject invalid admin selectors before admission (#7724)

* fix(heal): pass endpoints to overlap receipt test (#7738)

Reuse the heal-control endpoint fixture in the overlap receipt regression so the test matches the updated execution helper signature and selector validation boundary.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

Co-authored-by: hehutu <hehutu@gmail.com>

* fix(heal): verify inline shard bitrot during deep scans (#7731)

* fix(heal): verify inline shard bitrot during deep scans

* test(heal): fix CI lint and topology fixtures

* fix(heal): persist and retry partial-write repairs (#7729)

* fix(heal): persist and retry partial-write repairs

* test(heal): pass topology to overlap RPC tests

Use the existing coordinator endpoint fixture for the three overlap-test
calls to the endpoint-aware heal control executor. This repairs the E0061
test-build failure inherited from the release base.

* fix(scanner): prove dirty ACK after confirmed root write (#7732)

Allow a confirmed scanner root data-usage CAS write to prove its own publication when the follow-up root readback cannot provide a proof. Keep AlreadyDurable and all stale or companion paths on the existing readback-only proof boundary.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): rebuild truncated xl.meta from healthy quorum (#7730)

* fix(heal): rebuild truncated xl.meta from healthy quorum

* fix(test): pass topology to heal overlap RPC regression

* fix(test): drive heal admission alongside partial PUT

Poll the partial PUT and its mock heal receiver together, bound their handshake, and retain the existing repair-scope assertions.

* fix(test): prepare durable MRF fixtures and Linux heal stack

* fix(test): drive tier cleanup recovery after deferred attempts

---------

Co-authored-by: Hauser <housemecn@gmail.com>

* fix(heal): prove completed historical version cleanup (#7736)

* fix(heal): prove completed historical version cleanup

* test(heal): use debug runtime stack for C06 regression

* test(ecstore): acknowledge partial PUT heal admission concurrently

* test(mrf): fix journal fault and ownership fixtures

* fix(tables): retain dropped warehouse protection (#7675)

* fix(ecstore): gate shard integrity and preserve legacy recovery (#7741)

* fix(ecstore)!: bind bitrot shards to immutable part identities

Verify part, coding-index, and block identity across write, GET, and Heal
paths. Preserve identities across metadata-only copies and repair, include
them in multipart quorum selection, and require payload proof for receipts.

Keep legacy decoding with conservative parity and target-digest validation,
and document its unsupported cases and additional verification I/O.

BREAKING CHANGE: New bound-v1 shards require compatible readers throughout
the fleet. Legacy objects without sufficient integrity evidence return an
error; binary rollback after new writes requires verified data migration.

Refs: rustfs/backlog#2497

* fix(ecstore): preserve shard framing with independent integrity

Commit immutable part-generation Merkle roots and replicated proof indexes without changing existing checksum frames. Verify reads, reconstruction and Deep Heal against metadata quorum; keep legacy reads and explicitly defer unproven legacy data repair.

Preserve multipart rollback generations, require acknowledged durable index publication, and add decoder compatibility and donor-shard regression coverage.

* fix(heal): verify protected partial-write replay

* fix(heal): retire stale delete markers after bucket recreation (#7743)

* fix(heal): prove completed historical version cleanup

* test(heal): use debug runtime stack for C06 regression

* fix(heal): retire stale delete markers after bucket recreation

* test(ecstore): fix Clippy in retired marker regressions

* fix(heal): preserve historical null version identity (#7749)

* fix(s3): preserve version identities on suspended reads (#7751)

* fix(rebalance): retry contended runtime metadata access (#7752)

* fix(rebalance): retry contended runtime metadata access

* chore(rebalance): shrink typed error ratchet baseline

* fix(heal): bind bucket recovery to its original incarnation (#7744)

* fix(heal): bind bucket recovery to its original incarnation

* fix(heal): preserve stable bucket heal task errors

* fix(s3): classify copy source part read failures (#7746)

Map PartMissingOrCorrupt to SlowDownRead only at the GetObject/CopyObject source-reader boundary so quota metadata corruption keeps its internal fail-closed response.

Add store and e2e coverage for Harbor-style multipart staging CopyObject boundaries.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(tier): classify invalid remote credentials (#7753)

* fix(tier): classify invalid remote credentials

Classify deterministic remote-tier auth rejections before the probe cleanup fallback, and preserve the admin 4xx status for invalid credential responses.

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

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

* fix(ecstore): stabilize delete-marker movement regressions

Ignore target-local bucket incarnation fence metadata when comparing data-movement delete-marker identities, keep the release null-version listing fixture valid, and fix the nextest decommission-family filter so fault-hook tests actually join the serial group.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): serialize tier cleanup owner matrix (#7765)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): run bootstrap usage rebuild promptly (#7758)

Treat non-authoritative usage floor startup as pending bootstrap rebuild work so reset-published bootstrap markers cannot sit behind clean-idle or empty pause-backlog delay.

Wire recovery wakeups into the normal scanner cycle wait and expose the pending rebuild state in scanner status.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(tier): preserve cleanup scheduling for overwrites (#7766)

Queue committed tier free-version cleanup receipts for PUT and materialized CopyObject overwrites of transitioned null versions, while keeping remote deletion behind the existing persisted free-version cleanup path.

Tighten data-movement delete-marker retry equivalence by ignoring local bucket-incarnation fencing metadata, avoid retry fallback to the source pool, and keep version-list pagination from manufacturing an empty final page.

Refresh the e2e-distributed selector hash for the current release test set.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(release): backport main fixes and stabilize tier cleanup tests (#7793)

* fix(ecstore): stop pruning at nonempty directories (#7616)

* fix(ecstore): stop pruning at nonempty directories

* test(ecstore): release pruning fixtures before temp cleanup

(cherry picked from commit 8f150d1d8e)

* fix(heal): preserve retryable batch failures during recovery (#7642)

* fix(heal): preserve retryable batch failures during recovery

* test(heal): pin prebuilt hooks binaries in ci

(cherry picked from commit 5cd58319ed)

* fix(s3): reject oversize single PUT early and map body errors to 4xx (#7635)

* fix(s3): reject oversize single PUT early and map body errors to 4xx

A single PutObject above the 5 GiB single-request ceiling was only
rejected after the client had streamed 5 GiB into s3s's read-time body
budget, and the resulting BodySizeLimitExceeded surfaced from the erasure
writer as 500 InternalError. A body whose connection hit EOF before
Content-Length bytes arrived (hyper's IncompleteBody) was also a 500.
SDKs retry 500s, so one oversize upload was resent from offset 0 five
times.

- PutObject and UploadPart reject a declared length above
  MAX_SINGLE_PUT_OBJECT_SIZE with 400 EntityTooLarge before reading the
  body; the constant moves to rustfs_config so the s3s limit and the
  admission check share one value.
- ApiError maps BodySizeLimitExceeded to EntityTooLarge and a hyper body
  EOF to IncompleteBody across both io::Error conversions.

Fixes #7596.

* test(s3): cover UploadPart admission, aws-chunked length, real s3s limit

- Poll-counting test body proves PutObject and UploadPart reject a
  declared size above the ceiling with zero body polls; exact-cap and
  zero-length parts pass admission.
- A STREAMING-* aws-chunked PUT whose framed Content-Length exceeds the
  cap is admitted when the decoded length is within it and rejected when
  the decoded length is over it.
- The display-based BodySizeLimitExceeded matcher is checked against the
  real error produced by the pinned s3s Body budget.

(cherry picked from commit 50b31bc75b)

* fix(ecstore): make directory mtime fixture portable (#7623)

* fix(ecstore): make directory mtime fixture portable

* style(ecstore): format mtime fixture assertion

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
(cherry picked from commit b1cc286cac)

* fix(storage): prevent readiness after native migration failures (#7652)

* fix(storage): prevent readiness after native migration failures

* fix(storage): skip unsupported IAM records before reading

* fix(storage): use stable typed migration metadata errors

* fix(storage): include migration record in startup errors

* test(storage): cover native migration startup failures

* test(storage): use array chunks in migration fixture

---------

Co-authored-by: RJ Regenold <214054+rjregenold@users.noreply.github.com>
Co-authored-by: cxymds <cxymds@gmail.com>
(cherry picked from commit 0cbc3ffe61)

* fix(admin): expose OIDC account display fields (#7654)

Expose verified OIDC username and email claims as display-only metadata on self-account responses while preserving the virtual parent as the authorization identity.\n\nKeep rustfs-madmin public response structs unchanged by adding the optional wire fields through private handler response wrappers.

(cherry picked from commit f02bc947cd)

* fix(replication): correct peer joins and remote-state reporting (#7650)

* fix(replication): propagate verified peer deployment identities

* fix(replication): report actual remote peer state

* fix(replication): defer initial sync until all peers join

* test(replication): shut down TLS fixtures cleanly

---------

Co-authored-by: houseme <housemecn@gmail.com>
(cherry picked from commit 853ae63b6a)

* fix(s3): bound stalled UploadPart request bodies (#7659)

* fix(s3): bound stalled UploadPart request bodies

* fix(ci): preserve the S3S footprint ratchet

(cherry picked from commit 666dfd9f9f)

* fix(tables): reject reserved warehouse locations (#7671)

Co-authored-by: cxymds <cxymds@gmail.com>
(cherry picked from commit 414176c47f)

* fix(ci): bind nightly lanes to one resolved source (#7688)

(cherry picked from commit 01d8e4347f)

* fix(replication): close the pre-stable convergence gaps from backlog#2367 (#7626)

* fix(replication): total-order rule sort and honor V1 top-level Prefix

Rule matching had two defects from the pre-GA replication audit
(rustfs/backlog#2367 C-1 and C-2):

- The actionable-rule sort compared same-destination rules by priority but
  answered Equal for any other pair, which is not a total order; the
  standard library sort panics on such comparators once a slice exceeds the
  insertion-sort threshold, so an object matching more than 20 enabled rules
  across two or more targets could panic the PUT or DELETE task. Rules now
  sort by priority descending with destination and id as tie-breakers, and
  filter_target_arns preserves that order instead of draining a HashSet.

- A V1 rule written without a <Filter> carries its prefix at the top level;
  that field was never read, so <Prefix>logs/</Prefix> matched every object.
  ReplicationRuleExt::prefix now falls back to it, with a <Filter> keeping
  precedence. The existing prefix fixtures were built this way and had been
  asserting nothing.

* fix(admin): advertise data-usage and listen capabilities to rc

The rc client gated `rc du` and `rc watch` on a pinned contract that
matched server versions by the string prefix `1.0.0-rc.`; a server that
reports `1.0.0` no longer matches, and the dynamic `advertised` list did
not carry either name, so `rc du` against a GA server fails with an
unsupported-capability error (rustfs/backlog#2367 E-2).

Advertise `admin.data-usage` from the admin route inventory like the IAM
entries, and `listen_notification` for the bucket `?events=` extension
route the admin router dispatches. The client merges advertised entries
ahead of its pinned contract, so no version sniffing is needed.

* fix(site-replication): stop notifying the local site on remove and rotate

The pending-remove and pending-rotation notification loops skipped the
local site by endpoint only, while finalization identifies it by
deployment id or endpoint. The reconcile tick resolves the local peer from
the node's own listen address (and a handler from the request Host), so
`remove --all` dialed the site's registered endpoint, waited out the
request timeout against the lifecycle lock it was holding, and answered
`Partial: failed to notify 1 peer(s)` for a removal that had succeeded
(rustfs/backlog#2367 A-4, backlog#2195 item 3).

Both loops now iterate the peers still awaiting notification through one
helper that applies the finalization identity.

* fix(site-replication): promote and settle IAM retries without a tick of slack

Two retry-queue behaviours kept an IAM change from converging for ten to
twenty minutes after a peer came back (rustfs/backlog#2367 A-1 and A-3,
backlog#2305):

- The lightweight 30-second pass filtered its reachability probe to bucket
  ops, so a backed-off IAM or bucket-metadata snapshot waited for the
  600-second tick to notice the peer. It now probes every backed-off class
  and still replays only bounded bucket ops; promotion is a state flip the
  heavyweight tick acts on.

- Backoffs are multiples of the tick interval, so a failure stamped δ
  seconds after a tick was 600 − δ old at the next tick and slipped a whole
  extra interval. The heavyweight drain now evaluates backoff halfway to its
  next tick.

- An IAM entry first created by a non-deletion failure (the add bootstrap's
  snapshot send, the drain's own replay, an import-iam schedule) was never
  stamped `deletions_recorded`, so a later recorded deletion could not
  settle it and it escalated to the marker only `replicate repair` clears.
  Entries created by this binary now start recorded; a row persisted by an
  older binary keeps the escalation semantics.

* fix(site-replication): reload peer node caches after bucket wiring writes

Every S3 bucket-config write ends by asking the other nodes of the cluster
to reload the bucket's metadata; the site-replication writers never did.
On a multi-node site the node that ran the pairing (or applied a peer's
bucket-meta item) rewrote the bucket targets and the derived replication
rules on disk, while every other node kept serving its cached copy for up
to the 15-minute refresh. A `resync start` routed to such a node reported
every freshly wired bucket as `Config not found` and a bucket whose
operator target the pairing had replaced as `recorded remote target no
longer exists` (rustfs/backlog#2367 A-5, backlog#2195 item 2; functional
SITE-105).

Add one best-effort reload helper in the site-replication hooks and call it
after the bucket setup, versioning, peer bucket-meta apply, removed-peer
cleanup, make-with-versioning, and endpoint-refresh writes; the ensure
helpers now report whether they wrote so unchanged passes stay silent. The
resync manifest and start now read the persisted wiring instead of the
node-local cache, matching the target read the start path already did.

The new four-node e2e pairs two clusters and starts a resync through a
non-coordinator node right after pairing; it also covers an IAM user
created on a non-coordinator node converging to the peer site.

* test(e2e): cover delete-marker replication from a multi-node source

The functional suite reported delete markers created on a 3-node source
never reaching the target (rustfs/backlog#2195 item 4, REP-105). The report
was a probe defect, but the shape had no coverage: the existing
delete-marker e2e runs a single-node source. Pin it against a four-node
source replicating to a four-node peer and to a single-node target, with
the write and the delete issued through different nodes.

* ci(e2e): refresh the distributed selection for the new replication cases

Four distributed cases were added (two site-replication, two delete-marker
replication). The linux digest is derived from the last CI listing of the
lane (34 cases, matching the previous pin) plus the four new names; the
darwin digest is the local listing, which selects the same 38 cases.

(cherry picked from commit aeaba86d73)

* fix(e2e): require a verified server binary for every e2e run (#7687)

* fix(ci): share quick checks and lint workflows

* fix(ci): install actionlint from its verified release

* fix(ci): reject dependencies on required quick checks

* feat(test): verify the E2E server build and source identity

* test(e2e): register verified Darwin test membership

* test(e2e): record verified Linux receipt test membership

* test(e2e): record compiled Darwin receipt test membership

* test(e2e): record compiled Linux receipt test membership

* test(e2e): record Darwin e2e-full membership after merging main

* fix(test): route scanner/heal evidence E2E runs through the verified server binary

The evidence runners built rustfs with plain cargo and then ran e2e_test directly, which now fails without a run receipt. They build through scripts/e2e_binary.py and run the e2e_test invocations under e2e_binary.py run; the obsolete rustfs.features stamp is removed.

* docs(e2e): run server-backed e2e commands through the verified binary wrapper

* test(e2e): record Linux e2e-full membership from the branch CI listing

(cherry picked from commit 2909b1bfe1)

* fix(kms): classify KMS/SSE error contracts and SSE-S3 headers (#7697)

* fix(sse): classify bare SSE-KMS writes when no KMS is available

A `aws:kms` request without a key id, on a bucket without a default key,
returned `500 InternalError` whenever no KMS service was running: the
"no KMS key available" branch exited with an untyped storage error before
the availability classification that the keyed form already received.

Route that branch through the same split: `503 ServiceUnavailable` while
a configured KMS is stopped, `400 InvalidRequest` when KMS was never
configured, and `400 InvalidRequest` naming the missing key id when a
running KMS has no default key. `CreateMultipartUpload` shares the path.

Adds a unit test for the bare form and an e2e module that stops KMS
through the admin API, runs a master-key-only node, and runs a Local KMS
without a default key; refreshes the e2e-full selection digests.

(cherry picked from commit c3259dadc3d603a9185a5b0ad9f83dfb884e61c8)

* fix(sse): keep KMS error classes on the encrypted read path

GetObject, CopyObject and UploadPartCopy on an SSE-KMS object whose key
no longer exists answered `500 InternalError` ("KMS key not found") while
PutObject under the same key already answered `400 KMS.NotFoundException`.
The read path carries its classification through ecstore's
`EncryptionResolutionErrorKind`, which had no kind for a missing key, a
denied KMS grant or a missing backend capability, so all three folded
onto `DecryptionFailed` and the S3 layer reported an internal fault.

Add `KeyNotFound`, `AccessDenied` and `NotImplemented` kinds, map them on
both sides of the boundary, and give an envelope the configured backend
cannot unwrap a diagnosable message while keeping its `500`.

Unit tests cover the kind round trip and the reader wrapping; a new e2e
test deletes a key immediately and checks GET/Copy return 400 with
`KMS.NotFoundException` while HEAD stays 200. The e2e-full selection
digests are refreshed from the current listing (the previous digests
predated the delete-authorization tests) and the e2e `create_default_key`
helper is updated to the accepted `EncryptDecrypt` spelling.

(cherry picked from commit 2523a9814e97caea318d4ff1a51bef3a4d4445b2)

* fix(kms): classify key-management errors on the admin routes

`POST /kms/keys`, the legacy `create-key` alias and `generate-data-key`
reported every backend refusal as `500`: a blank key name (which each
backend failed on differently, the Local backend by writing a key file
with an empty stem), a name already taken, an unknown key, a disabled key
and a capability the backend lacks. `delete` and the lifecycle routes
already classified the same errors.

Refuse a blank or whitespace name in `KmsManager::create_key` before any
backend sees it, and share one `KmsError` to status mapping across
create, delete and generate-data-key (400 for validation and key state,
404 for an unknown key, 409 for a taken name, 501 for a missing
capability, 500 only for damaged material). The XML-error routes carry
the same status explicitly since s3s derives none for a custom code.

The read-only Static backend now reports create, delete and
cancel-deletion as `UnsupportedCapability`, matching its rotate and
enable/disable answers, so the admin API returns 501 for all of them.

(cherry picked from commit e33cac5493c4d9d6662e0d2980b58ba2b24a6d1b)

* fix(sse): stop SSE-S3 responses from naming the wrapping KMS key

`x-amz-server-side-encryption-aws-kms-key-id` is defined for `aws:kms`
objects only, but PutObject, CopyObject, CreateMultipartUpload and
GetObject returned it for `AES256` objects too, carrying the KMS key that
wraps the SSE-S3 data key (the service default, or the literal `default`
on a node without KMS). The write paths copied `kms_key_id` from the
encryption material unconditionally, and the single-decrypt GET
classification did the same after resolving the key for authorization.

Add `EncryptionMaterial::response_kms_key_id`, which yields the id only
for SSE-KMS, use it at the four write-response sites, and gate the GET
classification the same way. CompleteMultipartUpload and HeadObject
already omitted the header.

Unit tests pin both directions; a new e2e test covers Put/Get/Head/Copy
and CreateMultipartUpload for AES256 with an aws:kms control. The
e2e-full selection digests are refreshed from the current listing.

(cherry picked from commit 29d793a63352b0b60fd53c565e80fdbede8964bb)

* fix(s3): validate PutBucketEncryption rules before storing them

A default-encryption rule naming an unknown `SSEAlgorithm` (for example
`AES128`), a rule without `ApplyServerSideEncryptionByDefault`, an empty
rule list, or a `KMSMasterKeyID` on an `AES256` rule was stored as
written: the only algorithm check on the route decided whether to fill
in the default KMS key. `GetBucketEncryption` then advertised that
configuration while the write path encrypted header-less writes under
its `AES256` fallback, so the bucket's declared and actual schemes
disagreed. Two comments claimed the route already refused unknown
algorithms.

Validate the configuration before any of it is applied: `MalformedXML`
for a malformed rule set or unknown algorithm, `InvalidArgument` for a
key id on a non-KMS rule, and nothing stored on refusal. Correct the two
comments to describe when the AES256 fallback is still reachable.

Unit tests cover every refusal and the accepted shapes; an e2e test
checks the refusals leave the previous configuration in place. The
e2e-full selection digests are refreshed from the current listing.

(cherry picked from commit 29e4486dce41197ed93f5253cdbabc57d27a4ddb)

* test(e2e): refresh e2e-full selection for the combined KMS/SSE fixes

* test: align two unit tests with the new KMS and bucket-encryption contracts

`scheduled_deletion_carries_a_deadline_and_can_be_cancelled` still
expects the state error (`InvalidOperation`) for cancelling a key that
is not pending deletion; only the Static backend's mutations moved to
`UnsupportedCapability`. The uninitialized-store PutBucketEncryption
test now sends a well-formed AES256 rule so it reaches the store lookup
instead of the new configuration validation.

(cherry picked from commit e2e6a2535a)

* fix(site-replication): keep an operator's bucket-level target to a peer instead of taking it over (#7709)

* fix(site-replication): keep an operator's bucket-level target to a peer instead of taking it over

Site replication wired each bucket by looking for an existing replication
target "to the same peer" and rewriting the first match in place as its own
same-name target. An operator's bucket-level target that happened to point
at that site (different target bucket, operator credentials) was the first
match whenever it pre-dated the join, and the reconciler repeats the pass
every 600s, so the takeover also depended on target order afterwards. The
operator's rule then named an ARN no target backed and their bucket
replication stopped silently, while the inherited bucket-level reset id
made every site resync report the bucket as owned by another resync
(rustfs/backlog#2479, rustfs/backlog#2489).

Follow MinIO's `getRemoteARN` / `getRemoteARNForPeer` shape instead:

- Wiring updates a target in place only under the same ARN, or when it is
  recognisably the site's own under an older ARN shape (same peer,
  same-name target bucket, site replication service account). Anything
  else gets the site target added next to it.
- The site resync manifest takes the target the derived
  `site-repl-<deployment id>` rule names (same-name shape as fallback), so
  an operator target to the peer neither aborts the bucket as "multiple
  remote targets matched peer" nor gets resynced into.
- Peer removal prunes only targets a pruned derived rule names or the
  same-name target bucket; operator targets stamped with the peer's
  deployment id survive together with their rules.

Unit tests cover the three predicates. e2e
`test_site_replication_keeps_operator_bucket_target_to_peer` runs a
bucket-level replication plus `replication-reset` to the future peer, joins
the sites, and requires the operator target untouched, both paths
delivering, the site resync completing against the site target, and the
operator target and rule surviving `replicate remove --all`; without the
fix it fails at the join with the operator target gone. The repl-nightly
selection digest is refreshed for the new case.

* test(site-replication): drop a redundant clone flagged by clippy

The reconcile unit test cloned the remote peer into the state map although
the binding is not used afterwards; workspace clippy (-D warnings) rejects
that as redundant_clone.

(cherry picked from commit ecdc55fa4b)

* fix: enforce S3 permissions for recursive force deletion (#7661)

* fix: enforce S3 authorization for recursive deletion

* fix: satisfy the s3s footprint guard

* fix: restore list versions policy compatibility (#7686)

(cherry picked from commit 3fd1ce414d)

* fix(ci): repair functional defaults and chain regression checks (#7664)

* fix(ci): default functional suites to nightly packages

* test(ci): follow the fault-tolerance chain handoff

(cherry picked from commit 509a0fa90c)

* fix(ci): align security workflow tests with chain (#7679)

(cherry picked from commit d9e47d2813)

* test(ecstore): keep tier cleanup tests stable after immediate receipt queueing

Release PR #7766 made PUT/CopyObject overwrites queue the tier free-version cleanup receipt immediately, which broke two ecstore tests on release CI. In tier_overwrite_put_and_self_copy_recover_persisted_cleanup_owners the restarted store already runs expiry workers from the second iteration on, so they deleted the remote bytes before the test could assert that the commit leaves them in place; the test now fails the first remote DELETE via set_remove_failure(true) so the cleanup owner stays durable and the later restart still has to rediscover it from xl.meta (the failed remove does not bump remove_count, and the test re-enables removes before the recovery wait). In batch_transitioned_delete_post_commit_failures_roll_back_without_free_version_receipt the convergence loop now treats a transient InsufficientReadQuorum as "not yet converged", because cleanup rewrites xl.meta disk by disk and a racing read can briefly miss quorum (seen in the rio-v2 lane); any other error still panics.

* Revert "fix(ci): align security workflow tests with chain (#7679)"

This reverts commit 4544359f6d.

* Revert "fix(ci): repair functional defaults and chain regression checks (#7664)"

This reverts commit 5ba7ec0291.

* fix(ecstore): pass shard integrity to backported ingest-mode test

The stalled-reader test backported with #7659 used main's four-argument encode_with_ingest_mode, but release's signature takes an optional IntegrityBuilder, so pass None to keep the test focused on ingest-mode cleanup.

---------

Co-authored-by: Henry Guo <marshawcoco@gmail.com>
Co-authored-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: RJ Regenold <rregenold@teamraft.com>
Co-authored-by: RJ Regenold <214054+rjregenold@users.noreply.github.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: GatewayJ <835269233@qq.com>
Co-authored-by: Jason Kossis <jkossis@gmail.com>

* fix(ci): restore release test compilation and e2e membership (#7799)

* fix(s3): return stored content language on local GET (#7755)

* fix(s3): return resolved object tagging version identities (#7757)

* fix(tables): bound warehouse index recovery (#7677)

Co-authored-by: Chris <anzhengchao@gmail.com>

* fix(upgrade): probe legacy IAM namespace before migration (#7816)

* fix(release): backport IAM migration startup fixes and recent main fixes (#7825)

* fix(auth): reject unsigned x-amz headers on header-signed SigV4 requests (release) (#7813)

* fix(s3): preserve missing version errors on object reads (#7800)

* fix(s3): preserve missing version errors on object reads

* test(s3): align missing-version assertions with read contract

* fix(heal): bind admin traversal to bucket incarnation (#7802)

* fix(heal): bind admin traversal to bucket incarnation

* fix(ecstore): reopen healthy hedged readers after peer loss

* test(ecstore): isolate tier overwrite recovery scheduling

* test(ecstore): observe tier cleanup under object read locks

* fix(ecstore): bound decommission entry tracing

* test(ecstore): drain incarnation heal fixture writes

* test(heal): match debug server stack in deep heal fixtures

* test(heal): include identities in C06 count failures

* test(heal): drain PUT tails before inspecting B920 fixtures

* test(scanner): isolate retained MRF retry slots

* fix(ecstore): separate PUT cleanup intent from persisted metadata

* fix(ecstore): capture failed object lock acquisitions (#7803)

* fix(ecstore): capture failed object lock acquisitions

* test(heal): include identities in C06 count failures

* test(heal): drain PUT tails before inspecting B920 fixtures

* fix(ecstore): reopen healthy hedged readers after peer loss

* test(ecstore): isolate tier overwrite recovery scheduling

* test(ecstore): observe tier cleanup under object read locks

* fix(ecstore): bound decommission entry tracing

* test(ecstore): drain incarnation heal fixture writes

* test(heal): match debug server stack in deep heal fixtures

* test(scanner): isolate retained MRF retry slots

* fix(ecstore): separate PUT cleanup intent from persisted metadata

* fix(heal): retain terminal outcomes across restart (#7805)

* fix(heal): retain terminal outcomes across restart

* test(heal): use octal mode for terminal recovery fixture

* fix(ecstore): reopen healthy hedged readers after peer loss

* test(ecstore): isolate tier overwrite recovery scheduling

* test(ecstore): observe tier cleanup under object read locks

* fix(ecstore): bound decommission entry tracing

* test(ecstore): drain incarnation heal fixture writes

* test(heal): match debug server stack in deep heal fixtures

* test(heal): include identities in C06 count failures

* test(heal): drain PUT tails before inspecting B920 fixtures

* test(scanner): isolate retained MRF retry slots

* fix(ecstore): separate PUT cleanup intent from persisted metadata

* fix(heal): recover replacement ownership across VM restarts (#7810)

* fix(heal): recover replacement ownership across VM restarts

* test(ecstore): isolate tier overwrite recovery scheduling

* test(ecstore): observe tier cleanup under object read locks

* fix(ecstore): bound decommission entry tracing

* test(ecstore): drain incarnation heal fixture writes

* fix(ecstore): reopen healthy hedged readers after peer loss

* test(heal): match debug server stack in deep heal fixtures

* test(heal): include identities in C06 count failures

* test(heal): drain PUT tails before inspecting B920 fixtures

* test(scanner): isolate retained MRF retry slots

* fix(ecstore): separate PUT cleanup intent from persisted metadata

* fix(tables): isolate protected S3 listings (#7676)

Co-authored-by: overtrue <anzhengchao@gmail.com>

* fix(ci): restore Linux E2E selection and split offline enrollment (#7843)

* fix(heal): assign stable identities to scanner repair requests (#7823)

* fix(heal): assign stable identities to scanner repair requests

* test(s3): isolate UploadPart inactivity timing

* fix(heal): backport pool metadata scope to release (#7846)

* fix(heal): scope replacement pool metadata to its owning set

(cherry picked from commit a72a75569c)

* fix(heal): use domain imports in pool metadata regression test

(cherry picked from commit e8c5448e21)

* test(heal): set pool metadata regression recursion limit

---------

Co-authored-by: Hiroaki KAWAI <1468181+hkwi@users.noreply.github.com>

* fix(heal): persist pool/set outcomes and recovery scope (#7839)

* fix(heal): persist pool/set outcomes and recovery scope

* test(heal): verify selector receipts and process-crash recovery

* fix(heal): scope replacement pool metadata to its owning set

(cherry picked from commit a72a75569c)

* fix(heal): use domain imports in pool metadata regression test

(cherry picked from commit e8c5448e21)

* test(heal): set pool metadata regression recursion limit

---------

Co-authored-by: Hiroaki KAWAI <1468181+hkwi@users.noreply.github.com>

* fix(ecstore): scope recursive walks to object prefixes (#7850)

* fix(heal): scope nested walks and reject admin read-repair (#7851)

* fix(heal): scope nested walks and reject admin read-repair

* fix(ci): allocate stack for heal metadata regression

* test: cover overlapped multipart staging copy (#7865)

Add Harbor-style E2E coverage for a CopyObject racing with CompleteMultipartUpload. The test allows the copy to observe an unpublished source as NoSuchKey, but rejects 5xx leakage and proves retrying the completed source remains readable.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(release): repair acceptance checks and prepare 1.0.0 (#7866)

* test(e2e): stabilize release acceptance checkpoints

* chore(release): prepare 1.0.0

* test(scanner): stabilize W13 release evidence gates (#7870)

Add the object-only completion diagnostic case and make the evidence runner resolve CARGO_TARGET_DIR before recording the rustfs binary path.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(kms): cover non-default Vault Transit paths (#7657)

Backport the Vault coverage from 4d31621f0a. Keep the release cleanup polls synchronized with object read locks.

* fix(replication): count a bodiless 405 as a replicated delete marker (#7756)

* fix(replication): accept a bodiless 405 as a replicated delete marker during resync

A resync verifies each delete marker with `HEAD ?versionId=<marker>` on the
target. S3 targets (RustFS, MinIO, AWS) answer that with 405 and no body,
and the SDK only synthesizes an error code for 404, so the error arrived
with `code() == None`. `is_retryable_delete_replication_head_error` then
treated it as an ambiguous failure: every delete marker counted as a failed
object with `target service error`, and a site resync over any bucket that
holds a delete marker reported the whole bucket as failed
(rustfs/backlog#2479, SITE-105 on rc.6 and nightly).

Use the raw HTTP status the way the 404 path already does: a 405 without a
code confirms the marker propagated. Ambiguous statuses still fail.

- Unit tests drive `verify_resync_head_result` against a scripted target
  answering 405 (accepted) and 503 (still failed).
- e2e `test_site_replication_resync_replicates_delete_marker` joins two
  sites, converges a live object and a delete marker, and requires the site
  resync to complete with zero failed objects; the repl-nightly selection
  digest is refreshed for the new case.

* fix(replication): verify marker-version purges by absence during resync

A delete-marker resync entry with a pending or failed version purge asks
the target to remove the marker, so the bodiless 405 that proves a
created marker propagated proves the purge did not happen. Accept the
405-as-success mapping only for marker creation (empty
version_purge_status); a purge counts as replicated only when the target
answers not found, and any other HEAD outcome stays failed. Unit tests
drive both purge statuses against the 405 fixture and the 404 fixture.

(cherry picked from commit 1ef3974bad)

* fix(kms): refuse key ids that leave the key prefix on the Vault backends (#7727)

Only the Local backend refused a key identifier containing a path
separator. On Vault KV2 a create with the name bad/name succeeded and
produced a nested KV2 path that the listing then reported as a
directory rather than a key, and an identifier containing .. addressed
a record outside the configured key prefix once the HTTP client
normalised the URL; Vault Transit built its transit key name and its
metadata path from the same unchecked identifier.

Lift the Local backend's containment rule into a shared segment check
(empty, /, backslash, NUL, . and ..) and apply it at the single point
where each backend turns the identifier into a path or a Transit key
name, so create, describe, encrypt, delete and the metadata writes all
refuse with InvalidKey before any request reaches Vault. The admin API
already maps that to 400. The AWS backend is untouched: it addresses
keys by ARN and alias, both of which contain /.

Refs rustfs/backlog#2474 (KMS-213 CreateNegatives on vault-kv2).

Co-authored-by: Hauser <housemecn@gmail.com>
(cherry picked from commit 2f2e775655)

* fix: resume GET body after peer short EOF (#7852)

Backport GET recovery from afc8861fec. The Connect TLS classification change requires the main-only proxy transport layer.

* fix(s3): honor presigned UploadPart checksum queries (#7748)

* fix(s3): honor presigned UploadPart checksum queries

* fix(s3): keep checksum errors within gateway boundary

---------

Co-authored-by: Hauser <housemecn@gmail.com>
(cherry picked from commit a8ace89516)

* fix(table-catalog): map transient lock failures to unavailable (#7841)

(cherry picked from commit 5c73a3d602)

* fix(ecstore): preserve ready read errors during encoding (#7798)

Backport the erasure fix from c95b65a7fc while retaining release shard integrity support. Dependency upgrades and the main-only Connect proxy and diagnostic fixes are excluded.

* ci: add the S3 Tables (Iceberg REST Catalog) functional suite workflow (#7873)

New workflow rustfs-table-test.yml following the shared suite contract:
Initialize functional evidence (FUNCTIONAL_ARTIFACTS_DIR/LOG_FILE/
REPORT_FILE/TMPDIR), checkout for the report parser, auto-testing
clone with retry, continue-on-error on the run step, report generation
through scripts/functional_case_report.py with the product gate (case
failures stay green; harness breakdowns turn red), dashboard upload
under functional-reports/table/, backlog issue manager wiring
(--suite table --category table), artifact upload (report.md, cases.md,
suite.log), before/after environment cleanup, and the chain handoff
step (repository_dispatch only, next: pool expansion).

The suite script itself lives in rustfs/auto-testing (TBL-101..107,
paired PR). The run step passes the product smoke script path
(scripts/table-catalog/pyiceberg_smoke.py) via --smoke-script and the
log via --log-file, matching the other suites.

Contract tests: the table suite is registered in the generic evidence
and workflow matrices (JOBS/DIRECT_TESTS/allowlist) plus a dedicated
TableSuiteTests wiring check. Full run: 21/21.

* test(ecstore): tolerate slow read-repair diagnostics CI (#7892)

* fix(heal): report verified normal shard repairs (#7890)

* fix(heal): retry interrupted internode shard writes

(cherry picked from commit 0bd75f47b6326edd8167ee43b8776f6276692e92)

* fix(deps): update rustls for RUSTSEC-2026-0285

(cherry picked from commit 938e1259d13da7832d85ac5c066a59ab3a28bc63)

* test(heal): wait for replacement disk and coordinator readiness

(cherry picked from commit feeaa6f053aac4b7c8e32477c0ab5ebbd3dcdff3)

* fix(heal): retain RPC transport failures across metadata probes

(cherry picked from commit f48ec9f0c6e37b1f941620c666a9357c4b892d68)

* fix(s3): resume GET bodies after erasure read quorum loss

(cherry picked from commit a38e859905)

* test(heal): keep outage objects in the replacement erasure set

(cherry picked from commit a669f50a366e35505e7745f8b680d8ecf1aa31be)

* fix(rpc): retry authenticated requests after peer epoch changes

Backport the stale-epoch recovery fix from main follow-up #7889. Keep invalid signatures and nonce replays terminal.

* test(rpc): assert retryable rejection of stale boot epochs

(cherry picked from commit 1c4b39b99)

* ci: bound concurrent heal clusters in the full E2E gate

Apply the #7889 scheduling fix while preserving release test membership and zero retries.

* fix(ecstore): hide pending purge entries from listings (#7895)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): scope pending purge listing regression (#7897)

Keep the store-level pending purge listing regression focused on the ordinary recursive path and exact-prefix shortcut. The deeper delimiter CommonPrefix case is a separate physical-walker boundary and should not leave the release test suite red.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(obs): expose scanner usage freshness metrics (#7896)

Add Prometheus scanner metrics for dirty-usage pending buckets, usage-cache save/publication timestamps, publication result codes, and deferred publication state. This gives operators a direct scrape path for confirming scanner dirty ACK behavior without relying on admin JSON.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): backport delete quorum rollback to release (#7902)

* ci: rotate the shared functional-tests concurrency group to -v2 (#7900)

* fix(s3): render null write version IDs consistently (#7904)

* fix(heal): quarantine corrupt root recovery records (#7905)

* fix(heal): quarantine corrupt root recovery records

Keep invalid root-heal records durable while isolating them from healthy recovery owners. Serialize startup replay with administrator admission and fence quarantined task IDs so independent compensation heals can proceed safely.

* fix(ci): align release workflow contract tests

* fix: satisfy root recovery clippy checks

* fix(heal): fence canonical repair receipts (#7909)

* fix(heal): fence canonical repair receipts

* fix(heal): preserve unversioned receipt identity

* fix(heal): persist replacement intent on remote survivor (#7912)

* chore(release): merge main into release (#7913)

* feat(nightly): publish packages as assets of the rolling 'nightly' release (sync from release) (#7593)

feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).

* fix(ecstore): stop pruning at nonempty directories (#7616)

* fix(ecstore): stop pruning at nonempty directories

* test(ecstore): release pruning fixtures before temp cleanup

* fix(replication): close the pre-stable convergence gaps from backlog#2367 (#7626)

* fix(replication): total-order rule sort and honor V1 top-level Prefix

Rule matching had two defects from the pre-GA replication audit
(rustfs/backlog#2367 C-1 and C-2):

- The actionable-rule sort compared same-destination rules by priority but
  answered Equal for any other pair, which is not a total order; the
  standard library sort panics on such comparators once a slice exceeds the
  insertion-sort threshold, so an object matching more than 20 enabled rules
  across two or more targets could panic the PUT or DELETE task. Rules now
  sort by priority descending with destination and id as tie-breakers, and
  filter_target_arns preserves that order instead of draining a HashSet.

- A V1 rule written without a <Filter> carries its prefix at the top level;
  that field was never read, so <Prefix>logs/</Prefix> matched every object.
  ReplicationRuleExt::prefix now falls back to it, with a <Filter> keeping
  precedence. The existing prefix fixtures were built this way and had been
  asserting nothing.

* fix(admin): advertise data-usage and listen capabilities to rc

The rc client gated `rc du` and `rc watch` on a pinned contract that
matched server versions by the string prefix `1.0.0-rc.`; a server that
reports `1.0.0` no longer matches, and the dynamic `advertised` list did
not carry either name, so `rc du` against a GA server fails with an
unsupported-capability error (rustfs/backlog#2367 E-2).

Advertise `admin.data-usage` from the admin route inventory like the IAM
entries, and `listen_notification` for the bucket `?events=` extension
route the admin router dispatches. The client merges advertised entries
ahead of its pinned contract, so no version sniffing is needed.

* fix(site-replication): stop notifying the local site on remove and rotate

The pending-remove and pending-rotation notification loops skipped the
local site by endpoint only, while finalization identifies it by
deployment id or endpoint. The reconcile tick resolves the local peer from
the node's own listen address (and a handler from the request Host), so
`remove --all` dialed the site's registered endpoint, waited out the
request timeout against the lifecycle lock it was holding, and answered
`Partial: failed to notify 1 peer(s)` for a removal that had succeeded
(rustfs/backlog#2367 A-4, backlog#2195 item 3).

Both loops now iterate the peers still awaiting notification through one
helper that applies the finalization identity.

* fix(site-replication): promote and settle IAM retries without a tick of slack

Two retry-queue behaviours kept an IAM change from converging for ten to
twenty minutes after a peer came back (rustfs/backlog#2367 A-1 and A-3,
backlog#2305):

- The lightweight 30-second pass filtered its reachability probe to bucket
  ops, so a backed-off IAM or bucket-metadata snapshot waited for the
  600-second tick to notice the peer. It now probes every backed-off class
  and still replays only bounded bucket ops; promotion is a state flip the
  heavyweight tick acts on.

- Backoffs are multiples of the tick interval, so a failure stamped δ
  seconds after a tick was 600 − δ old at the next tick and slipped a whole
  extra interval. The heavyweight drain now evaluates backoff halfway to its
  next tick.

- An IAM entry first created by a non-deletion failure (the add bootstrap's
  snapshot send, the drain's own replay, an import-iam schedule) was never
  stamped `deletions_recorded`, so a later recorded deletion could not
  settle it and it escalated to the marker only `replicate repair` clears.
  Entries created by this binary now start recorded; a row persisted by an
  older binary keeps the escalation semantics.

* fix(site-replication): reload peer node caches after bucket wiring writes

Every S3 bucket-config write ends by asking the other nodes of the cluster
to reload the bucket's metadata; the site-replication writers never did.
On a multi-node site the node that ran the pairing (or applied a peer's
bucket-meta item) rewrote the bucket targets and the derived replication
rules on disk, while every other node kept serving its cached copy for up
to the 15-minute refresh. A `resync start` routed to such a node reported
every freshly wired bucket as `Config not found` and a bucket whose
operator target the pairing had replaced as `recorded remote target no
longer exists` (rustfs/backlog#2367 A-5, backlog#2195 item 2; functional
SITE-105).

Add one best-effort reload helper in the site-replication hooks and call it
after the bucket setup, versioning, peer bucket-meta apply, removed-peer
cleanup, make-with-versioning, and endpoint-refresh writes; the ensure
helpers now report whether they wrote so unchanged passes stay silent. The
resync manifest and start now read the persisted wiring instead of the
node-local cache, matching the target read the start path already did.

The new four-node e2e pairs two clusters and starts a resync through a
non-coordinator node right after pairing; it also covers an IAM user
created on a non-coordinator node converging to the peer site.

* test(e2e): cover delete-marker replication from a multi-node source

The functional suite reported delete markers created on a 3-node source
never reaching the target (rustfs/backlog#2195 item 4, REP-105). The report
was a probe defect, but the shape had no coverage: the existing
delete-marker e2e runs a single-node source. Pin it against a four-node
source replicating to a four-node peer and to a single-node target, with
the write and the delete issued through different nodes.

* ci(e2e): refresh the distributed selection for the new replication cases

Four distributed cases were added (two site-replication, two delete-marker
replication). The linux digest is derived from the last CI listing of the
lane (34 cases, matching the previous pin) plus the four new names; the
darwin digest is the local listing, which selects the same 38 cases.

* feat(console): support a configurable console URL prefix (#7634)

Co-authored-by: houseme <housemecn@gmail.com>

* chore(release): merge release into main for rc.6 (#7638)

* test(heal): cover MRF manifest CAS legacy transition (#7443)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): enable release branch checks and repair test imports (#7441)

* test(scanner): structure heal release evidence lanes (#7442)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): require segment producer identities (#7444)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): expose recovery intent identity (#7445)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): summarize failed heal perf reports (#7446)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(common): retain unmatched MRF repair proofs (#7447)

Add a recorded verified-repair consumer that discharges only exact retained anchors for the requested bucket while leaving unmatched proofs in the event ring. This gives the future durable successor writer a fail-closed primitive before any tombstone or GC path is enabled.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): route replication read plan through boundary (#7448)

* test(scanner): cover flat-bucket quantum fairness (#7449)

Add a production-entry scanner cohort regression that combines a wide flat bucket with a small bucket under a fixed object budget. The test keeps partial budgeted rounds unpublished, verifies small buckets are only marked after real execution, and confirms a later unbudgeted round can publish the complete aggregate.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): reuse same admission request id (#7451)

Keep a retried heal start with the same request id from bypassing admission deduplication when the transport replay cache is unavailable but the manager still owns the task.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require perf summary evidence fields (#7453)

Fail closed when measured passing Scanner/Heal ABBA summaries omit W10/W11 foreground pressure, lock wait, or attempt-cost evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind release evidence fields (#7452)

Require explicit scanner/heal release evidence field contracts for the scoped ACK and mixed-version rollback gates.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): tag dirty usage producer identities (#7454)

Record production-facing segment invalidation producer identities when existing object-level dirty usage hooks observe PUT, CopyObject, DeleteObject/DeleteMarker, and CompleteMultipartUpload mutations. Keep the data non-authoritative and process-local so segment reuse activation still requires durable generation-window proof.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): gate release evidence bundles (#7457)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require hard evidence ABBA manifest (#7459)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(heal): add MRF committed snapshot writer (#7458)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover EC8+4 restart shard rebuild (#7461)

Retry heal-control RPCs once after transport auth rejects a stale replay-scope epoch, and add a distributed EC8+4 restart heal evidence case that rebuilds a replaced drive with exact shard/body assertions.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): echo measured release evidence in ABBA fake adapter (#7460)

Keep the synthetic ABBA test adapter aligned with the measured release evidence contract so result validation covers release_evidence drift.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require complete ABBA summary matrix (#7462)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: retain MRF replay journal after accepted/merged replays

Keep startup replay journal until a durable successor snapshot exists after Accepted/Merged admission.

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

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

* heal: reclaim superseded MRF snapshots after readback

Add a committed snapshot predecessor cleanup primitive that deletes only an older slot after the successor is read back as the current committed snapshot.

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

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

* heal: require exact owner for object receipts

Require object heal receipts to match the expected bucket incarnation before they can be recorded as positive repair evidence. This prevents stale or cross-incarnation receipts from clearing the wrong heal responsibility.

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

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

* heal: latch object receipt owner before repair

Capture the expected bucket incarnation before invoking object repair so a post-repair owner change cannot rewrite the responsibility that a storage receipt is allowed to prove.

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

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

* heal: replay committed MRF checkpoints durably (#7465)

Prefer committed MRF checkpoints during startup replay, retain accepted replay responsibilities until exact verified repair proofs arrive, and reclaim committed manifests only after discharge.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind hard evidence bundle provenance (#7467)

Require Scanner/Heal release bundle fields to carry source, run, window, timestamp, command, and artifact format provenance before a measured gate can pass.

Keep EC8+4 and performance gate fields tied to a single measurement window so unrelated artifacts cannot be stitched into a release approval.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: reject dry-run object repair receipts

Do not record positive storage repair receipts for dry-run object heal tasks, even if a producer accidentally returns a matching receipt.

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

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

* heal: reject cancelled object repair receipts

Do not record positive storage repair receipts once an object heal task has been cancelled, even if the receipt still matches the requested owner and object identity.

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

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

* test(heal): reject failed object repair receipts

Cover the receipt consumer path where a storage repair result carries both an error and a matching positive receipt. The failure may be recorded, but the receipt must not create repaired, healthy, or absent proof.

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

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

* test(heal): cover bucket object repair receipts (#7468)

Cover bucket and root heal sweeps recording authoritative object outcomes only when storage receipts match the latched bucket incarnation.

Verify unavailable or stale receipt ownership keeps object repair execution intact while leaving canonical outcome proof as Unknown.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add EC8+4 heal restart evidence (#7469)

Register and wire Scanner/Heal background target restart and crash evidence cases for the 3x4 EC8+4 topology.

Validate the observed data/parity geometry in scanner-heal evidence receipts so multi-drive runs cannot satisfy the gate without proving EC8+4 metadata.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: verify replacement pool metadata repair (#7471)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover quorum and mixed repair receipts

Add focused oracles for transient quorum results that carry a matching receipt and for mixed grace plus repaired receipt batches. Only the repaired object may produce positive proof.

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

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

* heal: verify admin recreate pool metadata (#7474)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: preserve merged result for duplicate submits

Keep same-request-id replay receipts accepted for the receipt API, but preserve the legacy submit_heal_request duplicate admission result as Merged.

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

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

* test(heal): preserve EC84 restart semantics (#7478)

Keep the EC8+4 background restart lane on the graceful-stop path and assert clean-restart marker absence only for restart scenarios. This prevents the hard evidence gate from silently exercising the crash path when it claims restart coverage.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF snapshot torn successor recovery

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

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

* test(heal): write EC84 distributed restart oracle (#7481)

Bind the distributed EC8+4 restart evidence lane to a scanner/heal oracle artifact so release validation can consume the real nextest run instead of accepting only a passing test.

Require the registry to assert 8+4 erasure geometry for the three-node, four-drive case.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): harden MRF replay boundaries (#7483)

Reject journal records with unknown version-presence flags even when their CRC is valid, so rollback/future payloads cannot be accepted as known records.

Gate committed checkpoint cleanup by the writer owner captured from the replay source, preserving retained manifests from other owners inside the same sequence window.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): bind MRF manifest CAS dirsync recovery (#7482)

Cover the committed MRF manifest path through LocalDisk conditional CAS when the metadata directory fsync fails. The fixture proves the previous manifest anchor survives rollback, an unanchored first successor is removed, and the legacy MRF journal remains readable even while global durability is relaxed.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): derive evidence runner profile from registry (#7484)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require mixed-version evidence roles (#7485)

Tighten the Scanner/Heal release bundle checker so mixed-version, scoped-ACK, and rollback fields cannot reuse a generic versions list without proving the expected evidence role.

Require version lists to use source revision identities and include the tested source revision. Also require profile evidence fields to name the core profiling artifacts before release approval.

The release gate remains blocked until measured field evidence is present.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): publish committed MRF runtime checkpoints (#7490)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind profile artifacts in release evidence (#7487)

Require Scanner/Heal release bundles to attach every required profiling artifact to P1 profile evidence with relative paths, artifact formats, non-empty files, hashes, and optional per-artifact measurement-window checks.

Document the tightened release bundle profile contract and cover missing, tampered, and mismatched-window profile artifact regressions in the existing checker self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden measured ABBA evidence claims (#7491)

Reject measured Scanner/Heal ABBA manifests whose mixed-version evidence uses the same baseline and candidate source revision or binary hash.

Require crash fault modes and profile artifact names to match the exact supported sets, rejecting missing, duplicate, and unknown values.

Update harness fixtures and regression coverage for same-build mixed-version claims and exact-set release evidence fields.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): group scanner heal evidence payload (#7494)

Group the EC8+4 Scanner/Heal evidence writer inputs into a typed payload so the distributed e2e crate stays within the clippy argument limit without weakening the lint.

The evidence writer still validates the same S3 bodies, physical shard census, process restart PIDs, and node listings.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF replay bundle fields (#7486)

Bind Scanner/Heal release bundle evidence for MRF durable replay to replay counts, retained responsibility anchors, and successor snapshot publication evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): profile EC84 evidence case runs (#7495)

Give Scanner/Heal evidence cases explicit runtime profiles so EC8+4 background restart and crash cases use their own object count, object size, and partial-progress timeout defaults instead of inheriting the legacy 4x1 case assumptions.

Expose the runtime profile in plan-only output and cover every registry case in the script self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): reject empty release evidence artifacts (#7496)

Require Scanner/Heal release bundle artifact paths to resolve to non-empty files before hashing them.

Cover empty hard-gate artifacts in the existing release bundle checker self-test and keep profile artifact size checking on the shared artifact boundary.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require G14 same-window field coverage (#7489)

Tighten the Scanner/Heal release bundle checker so G14 same-window evidence must name the EC8+4, multi-set, and multi-pool fields covered in that measurement window.

Keep the release gate blocked when same-window evidence omits one of the required G14 fields, without changing production runtime behavior.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require two-hour measured ABBA windows (#7493)

Reject measured Scanner/Heal release ABBA manifests and summaries whose evidence window is shorter than the W21 two-hour release requirement.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF idle checkpoint cleanup (#7497)

Add a runtime cleanup regression test that publishes both retained replay and runtime committed checkpoints, writes scoped and legacy journals, and verifies idle cleanup removes every recovery anchor from the registered local disks.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): ignore untracked files in build identity (#7498)

Align e2e_test build provenance with Scanner/Heal evidence receipts and server binary provenance by treating only tracked source changes as dirty.

This prevents unrelated untracked worktrees or evidence directories from causing compiled test identity mismatches before real evidence cases can run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF disk-full evidence fields (#7499)

Require the Scanner/Heal release registry and bundle checker to carry explicit G08 MRF capacity, disk-full, and replica-loss evidence fields before release approval can pass.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind release JSON artifact provenance (#7500)

Require scanner/heal release evidence JSON artifacts to repeat their measured source revision, run identity, measurement window, gate, and field identity inside the artifact payload. This keeps a refreshed outer bundle hash from accepting stale summary or profile JSON from another run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF rollback mirror filtering (#7501)

Add a regression oracle that keeps scoped-only MRF responsibilities in the authoritative runtime snapshot while omitting them from the v1 legacy rollback mirror.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require segment activation evidence (#7504)

* test(scanner): require segment activation evidence

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

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

* test(scanner): cover activation proof inputs

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF crash matrix cases (#7505)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require release bundle domain evidence (#7502)

Reject scanner/heal release bundles that omit field-level domain evidence for scoped ACK, durable intent, mixed-version, scheduler pressure, profile cost, and two-hour pressure lanes.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF retention GC evidence (#7506)

Require P4 retained-responsibility release bundle evidence to list the retained replay anchor and idle cleanup cases, prove a two-hour retention window, and record both idle cleanup and verified-proof discharge observations.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup GC soak evidence (#7507)

Require the P4 release bundle to carry measured cleanup/GC soak evidence for retained MRF replay responsibilities. The bundle now needs a two-hour cleanup window, exact cleanup case coverage, observed verified idle GC, and zero pending responsibilities or stale journals after GC.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF disk-full commit anchors (#7509)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): report port allocator bind errors (#7510)

Surface the scanned port window, attempt count, and last bind error when the e2e port allocator cannot reserve a localhost port. This keeps Scanner/Heal evidence failures actionable when the environment blocks binds before business assertions run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(sse): resolve 1.0.0 SSE/KMS blockers and P1 findings (#7511)

* fix(sse): resolve bucket default encryption per request

PUT and the POST-object/extract path resolved a bucket's default
encryption with a hard-coded "no explicit SSE-C" flag, so the default was
layered onto a request that already carried an SSE-C header triple and
then tripped that request's own mutual-exclusion check. Every bucket with
default encryption refused SSE-C single PUTs with 400 InvalidArgument,
while CreateMultipartUpload on the same bucket succeeded because it
resolves SSE elsewhere. Both call sites now derive the flag from the
request headers, as COPY already did.

The bucket default's KMS key id was also inherited independently of the
effective algorithm, so an explicit AES256 request against an aws:kms
default bucket produced a self-contradictory algorithm/key-id pair and
was rejected. The key id is now inherited only when the effective
algorithm is aws:kms, matching the storage-layer resolver.

Refs backlog#2368 B1, B2.

* fix(sse): refuse SSE-KMS without a running KMS service

A write requesting aws:kms on a node with no KMS service fell back to the
node-local SSE-S3 provider: the data key was wrapped with
RUSTFS_SSE_S3_MASTER_KEY while the object metadata still recorded
aws:kms and the requested KMS key id. The stored object claimed a KMS
protection it never had, under a key that was never consulted, and no
signal distinguished it from a genuine SSE-KMS object.

The managed-encryption path now asks the resolved DEK provider whether it
wraps with a node-local master key and refuses SSE-KMS in that case:
InvalidRequest when KMS was never configured, ServiceUnavailable when a
configured service is not running. The check sits after the per-key
authorization gate so an unauthorized caller still receives AccessDenied
whatever the KMS runtime state is, and asks the provider rather than a
parallel availability signal because the provider is what actually wraps
the key. A missing master key no longer answers an SSE-KMS request with
an SSE-S3-worded configuration error.

The SSE-S3 local fallback is unchanged.

Refs backlog#2368 B4.

* fix(ecstore): restore and archive tiers in stored coordinates

Multipart restore addressed the remote tier in plaintext coordinates
while the copy-back reads the stored representation. Each part received a
misaligned slice of the remote object whose length still satisfied the
range, the hash reader and the completion size check, so the restore
reported success and silently replaced the object's bytes. Encrypted and
compressed multipart objects were both affected. Restore now accumulates
stored part sizes, passes the stored length to the hash reader alongside
the plaintext length, and validates against the stored size.

The copy-back digests stored bytes, so its computed MD5 is not the
object's public ETag. Restore now preserves the object ETag on both the
single-part and multipart paths, and gives each restored part its own
recorded part ETag rather than the object-level value.

Transition also handed the tier the object's SSE headers and its
RustFS-wrapped data key as request headers. Any S3 target rejected an
SSE-C archive outright, an SSE-KMS archive asked the target to encrypt a
second time under a key id it does not own, and the wrapped DEK left the
cluster. The archive request now strips every SSE header and encryption
marker with the predicate the replication path already uses; the local
xl.meta keeps all of it, so read-through and restore are unaffected.

Objects restored by an affected release are not detected or repaired
retroactively and must be re-restored from the tier.

Refs backlog#2368 B3, B5; backlog#2369 P7.1.

* fix(rio): lock the v1 nonce layout within a segment

Decrypting a v1 segment tried three historical nonce layouts per frame,
independently for every frame. The last of them exists for streams
written before 1.0.0-alpha.91, which reused a segment's part nonce for
every block in it; because block zero's derived nonce equals that base
nonce, a frame encrypted at index zero authenticated at any position. An
attacker able to rewrite the underlying shards could replay it and have
the forged plaintext returned with 200 and an unchanged length. Shard
integrity uses a keyed-hash-free checksum, which such an attacker can
recompute, so it is not a barrier.

A segment now locks onto whichever layout decoded its first non-zero-index
frame and rejects any later frame needing a different one. That leaves one
residual shape: a stream built purely from repeats of frame zero has no
later frame to disagree. New RUSTFS_ENCRYPTION_LEGACY_NONCE_FALLBACK
(default true, so pre-alpha.91 objects keep decrypting) drops the third
layout entirely when set to false, which closes it. Turning it off refuses
pre-alpha.91 objects, so migrate them first by rewriting in place.

Refs backlog#2369 P2.

* fix(kms): reload a service that failed to start

POST /rustfs/admin/v3/kms/reload short-circuited whenever the persisted
configuration matched the in-memory one byte for byte. A node whose KMS
failed to start keeps that configuration and sits in Error, so the
documented recovery call returned "reloaded successfully" while leaving
the node down. Peers reached the same path through the reload broadcast,
so a cluster that lost Vault during a rolling restart had no working
recovery route other than the node-local start endpoint. Reload now
short-circuits only for a service that is actually running, and otherwise
reconfigures, which starts a service that is not running.

The AWS backend also advertised key-version enumeration through
kms/status, which its own documentation says it cannot do; the capability
and its golden snapshot now say false.

Refs backlog#2369 P1, P7.3.

* docs: record the SSE and KMS changes for 1.0.0

The Unreleased changelog section carried no entry for any encryption work
merged since 1.0.0-rc.5, including three items with operational impact:
the config-secret variable whose absence persists secrets in cleartext
with only a warning, the v2 frame write switch and its rolling-upgrade
constraint, and per-key authorization making a public bucket incompatible
with SSE-KMS objects. Adds those plus this batch, including the SSE-KMS
refusal as a breaking change with both routes out.

Also corrects four places where documentation contradicted the code: the
cleanup register still called encrypted range seek opt-in after its
default flipped, the Helm README claimed vault_mount_path only applies to
Transit while the template also feeds the KV2 mount, the disaster-recovery
drill listed bundle contents for backends whose export is refused with
501, and the Chinese README capability table predated most of the feature
set. Documents the SSE-S3 local master key as a first-class operational
mode with its rotation dead end, and what the v1 frame layout does and
does not authenticate.

Refs backlog#2369 P5.

* fix(kms): classify data-path KMS failures by what the caller can do

Only "key not found" and a backend outage were classified; every other
KMS failure that reached the S3 data path fell through to
500 InternalError with a generic message. A disabled or pending-deletion
key, a denied KMS grant, an encryption-context mismatch, an unsupported
algorithm, a credential or timeout failure, and a capability the
configured backend does not have all looked identical to a server fault.
SDKs therefore applied exponential backoff to configuration errors no
retry can fix, and monitoring counted every one of them against the
server's own error rate.

Unusable-key and request-side failures now answer 400, a denied grant
403, transient backend failures 503, and a missing backend capability
501. Damaged, unreadable, or unknown-format key material keeps its 500:
it is a server-side integrity fault, and existing tests pin it.

The classifier is deliberately separate from the admin lifecycle
mapping, which answers 404 for a missing key because there a key id is
the resource being addressed; on the data path it arrives inside a
request header or a bucket default. Messages either name what the caller
asked for or stay generic, with deployment-side detail left on the error
source the way the storage-IO mapping already does.

Refs backlog#2368 B6.

* fix(kms): track and renew static Vault tokens

Token authentication hard-coded "this token carries no lease", so the
renewal task never started, the remaining-TTL gauge was never published,
and nothing looked wrong. `vault token create` grants a 768-hour TTL by
default, so a cluster that had been healthy for a month turned every KMS
call into a 403 and could not recover without a restart or a
reconfigure. Production configuration validation only rejects the
literal dev-token, so an ordinary expiring token reaches a whole cluster.

The source now reads `auth/token/lookup-self` at login and adopts what
Vault reports. A token with no expiry behaves exactly as before. An
expiring renewable one is picked up by the existing renewal loop and
renewed at half TTL like every other auth method. An expiring
non-renewable one warns with its remaining lifetime and publishes the
gauge, so the fail-closed window is visible before it arrives.

The probe never fails the login: a policy that omits lookup-self, or a
Vault that is briefly unreachable, warns and falls back to exactly the
previous behaviour rather than taking down a deployment that works
today. The scripted Vault test double answers the lookup out of band so
existing scripts keep describing only the protocol under test.

Refs backlog#2369 P3.

* feat(sse): report SSE-C requests that arrive without TLS

An SSE-C request carries the customer's AES key in a request header, so
AWS S3 and MinIO both refuse one that did not arrive over TLS. RustFS
accepted them on any transport: a plaintext hop hands the key to anyone
on the path, and since the object cannot be read without that same key,
the exposure lasts as long as the object does.

Refusing outright is the correct end state but not a safe default to
adopt inside a release window, because the project's own s3-tests and
e2e lanes and most staging deployments speak plain HTTP. This release
reports instead: each such request increments
rustfs_ssec_plaintext_requests_total and logs one warning per process, so
an operator can confirm nothing would break before the default flips.
RUSTFS_SSE_C_REQUIRE_TLS=true opts into the AWS 400 now.

The verdict is per connection rather than per deployment: the layer is
built with whether this listener terminated TLS, and additionally accepts
an https protocol forwarded by a proxy the trusted-proxy configuration
already vetted. It sits beside the rate limiter, after the layer that
makes a forwarded protocol trustworthy and after the request context, so
a rejection can echo the request id.

Refs backlog#2369 P7.2.

* fix(kms): say what a node-local backend means for a cluster

The Local backend keeps key material on each node's own disk and
generates its Argon2id salt per node, so two nodes derive different keys
from the same master_key and an object encrypted on one node cannot be
decrypted on another. Behind a load balancer that surfaces as
intermittent 500s on reads that succeeded moments earlier, with nothing
tying the symptom to the cause: the only signal was a generic
"development, testing and demos only" positioning warning that says
nothing about what actually breaks.

Configuring or reconfiguring Local while the deployment is distributed
now logs a dedicated event and appends the consequence to the configure
response, so the operator who made the change sees it. The product
decision to warn rather than refuse is unchanged.

Refs backlog#2369 P7.4.

* docs: record the remaining SSE and KMS changes for 1.0.0

Adds changelog entries for the KMS data-path status classification, the
Vault static-token lease probe, the SSE-C plaintext-transport report and
its switch, and the node-local backend warning.

Documents two things the backend security guide never stated: that SSE-C
belongs on a secure transport, with the counter and switch to plan the
change around, and that the Local backend cannot be shared by a
multi-node deployment because each node derives different keys from the
same master key.

Refs backlog#2368 B6; backlog#2369 P3, P5, P7.2, P7.4.

* fix(kms): report an unreadable key store as an outage on the S3 path

A backend now distinguishes a key store it could not read from a key
that is genuinely absent, but the S3 boundary collapsed the first one
back onto 500 InternalError through the fallthrough for integrity
faults. The distinction was therefore invisible to the client: a
temporary key-directory outage looked exactly like a permanently damaged
key record, and neither the status nor the metric said the request was
worth retrying.

An unreadable key store joins the retryable class and answers 503, next
to a backend error and a credential failure. Damaged, unreadable or
unknown-format key material keeps its 500.

Refs backlog#2368 B6; builds on rustfs/rustfs#7470.

* test(storage): cover MRF cleanup delete ENOSPC anchor (#7513)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require heal retry stats evidence (#7514)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add release bundle dry-run fixture (#7515)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): confirm recovery intent accept readback (#7516)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup delete ENOSPC evidence (#7518)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require bounded retry window evidence (#7517)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): emit scanner heal G09 upgrade evidence (#7519)

Record measured Scanner/Heal G09 evidence artifacts from the upgrade compatibility lanes when a fresh evidence directory is provided.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): persist MRF rollback mirror boundary (#7520)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add G09 upgrade evidence runner (#7522)

* 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 Linux x86_64 runner for the Scanner/Heal G09 mixed-version and rollback upgrade evidence lanes.

The helper reads the pinned previous-release asset metadata from the upgrade workflow, verifies the downloaded binary, builds the current head, runs both ignored E2E tests, and fails unless the expected G09 JSON artifacts exist.

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>

* feat(scanner): implement V2 evidence preflights (#7523)

* feat(scanner): wire dirty usage producer identities

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

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

* test(scanner): add segment activation preflight proof

Keep scanner segment reuse behind a structured activation preflight so release evidence can prove the production gate remains disabled until every producer, generation, overflow, cold-oracle, and distributed invalidation check is satisfied.

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

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

* feat(scanner): expose distributed invalidation evidence

Record an explicit distributed segment invalidation evidence summary when remote dirty usage snapshots are bound to the current activity window and the authenticated scoped ACK capability probe succeeds.

Reject peer dirty usage snapshots that contradict the peer activity pending bit so scoped ACKs fail closed instead of clearing an unadvertised remote mutation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): assemble release evidence bundles (#7526)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

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

* fix(scanner): replay recovery intents while disabled (#7521)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): preflight G09 evidence disk space (#7527)

* 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…

* fix(list): reconcile split overwrite metadata (#7910)

fix(list): fail closed on split overwrite metadata

* fix(heal): persist explicit delete-marker purges (#7908)

* fix(heal): persist explicit delete-marker purges

* fix(ecstore): remove unused object info wrapper

* test(rpc): use valid incarnation in digest gate

---------

Signed-off-by: Hauser <housemecn@gmail.com>
Co-authored-by: Hauser <housemecn@gmail.com>

* ci(table): pass --outcome to the backlog issue manager (#7914)

The manager step failed with 'the following arguments are required:
--outcome' in the first table run (hidden by continue-on-error): every
other suite workflow passes the test step outcome through.

* fix(list): reconcile encoded directory objects (#7915)

* fix(ci): restore release checks after marker purge changes (#7918)

* fix(rpc): recover tier reloads after peer capability revocation (#7920)

* test(ecstore): drain expiry workers before restart (#7923)

* test: report tier cleanup recovery progress on timeout (#7922)

* fix(heal): resume replacement generation after target restart (#7926)

* ci(release): use sm-standard-4 for Linux builds (#7925)

* chore(deps): refresh release dependencies

Update the release branch dependency set and lockfile for the latest compatible AWS Smithy and RustFS allocator patch versions.

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

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

* perf(rustfs): backport skip of redundant content-sha256 pass (#7930)

perf(rustfs): skip redundant content-sha256 pass on signed PUTs (#7928)

A SigV4 header-signed or presigned PUT whose x-amz-content-sha256 header carries a real digest is already wrapped in a hash-checking UploadStream by the s3s auth layer, so the storage-side HashReader was hashing the same payload a second time. Hand the digest to the storage layer only when it arrives in a presigned query string alone, the one case s3s does not read.

(cherry picked from commit 95c06966cc)

* chore(deps): bump qrcode-rs to 2.1.1

* fix(ci): preserve tier failure enforcement after release merge

* docs(release): record 1.0.0 stable RPM release

---------

Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Hauser <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: cui fliter <imcusg@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: hector <42570491+majinghe@users.noreply.github.com>
Co-authored-by: xiaomage <devops008@sina.com>
Co-authored-by: GatewayJ <835269233@qq.com>
Co-authored-by: hehutu <hehutu@gmail.com>
Co-authored-by: Henry Guo <marshawcoco@gmail.com>
Co-authored-by: RJ Regenold <rregenold@teamraft.com>
Co-authored-by: RJ Regenold <214054+rjregenold@users.noreply.github.com>
Co-authored-by: Jason Kossis <jkossis@gmail.com>
Co-authored-by: Hiroaki KAWAI <1468181+hkwi@users.noreply.github.com>
Co-authored-by: Hiroaki KAWAI <hiroaki.kawai@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-16 17:58:29 +08:00
hector b74dd7942c ci: register the fault-tolerance suite workflow on the default branch (#7929)
The fault-tolerance suite (disk-outage and peer-failure scenarios,
FT-CASE verdicts) has never been dispatchable: the workflow file only
exists on release, and GitHub registers workflow_dispatch targets
exclusively from the default branch. The suite is otherwise ready - it
runs on the smoke-testing fleet and already uses the
rustfs-shared-functional-tests-v2 lock.

Adds the workflow adapted to this branch's current (pre-sync) gate
semantics - the run step's continue-on-error is dropped so the existing
contract holds - plus the evidence init aligned with the shared
four-variable pattern (FUNCTIONAL_ARTIFACTS_DIR under
rustfs-fault-tolerance-*, LOG_FILE/REPORT_FILE/EVIDENCE_DIR/TMPDIR,
scratch dir) and the standard artifact allowlist.

Contract registration: JOBS / DIRECT_TESTS entries, upload allowlist,
and fixture teaching for the FT-specific shapes (composite
package-url fallback expression, script-driven --cleanup instead of
ssh-driven cleanup). The generic legacy matrices exclude FT the same
way they exclude table/performance until the release->main sync lands.
2026-09-16 12:05:20 +08:00
唐小鸭 95c06966cc perf(rustfs): skip redundant content-sha256 pass on signed PUTs (#7928)
A SigV4 header-signed or presigned PUT whose x-amz-content-sha256 header carries a real digest is already wrapped in a hash-checking UploadStream by the s3s auth layer, so the storage-side HashReader was hashing the same payload a second time. Hand the digest to the storage layer only when it arrives in a presigned query string alone, the one case s3s does not read.
2026-09-16 12:05:04 +08:00
hector a7341abdbd ci(chain): stop gating all suites on the performance runner (#7924)
ci(chain): gate the performance lane on its fleet probe (rebase + contract)

Rebased onto latest main; adds the contract-test update that the first
push of this branch missed:

- test_functional_chain: the performance lane now asserts the new
  probe-gated 'if' (prepare success AND performance_ready == online);
  the other nine lanes keep the plain prepare gate; prepare must carry
  the two preflight steps (hard smoke-testing check, pf-testing probe)
  and publish the performance_ready output

Carried over unchanged from the previous push: per-fleet preflight
(prepare hard-fails only on smoke-testing), the pf-testing probe step,
the probe-gated performance lane, and aggregate's --allow-skipped
(permitted skips shrink the evidence set; unpermitted skips still fail
aggregation). Chain contract tests: 27/27 locally.
2026-09-16 08:32:38 +08:00
唐小鸭 20a38d133b fix(ecstore): admit transformed small objects inline by plaintext size (#7916)
* fix(ecstore): admit transformed small objects inline by plaintext size

* test(e2e): expect the small compressed object inline in compression roundtrip

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-16 08:12:50 +08:00
hector 2bf77c1c19 ci(build): run linux build lanes on sm-standard-4 runners (#7917) 2026-09-15 23:19:48 +08:00
hector 896e1a6b47 fix(ci): bound non-performance functional suites to one hour (#7911)
* fix(ci): bound functional suites to one hour

* fix(ci): exclude performance from suite timeout limits
2026-09-15 20:56:36 +08:00
hector f77efe5f2a fix(ci): retain failed functional chain reports and check runners (#7906) 2026-09-15 18:20:05 +08:00
Hauser 5498b3e5d5 chore(deps): refresh RustFS tokio tar dependency (#7907)
* chore(deps): refresh RustFS tokio tar dependency

Switch the astral-tokio-tar workspace alias to the released rustfs-tokio-tar 0.7.1 crate and remove the obsolete cargo-deny git source allowance.

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

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

* chore(deps): align AWS smithy dependency family

Upgrade the AWS SDK and smithy crates together while patching aws-smithy-json 0.63.0 for the aws-smithy-types 1.7.0 DocumentObject API used by aws-config 1.12.0.

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

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

* revert(deps): use official AWS smithy compatible set

Remove the local aws-smithy-json patch and roll the AWS smithy family back to the latest official combination that compiles with aws-config 1.12.0.

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

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

* docs(architecture): retire tokio tar cleanup entry

Remove the stale cleanup-register entry after switching back to the released rustfs-tokio-tar crate, leaving no unmatched compatibility marker for the architecture guard.

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

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

* ci: restore table workflow failure contract

Align the table functional workflow with the quick-check guard by letting suite failures stay red, generating PASS reports only from complete successful evidence, and preserving the shared backlog issue fallback for harness failures.

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

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

* fix(deps): expose rustfs tokio tar dependency name

Use the published rustfs-tokio-tar dependency key throughout the workspace while keeping source imports on the crate's tokio_tar lib target name.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-15 14:54:21 +08:00
Chris 2fd8c9d333 fix(ecstore): roll back failed delete marker writes (#7899) 2026-09-15 11:44:35 +08:00
Chris 6ef0740afe Preserve top.locks signed job nonce (#7898)
fix(connect): preserve top locks job nonce
2026-09-15 09:26:08 +08:00
GatewayJ f3f6e29f52 fix(ecstore): create markers for repeated versioned deletes (#7884)
Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-15 08:48:16 +08:00
Chris 93fdbcb721 test(ecstore): synchronize causal cleanup observations (#7894) 2026-09-15 08:47:42 +08:00
hector 4c710bf128 ci: register the table suite workflow on the default branch (#7893)
* ci: register the table suite workflow on the default branch

The table suite workflow (#7873) only exists on release so far, and
GitHub registers workflow_dispatch targets exclusively from the default
branch — the suite cannot be triggered until this file lands on main
(same mechanism that keeps the fault-tolerance suite undispatchable).

This adds the identical rustfs-table-test.yml (byte-for-byte the
release version) plus the contract-test registration (JOBS /
DIRECT_TESTS / upload allowlist), and nothing else from release.

* ci: pin actions in the table workflow to full commit SHAs

The default branch enforces check_workflow_pins.sh (unpinned
third-party action refs fail CI). Use the same pinned SHAs as the other
suite workflows on main: actions/checkout@9c091bb... (#v7) and
actions/upload-artifact@b7c566a... (#v6).

Note: the release copy of this workflow still uses @v4 tags; release
does not run the pin gate. Release and main will converge on the pinned
form during the full sync.
2026-09-15 08:47:29 +08:00
Chris 74b7c537f5 fix: recover heal after peer boot epoch changes (#7889)
* test(heal): keep outage objects in the replacement erasure set

* fix(rpc): retry authenticated requests after peer epoch changes

* test(rpc): assert retryable rejection of stale boot epochs

* ci: bound concurrent heal clusters in the full E2E gate
2026-09-15 06:34:20 +08:00
Chris 7a37271bc0 fix: keep top locks within its capture window (#7888) 2026-09-15 03:11:22 +08:00
Chris a8302b511c fix: recover interrupted GETs and heal operations (#7876)
* fix(heal): retry interrupted internode shard writes

* fix(deps): update rustls for RUSTSEC-2026-0285

* fix(heal): retain RPC transport failures across metadata probes

* test(heal): wait for replacement disk and coordinator readiness

* test(connect): validate service-only diagnostic capabilities

* fix(s3): resume GET bodies after erasure read quorum loss
2026-09-15 03:11:18 +08:00
Chris f93237aa95 feat(connect): export signed environment inventory (#7887)
feat: export signed environment inventory
2026-09-15 03:02:22 +08:00
Chris a015d8a181 fix(connect): deliver diagnostic results until expiry (#7886) 2026-09-15 03:01:49 +08:00
Chris 446285265d ci: add threads and RPC acceptance workflows (#7883) 2026-09-15 01:59:23 +08:00
Chris bfdcd1ca29 ci: add service diagnostic acceptance workflows (#7882) 2026-09-15 01:56:12 +08:00
Chris d2c4383ce0 Execute authenticated top.rpc service jobs (#7881)
feat(connect): execute top.rpc service jobs
2026-09-15 01:53:05 +08:00
Chris bdd18f1996 feat(connect): execute thread profile service jobs (#7880) 2026-09-15 01:23:42 +08:00
Chris 7b82e6c375 Preserve the top API service job nonce (#7879)
fix(connect): preserve top api job nonce
2026-09-15 01:19:28 +08:00
Chris 2f266191fd feat(connect): execute bounded drive performance jobs (#7878) 2026-09-15 01:07:26 +08:00
Chris 4d682cc577 feat(connect): execute performance network jobs (#7877) 2026-09-15 01:06:29 +08:00
Chris c525904731 fix(ci): repair E2E inventory and network CLI coverage (#7871)
* ci: refresh Linux full E2E inventory after SigV4 tests

* test(connect): drive traffic for top net CLI export coverage
2026-09-14 16:29:04 +00:00
Chris a61a56e649 fix(ci): gate Connect acceptance on x86 build job (#7875) 2026-09-15 00:21:21 +08:00
Chris 5656ba0ec0 feat(connect): execute top.locks service jobs (#7874) 2026-09-15 00:19:44 +08:00
Chris 9d8cb0d487 Execute authenticated top.api service jobs (#7872)
feat(connect): execute top.api service jobs
2026-09-14 23:10:47 +08:00
Chris 77a0de9a37 Pace diagnostic job result redelivery (#7869)
fix(connect): pace diagnostic result redelivery
2026-09-14 22:16:56 +08:00
hector c0e227b562 ci: expect BACKLOG_ISSUE_TOKEN in scheduled alert wiring checks (#7867)
#7847 routed schedule-failure alerting to rustfs/backlog by switching the
alert jobs' token to secrets.BACKLOG_ISSUE_TOKEN, but check_test_wiring.py
still asserts the literal 'github-token: ${{ secrets.GITHUB_TOKEN }}', so
Quick Checks fails on every open PR. Update the wiring assertions and the
self-test fixtures to the new literal.

Both `--self-test` (52 tests) and the full `validate()` pass locally.
2026-09-14 22:03:44 +08:00
dependabot[bot] c95b65a7fc fix: update dependencies and preserve transport errors (#7798)
* chore(deps): bump ed25519-dalek in the dependencies group

Bumps the dependencies group with 1 update: [ed25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek).


Updates `ed25519-dalek` from 2.2.0 to 3.0.0
- [Release notes](https://github.com/dalek-cryptography/curve25519-dalek/releases)
- [Changelog](https://github.com/dalek-cryptography/curve25519-dalek/blob/3.0.0/CHANGELOG.md)
- [Commits](https://github.com/dalek-cryptography/curve25519-dalek/compare/ed25519-2.2.0...3.0.0)

---
updated-dependencies:
- dependency-name: ed25519-dalek
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump jiff, aws-sdk-s3, datafusion and smallvec

Bump the dependencies group:

- jiff 0.2.35 -> 0.2.37
- aws-sdk-s3 1.146.0 -> 1.146.1
- datafusion 55.0.0 -> 55.1.0
- smallvec 1.16.0 -> 1.16.1

Update Cargo.lock for the matching transitive bumps (datafusion
subcrates, hickory, cc, crc32fast, tinyvec, toml_edit and others);
tinyvec_macros is no longer required.

* chore(deps): use s3s 0.16.0 release and bump rust-version to 1.98.1

* fix: preserve ready reader errors during erasure write failures

When erasure writers fail after the producer has already surfaced a reader error, return the reader error instead of aborting the producer and masking it with the write-side failure.

This keeps UploadPart body read timeouts classified as RequestTimeout under loaded pipeline timing.

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

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

* test: use current checksum API in multipart regressions

* fix: stabilize connect dependency update tests

Classify Connect proxy failures only when an explicit proxy is configured, preserve TLS classification across request error shapes, keep short object performance windows from spending too much budget on cleanup, and update multipart checksum tests for the current HashReader API.

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Hauser <housemecn@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hauser <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-14 22:03:14 +08:00
Chris 270180674b Retain profile service failure evidence (#7868)
ci: retain profile service failure evidence
2026-09-14 22:01:04 +08:00
Chris c08be224ce Run profile service acceptance directly (#7864)
ci: run profile service acceptance directly
2026-09-14 21:17:30 +08:00
Chris 66768e82cb Authorize the private Connect acceptance checkout (#7863)
ci: authorize private Connect harness checkout
2026-09-14 21:15:11 +08:00
Chris a107e1b171 Verify profile service jobs on native Linux (#7862)
ci: verify profile service jobs on native Linux
2026-09-14 21:12:12 +08:00
Henry Guo 5c73a3d602 fix(table-catalog): map transient lock failures to unavailable (#7841) 2026-09-14 21:09:25 +08:00
Chris 5eee2aaead fix(auth): reject unsigned x-amz headers on header-signed SigV4 requests (#7796)
* fix(auth): reject unsigned x-amz headers on header-signed SigV4 requests

A SigV4 request authenticated with an Authorization header only binds the headers named in its SignedHeaders list, but RustFS acted on every x-amz-* header that arrived, so a replayed header-signed PutObject carrying an unsigned x-amz-copy-source became a CopyObject run as the signer that could copy any object the signer can read (GHSA-xm99-m3gq-83g8). The presigned form was already closed by GHSA-g8w9-qw9q-fghr.

reject_unsigned_amz_headers_on_sigv4_request now guards S3Access::check and S3Router::check_access: every SigV4 signed-header list the request carries must cover every x-amz-* header, the Authorization header is parsed with the verifier's own s3s-sigv4 parser and compared case-insensitively, the algorithm token is pinned to AWS4-HMAC-SHA256 because the upstream header path accepts any token, and the exempt set mirrors the upstream s3s fix (x-amz-content-sha256, x-amz-decoded-content-length, x-amz-trailer, x-amz-checksum-algorithm) plus x-amz-cf-id. Adds ghsa_xm99 unit, router and e2e regressions, raises the security smoke floor to 28, and records the advisory in docs/testing/security-regressions.md and CHANGELOG.md.

* chore(deps): switch s3s and s3s-sigv4 to crates.io 0.16.0

* fix(server): enforce the SigV4 header guard ahead of s3s dispatch

s3s 0.16 verifies the claimed algorithm as the first step of its own
signature flow, so a request whose Authorization header swaps the
AWS4-HMAC-SHA256 token was answered with 501 NotImplemented before
RustFS's access layer could rule on the unsigned x-amz-copy-source
(GHSA-xm99-m3gq-83g8). Add the SigV4HeaderGuardLayer as the innermost
external stack layer, running reject_unsigned_amz_headers_on_sigv4_request
in front of s3s and serializing its rejections as the same AccessDenied
S3 error document the access layer produces.

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-14 21:08:45 +08:00
Chris a8ace89516 fix(s3): honor presigned UploadPart checksum queries (#7748)
* fix(s3): honor presigned UploadPart checksum queries

* fix(s3): keep checksum errors within gateway boundary

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-14 21:05:00 +08:00
Chris dd847d8e4c fix(admin): keep the gateway key inventory off the s3s surface (#7817)
The gateway key inventory handler added in #7785 imported s3s directly and built four errors with s3_error!, pushing the s3s footprint ratchet to 210 files and 1592 s3_error! lines on main. Route its S3 types and error construction through the crate::admin::storage_api::s3 facade like sibling admin handlers; the codes, HTTP statuses and messages are unchanged, and a new unit test pins them.

Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-14 21:04:45 +08:00
hector 6b2dd06ce6 ci: file scheduled-failure issues in rustfs/backlog (#7847)
Scheduled pipeline failures currently open [scheduled-failure] tracking
issues in rustfs/rustfs itself, adding bot noise to the code repository's
issue tracker. Route that alerting to rustfs/backlog instead.

- schedule-failure-issue gains target-repository (default rustfs/backlog);
  dedupe lookup, comment appends and issue creation target that repository.
- New source-token input (default ${{ github.token }}) reads the failed-job
  list from the reported run; the run still lives in and links to
  rustfs/rustfs. The issue body now carries a '- Repository:' line.
- All consumer workflows pass secrets.BACKLOG_ISSUE_TOKEN; the
  workflow-scoped GITHUB_TOKEN cannot open issues cross-repo. Alert job
  permissions blocks are unchanged.
2026-09-14 21:04:30 +08:00
Hauser afc8861fec fix: resume GET body after peer short EOF (#7852)
* fix: resume get body after short eof

Treat mid-stream short EOF errors as resumable when the S3 GET body has an attached resume context. This lets peer-kill reads reopen the same object version at the emitted offset instead of aborting the 200 response body.

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

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

* fix(connect): surface TLS peer bootstrap failures

Map Connect registration TLS peer validation failures to the dedicated bootstrap error so wrong CA inputs fail closed with the sanitized TLS variant instead of the generic exchange error.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-14 21:04:09 +08:00
Chris a5ec319872 Freeze the heartbeat producer capability registry (#7861)
test(connect): freeze heartbeat producer capabilities
2026-09-14 21:01:03 +08:00
Chris 2a631215dd Preserve safe profile job failure reasons (#7860)
fix(connect): preserve safe profile job failure reasons
2026-09-14 20:50:12 +08:00
唐小鸭 2f2e775655 fix(kms): refuse key ids that leave the key prefix on the Vault backends (#7727)
Only the Local backend refused a key identifier containing a path
separator. On Vault KV2 a create with the name bad/name succeeded and
produced a nested KV2 path that the listing then reported as a
directory rather than a key, and an identifier containing .. addressed
a record outside the configured key prefix once the HTTP client
normalised the URL; Vault Transit built its transit key name and its
metadata path from the same unchecked identifier.

Lift the Local backend's containment rule into a shared segment check
(empty, /, backslash, NUL, . and ..) and apply it at the single point
where each backend turns the identifier into a path or a Transit key
name, so create, describe, encrypt, delete and the metadata writes all
refuse with InvalidKey before any request reaches Vault. The admin API
already maps that to 400. The AWS backend is untouched: it addresses
keys by ARN and alias, both of which contain /.

Refs rustfs/backlog#2474 (KMS-213 CreateNegatives on vault-kv2).

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-14 12:37:45 +00:00
唐小鸭 1ef3974bad fix(replication): count a bodiless 405 as a replicated delete marker (#7756)
* fix(replication): accept a bodiless 405 as a replicated delete marker during resync

A resync verifies each delete marker with `HEAD ?versionId=<marker>` on the
target. S3 targets (RustFS, MinIO, AWS) answer that with 405 and no body,
and the SDK only synthesizes an error code for 404, so the error arrived
with `code() == None`. `is_retryable_delete_replication_head_error` then
treated it as an ambiguous failure: every delete marker counted as a failed
object with `target service error`, and a site resync over any bucket that
holds a delete marker reported the whole bucket as failed
(rustfs/backlog#2479, SITE-105 on rc.6 and nightly).

Use the raw HTTP status the way the 404 path already does: a 405 without a
code confirms the marker propagated. Ambiguous statuses still fail.

- Unit tests drive `verify_resync_head_result` against a scripted target
  answering 405 (accepted) and 503 (still failed).
- e2e `test_site_replication_resync_replicates_delete_marker` joins two
  sites, converges a live object and a delete marker, and requires the site
  resync to complete with zero failed objects; the repl-nightly selection
  digest is refreshed for the new case.

* fix(replication): verify marker-version purges by absence during resync

A delete-marker resync entry with a pending or failed version purge asks
the target to remove the marker, so the bodiless 405 that proves a
created marker propagated proves the purge did not happen. Accept the
405-as-success mapping only for marker creation (empty
version_purge_status); a purge counts as replicated only when the target
answers not found, and any other HEAD outcome stays failed. Unit tests
drive both purge statuses against the 405 fixture and the 404 fixture.
2026-09-14 11:41:35 +00:00
Chris 36227adc96 fix(io-metrics): drop the rustfs-common edge by injecting the S3 telemetry observer (#7795)
* fix(io-metrics): drop the rustfs-common edge by injecting the S3 telemetry observer

dc700d366 (#7775) made the io-metrics leaf crate depend on rustfs-common
to publish typed telemetry trace events from S3HttpRequestGuard, which
breaks the backlog#1834 leaf-crate rule (io-metrics may only depend on
rustfs-s3-ops).

S3HttpRequestGuard now takes an optional fn-pointer completion observer
(operation, latency, 2xx?) and knows nothing about the trace bus. The
server builds guards through rustfs::server::s3_http_request_guard, which
attaches the observer only while a telemetry subscriber exists and owns
the S3Operation -> TelemetryTraceOperation mapping.

* chore(guard): admit #7785 s3s ratchet growth (+1 file, +4 lines)

#7785 landed the gateway key inventory admin handler after the
baselines were verified, leaving check_s3s_footprint.sh red on main
and every branch cut from it (measured 210 files / 1592 lines vs
209/1588 baselines).

The handler follows the house admin convention whose Operation::call
signature is s3s-typed (S3Request/S3Result), so it cannot route
through a non-s3s seam until the s3gate admin migration replaces the
admin router (rustfs/backlog#1677 F1); no local refactor can shed the
file-level import. Record the measured growth with rationale:
files 209 -> 210, s3_error! lines 1588 -> 1592.

* fix

* fix: resolve clippy gate failures in connect and CLI surfaces

- Box the two large CommandResult performance variants and
  LicenseRenewalOutcome::Installed (clippy::large_enum_variant)
- Return RelayError instead of () from RelayTransport::deliver and the
  test destination (clippy::result_unit_err)
- Drop an unused mut on the protected relay file handle (unused_mut)
- Pass the digest by value to base64 encoding and drop a redundant
  result_json clone (clippy::needless_borrows_for_generic_args,
  clippy::redundant_clone)

* fix(connect): resolve the remaining site-replication clippy errors

- Group the endpoint credential triple into SiteReplicationCredentials
  so SiteReplicationEndpoint::new takes 6 arguments
  (clippy::too_many_arguments)
- Take the build-feature validator by &str (clippy::ptr_arg); the
  validator closure stays because &String needs the deref coercion

* style: apply rustfmt to the site-replication credential grouping

* fix(connect): keep the CPU profile fixture off the ecstore import scan

#7811 used "rustfs_ecstore::disk::read_object" as the raw-symbol fixture
of the profile redaction test, which the architecture guard counts as a
direct rustfs_ecstore reference outside the compatibility boundaries
(its scan deliberately includes inline test modules). The accumulator
treats symbols opaquely, so use the internal rustfs::storage:: path that
keeps the fixture realistic without matching the import scanner.

* fix

* fix

* fix

* fix

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-14 10:23:52 +00:00
Chris cdf8a81c47 fix(connect): publish diagnostic producer capabilities (#7837) 2026-09-14 14:09:43 +08:00
Chris 6db5ac4968 fix(connect): emit canonical top envelope time (#7836) 2026-09-14 13:53:08 +08:00
Chris c842eed84c Upload diagnostic job results through support bundles (#7831)
feat(connect): upload diagnostic job results
2026-09-14 12:58:00 +08:00
Chris f59e6be338 fix(connect): emit canonical report manifest time (#7830) 2026-09-14 12:38:42 +08:00
Chris 17a9dff081 Run signed diagnostic jobs in the RustFS service (#7829)
feat(connect): run diagnostic jobs in service
2026-09-14 12:21:27 +08:00
Chris f8f2cb05fb fix(connect): accept KMS report upload authorization (#7828) 2026-09-14 12:18:58 +08:00
Chris 174948bd2a feat(connect): verify typed diagnostic jobs (#7827) 2026-09-14 12:11:54 +08:00
Chris 20f4c26417 fix(connect): parse inspect paths as paths (#7826) 2026-09-14 12:11:31 +08:00
Chris 0d322c5086 feat(connect): wrap diagnostic reports for upload (#7822) 2026-09-14 11:02:33 +08:00
Chris db9a420fd2 feat(connect): expose object inspect CLI (#7821) 2026-09-14 10:54:51 +08:00
Chris 9749447825 test(connect): retain CPU profile import bundle (#7820) 2026-09-14 10:54:25 +08:00
Chris 874d0d7b63 test(connect): retain CPU profile verification key (#7819) 2026-09-14 10:50:42 +08:00
Chris d2e2aa7d0b test(connect): verify profile cpu release artifacts (#7818) 2026-09-14 10:48:17 +08:00
Chris c1ecf7155b ci: enable CPU profiling in supported release builds (#7815) 2026-09-14 10:32:24 +08:00
Chris 3eb3213bca fix(connect): observe host traffic in top net (#7814) 2026-09-14 10:31:19 +08:00
Chris b391ec4f0e ci: retain top disk acceptance archive (#7812) 2026-09-14 10:29:28 +08:00
Chris 97ae2ca39a feat(connect): capture bounded local CPU profiles (#7811) 2026-09-14 10:28:39 +08:00
Chris a0bbcba2b9 test(connect): require native top.disk runner (#7809) 2026-09-14 10:24:28 +08:00
Chris dfcee45b98 test(connect): verify top.disk release artifacts (#7808) 2026-09-14 10:21:37 +08:00
Chris b2420a0761 feat(connect): capture live RPC activity (#7807) 2026-09-14 10:21:09 +08:00
Chris 1cd9d1ed5d feat(connect): collect native Linux thread states (#7806)
Collect bounded aggregate thread states from Linux procfs while preserving explicit unsupported outcomes for unavailable scopes.
2026-09-14 10:00:48 +08:00
Chris 797f5b88ad feat(connect): capture live API activity (#7804) 2026-09-14 09:58:43 +08:00
Chris 27b167b543 feat(connect): expose live lock count snapshots (#7801) 2026-09-14 09:40:40 +08:00
Chris b6fbe8f043 fix(release): serialize Linux binary links (#7797) 2026-09-14 08:11:54 +08:00
Chris 3d75acadd3 fix(release): include CLI in Linux artifacts (#7794) 2026-09-14 07:31:34 +08:00
Chris ecaf68d641 fix: skip IAM migration when legacy volume is absent (#7792) 2026-09-14 06:57:54 +08:00
Chris 2a4c13013a fix: classify explicit proxy failures (#7791) 2026-09-14 06:01:23 +08:00
Chris b5a915e338 fix(connect): capture telemetry from server runtime (#7790) 2026-09-14 05:53:00 +08:00
Chris 5d66e9e15b fix: retry interrupted uploads through proxies (#7789)
fix: retry proxied upload interruptions
2026-09-14 05:52:29 +08:00
Chris 4772095e86 fix: retry IAM migration quorum failures on startup (#7788) 2026-09-14 05:52:17 +08:00
Chris 64e0ac08ce revert: remove M1 gateway git dependency until CI can access rustfs/gateway (#7787)
Revert "feat(server): add RUSTFS_S3_STACK with a gateway GetBucketLocation path (#7786)"

This reverts commit b5c2e1da90.
2026-09-14 04:12:35 +08:00
Chris b5c2e1da90 feat(server): add RUSTFS_S3_STACK with a gateway GetBucketLocation path (#7786)
* fix(connect): restore the site replication producer build

The producer shadowed its object_url helper with a local of the same
name, and its test placed the consent nonce on the request, so neither
the rustfs library nor its tests compiled on main.

* feat(server): add RUSTFS_S3_STACK with a gateway GetBucketLocation path

RUSTFS_S3_STACK=legacy (default) keeps the s3s service as the whole S3
entry. RUSTFS_S3_STACK=gateway routes GetBucketLocation through the
RustFS Gateway pipeline pinned at rustfs/gateway@90b83a20 and falls back
to the same s3s service for every other request, decided before the
body is read. Refs rustfs/backlog#1752.
2026-09-14 04:02:20 +08:00
Chris 736e4fb8e8 feat(admin): add a gateway key inventory for the S3 stack switch (#7785)
GET /rustfs/admin/v3/gateway-key-inventory (admin:InspectData) lists
every stored object key the RustFS S3 gateway would refuse on every
operation, so an operator can copy those objects to a safe key before
switching stacks. The rules mirror the gateway key floor; . and ..
segments never appear because ecstore refuses them on every write.

Refs rustfs/gateway#754
2026-09-14 03:51:34 +08:00
Chris df6563e5c0 feat(connect): add service license relay import (#7784) 2026-09-14 03:17:57 +08:00
Chris 2fc3494efb refactor(filemeta): own RestoreStatus and drop the s3s dependency (A3c) (#7782)
Replace s3s::dto::{RestoreStatus, Timestamp} in filemeta with a
filemeta-owned RestoreStatus (same field names, OffsetDateTime expiry).
RestoreStatusOps and parse_restore_obj_status keep their signatures and
persisted rendering; the ecstore restore finalize / lifecycle restore
writers and rustfs RestoreObject now build the filemeta type.

- crates/filemeta: drop s3s; tokio "time" becomes a dev-dependency
  (the metacache tests got it through s3s feature unification).
- metadata_keys: drop the s3s half of the historical-source cross-check;
  PINNED, the pre-A3a fixture and per-character mutation still pin keys.
- new test re-renders the pre-A3a fixture restore value byte-for-byte.
- s3s footprint baselines 210 -> 209 files, ecstore 37 -> 36.

Refs rustfs/backlog#1735
2026-09-14 03:05:49 +08:00
Chris f417d67d96 feat(connect): sign site replication target pair (#7781) 2026-09-14 03:05:38 +08:00
Chris 478cce587d fix(rustfs): restore test compilation (#7780) 2026-09-14 02:59:39 +08:00
Chris 02b3cd3032 fix(connect): restore rustfs build after site replication perf producer (#7779)
#7769 left rustfs/src/connect/diagnostics/perf_site_replication.rs failing
to compile: a local binding named object_url shadowed the object_url()
helper in the same scope (E0618), and a test put the consent nonce on the
request instead of LocalSiteReplicationConsent (E0063/E0560).

Rename the binding to source_url and move the test nonce into the consent.
2026-09-14 02:56:45 +08:00
Chris e5d9c385bd fix(connect): restore site replication test nonce (#7778) 2026-09-14 02:54:25 +08:00
Chris 9ce1bccfb1 fix(connect): avoid shadowing replication URL helper (#7777) 2026-09-14 02:52:38 +08:00
Chris dc700d3662 feat(connect): add typed telemetry trace source (#7775) 2026-09-14 02:46:22 +08:00
Chris 44c49733fd feat(connect): add device report upload client (#7774) 2026-09-14 02:46:12 +08:00
Chris 136bb5de61 fix(connect): export relay CLI types (#7773) 2026-09-14 02:40:19 +08:00
Chris 2ac1a70e68 feat(connect): add approved artifact relay (#7772)
feat(connect): add approved artifact relay core
2026-09-14 02:33:12 +08:00
Chris ca20e06405 feat(connect): add device license renewal client (#7771) 2026-09-14 02:21:53 +08:00
Chris d99460ced0 refactor(ecstore): read persisted object-metadata keys from the filemeta authority (A3b) (#7770)
Migrate ecstore and rustfs-lifecycle consumers of the persisted xl.meta
meta_user keys (object lock, restore, replication status, storage class,
SSE read) from s3s::header / rustfs_utils header constants to
rustfs_filemeta::metadata_keys. HTTP header production is unchanged and
every lookup keeps its previous exact / case-insensitive mode.

- SSE key: persisted only as lowercase; the mixed-case spelling is
  outbound replication user metadata. Keep the case-insensitive read and
  pin it against the pre-A3a fixture.
- warm tier: strip promoted keys case-insensitively so the persisted
  X-Amz-Replication-Status is no longer forwarded to the tier.
- fix stale HashReader::add_checksum_from_s3s call in ecstore tests.

Refs rustfs/backlog#1735
2026-09-14 02:12:36 +08:00
Chris c1190c24e6 feat(connect): add site replication performance producer (#7769)
feat(connect): add site replication performance core
2026-09-14 01:42:53 +08:00
Chris 1066a08aca fix(ecstore): refuse writes on unreadable bucket configs with 503 (#7768)
Close the remaining paths where a stored bucket sub-configuration whose
bytes cannot be parsed still read as absent (rustfs/backlog#1734, slice 2):

- Object writes resolve versioning through BucketVersioningSys::get_for_write;
  RUSTFS_BUCKET_CONFIG_PARSE_MODE=strict refuses them, the default permissive
  mode keeps the historical unversioned write and records it.
- Versioning, Object Lock, encryption, public access block and notification
  getters and the delete-time versioning check return the typed
  UnreadableBucketConfig refusal, which ApiError maps to 503 naming the
  bucket, config and stored length. StorageError::clone keeps it typed.
- Object Lock checks stay on when the lock config is unreadable.
- Notification setup isolates an unreadable config to its bucket instead of
  clearing its rules; GetBucketNotificationConfiguration reports it.
- Add rustfs_bucket_metadata_parse_failed_total and
  rustfs_bucket_metadata_unparsable_current, an invalid-mode startup check,
  and the s3gate-parse-strict compat register entry.
2026-09-14 01:40:29 +08:00
Chris 7111f8b44d Deliver diagnostic scheduler receipts to Connect (#7767)
feat(connect): deliver diagnostic scheduler receipts
2026-09-14 01:34:26 +08:00
Chris 1e53090cbb refactor(rio): own trailer source and drop s3s dependency (#7764)
Move the aws-chunked trailing checksum handle behind a RustFS-owned rustfs_rio::TrailerSource (Pending/Missing/Present lookup with a documented EOF timing contract) and adapt s3s::TrailingHeaders once in the application crate. rio no longer depends on s3s; behaviour is unchanged.

Refs rustfs/backlog#1735
2026-09-14 01:12:56 +08:00
Chris fad133f120 Sync telemetry producer fixtures (#7763)
test(protocol): sync telemetry producer vectors
2026-09-14 01:02:57 +08:00
Chris cbaa3367d9 Sync site replication result fixtures (#7762)
test(protocol): sync site replication result fixtures
2026-09-14 00:55:16 +08:00
Chris a875cdea1e Sync network performance protocol fixtures (#7761)
test(connect): sync diagnostic protocol fixtures
2026-09-14 00:48:56 +08:00
Chris 1a7db3ee12 refactor(filemeta): own persisted metadata key authority (#7760)
Add rustfs_filemeta::metadata_keys as the single source of the nine xl.meta meta_user keys, byte-identical to current literals, and migrate filemeta consumers off s3s::header and rustfs_utils header constants. Pin the literals, cross-check historical sources, and prove old xl.meta bytes still decode via a captured fixture and a per-character mutation sweep.

Refs rustfs/backlog#1735
2026-09-14 00:08:40 +08:00
Chris f37e07cca3 fix(ecstore): refuse RMW and reads of unreadable bucket configs (#7759)
An XML bucket sub-config whose stored bytes cannot be parsed was still folded into "absent" on two paths: update_config_with handed it to mutate, which rebuilt it from nothing and overwrote the only copy of the bytes, and the tagging/lifecycle/CORS/website/logging/accelerate/request-payment getters reported ConfigNotFound.

Add an explicit ConfigState (Absent / Valid / Unreadable) over the existing retained parse failure, refuse the read-modify-write of an unreadable target config before mutate runs (per config, other configs stay writable), and make those getters fail closed. GetBucketLifecycle now only maps ConfigNotFound to NoSuchLifecycleConfiguration. The Swift tagging rewrite recognizes the refusal by type instead of a string sentinel.

Refs rustfs/backlog#1734
2026-09-13 23:50:58 +08:00
Chris cc29b03a05 docs(security): add presigned copy advisory lesson (#7754) 2026-09-13 21:56:02 +08:00
Chris feb3ecb12b test(connect): cover nested allocator profile stats (#7717)
* test(connect): cover nested allocator profile stats

* test(connect): use async profile test lock

* test(security): allow synthetic redaction fixture

---------

Co-authored-by: Hauser <housemecn@gmail.com>
2026-09-13 21:17:29 +08:00
Chris a7e2b40735 feat(connect): add local inspect export producer (#7750)
* feat(connect): add local inspect export producer

* fix(connect): use the storage inspection facade
2026-09-13 20:26:21 +08:00
Chris 50d4ac1fa5 feat(connect): emit drive unavailable log events (#7747) 2026-09-13 19:59:59 +08:00
Chris da56f5bf5b feat(connect): support explicit enterprise proxies (#7745)
feat(connect): add explicit proxy transport
2026-09-13 19:40:40 +08:00
Hauser 4ac1a9fbb9 chore(deps): update flake.lock (#7733)
* chore(deps): update flake.lock

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/17de0b9' (2026-09-04)
  → 'github:NixOS/nixpkgs/aff8a0b' (2026-09-10)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c361047' (2026-09-05)
  → 'github:oxalica/rust-overlay/228ecef' (2026-09-12)

* fix: satisfy Rust 1.98 clippy lints

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-13 17:40:06 +08:00
Hauser 109afa17b7 chore(deps): update flake.lock (#7733)
* chore(deps): update flake.lock

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/17de0b9' (2026-09-04)
  → 'github:NixOS/nixpkgs/aff8a0b' (2026-09-10)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c361047' (2026-09-05)
  → 'github:oxalica/rust-overlay/228ecef' (2026-09-12)

* fix: satisfy Rust 1.98 clippy lints

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-13 17:38:29 +08:00
唐小鸭 ecdc55fa4b fix(site-replication): keep an operator's bucket-level target to a peer instead of taking it over (#7709)
* fix(site-replication): keep an operator's bucket-level target to a peer instead of taking it over

Site replication wired each bucket by looking for an existing replication
target "to the same peer" and rewriting the first match in place as its own
same-name target. An operator's bucket-level target that happened to point
at that site (different target bucket, operator credentials) was the first
match whenever it pre-dated the join, and the reconciler repeats the pass
every 600s, so the takeover also depended on target order afterwards. The
operator's rule then named an ARN no target backed and their bucket
replication stopped silently, while the inherited bucket-level reset id
made every site resync report the bucket as owned by another resync
(rustfs/backlog#2479, rustfs/backlog#2489).

Follow MinIO's `getRemoteARN` / `getRemoteARNForPeer` shape instead:

- Wiring updates a target in place only under the same ARN, or when it is
  recognisably the site's own under an older ARN shape (same peer,
  same-name target bucket, site replication service account). Anything
  else gets the site target added next to it.
- The site resync manifest takes the target the derived
  `site-repl-<deployment id>` rule names (same-name shape as fallback), so
  an operator target to the peer neither aborts the bucket as "multiple
  remote targets matched peer" nor gets resynced into.
- Peer removal prunes only targets a pruned derived rule names or the
  same-name target bucket; operator targets stamped with the peer's
  deployment id survive together with their rules.

Unit tests cover the three predicates. e2e
`test_site_replication_keeps_operator_bucket_target_to_peer` runs a
bucket-level replication plus `replication-reset` to the future peer, joins
the sites, and requires the operator target untouched, both paths
delivering, the site resync completing against the site target, and the
operator target and rule surviving `replicate remove --all`; without the
fix it fails at the join with the operator target gone. The repl-nightly
selection digest is refreshed for the new case.

* test(site-replication): drop a redundant clone flagged by clippy

The reconcile unit test cloned the remote peer into the state map although
the binding is not used afterwards; workspace clippy (-D warnings) rejects
that as redundant_clone.
2026-09-13 15:39:59 +08:00
Chris 37250f649a Add authenticated inter-node network performance probes (#7739)
* feat(connect): add authenticated inter-node network probes

* fix(connect): respect storage facade boundary
2026-09-13 14:10:23 +08:00
Chris 8ae8de0d28 feat(connect): add bounded object performance diagnostics (#7734)
feat(connect): add object performance producer
2026-09-13 13:17:15 +08:00
Chris 909de31d19 Add a local environment inventory command (#7728)
feat(connect): add local environment inventory command
2026-09-13 12:11:12 +08:00
Chris 71a8939d9b Add client-to-deployment performance diagnostics (#7726)
feat: add client performance diagnostics
2026-09-13 11:04:37 +08:00
Chris b5d481c24b Add bounded top diagnostic captures (#7722)
feat(connect): add bounded top diagnostic producers
2026-09-13 08:14:29 +08:00
Chris 45b19c7383 Add bounded drive performance command (#7721)
feat(connect): add bounded drive performance producer
2026-09-13 07:35:46 +08:00
Chris f2cdca42ed Add bounded local telemetry export commands (#7720)
feat(connect): add bounded telemetry producers
2026-09-13 07:00:08 +08:00
Chris 6a5314f698 feat(connect): add bounded network performance producer (#7719) 2026-09-13 06:09:28 +08:00
Chris 64c11f5764 feat(connect): add bounded local log capture (#7718) 2026-09-13 05:45:23 +08:00
Chris 95e1b84ecf feat(connect): add consent-bound profile exports (#7716)
feat: add consent-bound profile exports
2026-09-13 04:33:30 +08:00
Chris d8ef9e803e feat(connect): advertise environment diagnostics (#7714) 2026-09-13 04:01:17 +08:00
Chris eb97666050 Add consent-bound diagnostic scheduling (#7713)
feat(connect): schedule consent-bound diagnostics
2026-09-13 03:28:03 +08:00
Chris 6e60737cc4 Add Connect service license commands (#7711)
feat: add Connect service license client
2026-09-13 01:46:20 +08:00
Chris 3697d06850 Collect bounded Connect environment inventory (#7710)
feat(connect): collect environment inventory
2026-09-13 00:59:08 +08:00
唐小鸭 e2e6a2535a fix(kms): classify KMS/SSE error contracts and SSE-S3 headers (#7697)
* fix(sse): classify bare SSE-KMS writes when no KMS is available

A `aws:kms` request without a key id, on a bucket without a default key,
returned `500 InternalError` whenever no KMS service was running: the
"no KMS key available" branch exited with an untyped storage error before
the availability classification that the keyed form already received.

Route that branch through the same split: `503 ServiceUnavailable` while
a configured KMS is stopped, `400 InvalidRequest` when KMS was never
configured, and `400 InvalidRequest` naming the missing key id when a
running KMS has no default key. `CreateMultipartUpload` shares the path.

Adds a unit test for the bare form and an e2e module that stops KMS
through the admin API, runs a master-key-only node, and runs a Local KMS
without a default key; refreshes the e2e-full selection digests.

(cherry picked from commit c3259dadc3d603a9185a5b0ad9f83dfb884e61c8)

* fix(sse): keep KMS error classes on the encrypted read path

GetObject, CopyObject and UploadPartCopy on an SSE-KMS object whose key
no longer exists answered `500 InternalError` ("KMS key not found") while
PutObject under the same key already answered `400 KMS.NotFoundException`.
The read path carries its classification through ecstore's
`EncryptionResolutionErrorKind`, which had no kind for a missing key, a
denied KMS grant or a missing backend capability, so all three folded
onto `DecryptionFailed` and the S3 layer reported an internal fault.

Add `KeyNotFound`, `AccessDenied` and `NotImplemented` kinds, map them on
both sides of the boundary, and give an envelope the configured backend
cannot unwrap a diagnosable message while keeping its `500`.

Unit tests cover the kind round trip and the reader wrapping; a new e2e
test deletes a key immediately and checks GET/Copy return 400 with
`KMS.NotFoundException` while HEAD stays 200. The e2e-full selection
digests are refreshed from the current listing (the previous digests
predated the delete-authorization tests) and the e2e `create_default_key`
helper is updated to the accepted `EncryptDecrypt` spelling.

(cherry picked from commit 2523a9814e97caea318d4ff1a51bef3a4d4445b2)

* fix(kms): classify key-management errors on the admin routes

`POST /kms/keys`, the legacy `create-key` alias and `generate-data-key`
reported every backend refusal as `500`: a blank key name (which each
backend failed on differently, the Local backend by writing a key file
with an empty stem), a name already taken, an unknown key, a disabled key
and a capability the backend lacks. `delete` and the lifecycle routes
already classified the same errors.

Refuse a blank or whitespace name in `KmsManager::create_key` before any
backend sees it, and share one `KmsError` to status mapping across
create, delete and generate-data-key (400 for validation and key state,
404 for an unknown key, 409 for a taken name, 501 for a missing
capability, 500 only for damaged material). The XML-error routes carry
the same status explicitly since s3s derives none for a custom code.

The read-only Static backend now reports create, delete and
cancel-deletion as `UnsupportedCapability`, matching its rotate and
enable/disable answers, so the admin API returns 501 for all of them.

(cherry picked from commit e33cac5493c4d9d6662e0d2980b58ba2b24a6d1b)

* fix(sse): stop SSE-S3 responses from naming the wrapping KMS key

`x-amz-server-side-encryption-aws-kms-key-id` is defined for `aws:kms`
objects only, but PutObject, CopyObject, CreateMultipartUpload and
GetObject returned it for `AES256` objects too, carrying the KMS key that
wraps the SSE-S3 data key (the service default, or the literal `default`
on a node without KMS). The write paths copied `kms_key_id` from the
encryption material unconditionally, and the single-decrypt GET
classification did the same after resolving the key for authorization.

Add `EncryptionMaterial::response_kms_key_id`, which yields the id only
for SSE-KMS, use it at the four write-response sites, and gate the GET
classification the same way. CompleteMultipartUpload and HeadObject
already omitted the header.

Unit tests pin both directions; a new e2e test covers Put/Get/Head/Copy
and CreateMultipartUpload for AES256 with an aws:kms control. The
e2e-full selection digests are refreshed from the current listing.

(cherry picked from commit 29d793a63352b0b60fd53c565e80fdbede8964bb)

* fix(s3): validate PutBucketEncryption rules before storing them

A default-encryption rule naming an unknown `SSEAlgorithm` (for example
`AES128`), a rule without `ApplyServerSideEncryptionByDefault`, an empty
rule list, or a `KMSMasterKeyID` on an `AES256` rule was stored as
written: the only algorithm check on the route decided whether to fill
in the default KMS key. `GetBucketEncryption` then advertised that
configuration while the write path encrypted header-less writes under
its `AES256` fallback, so the bucket's declared and actual schemes
disagreed. Two comments claimed the route already refused unknown
algorithms.

Validate the configuration before any of it is applied: `MalformedXML`
for a malformed rule set or unknown algorithm, `InvalidArgument` for a
key id on a non-KMS rule, and nothing stored on refusal. Correct the two
comments to describe when the AES256 fallback is still reachable.

Unit tests cover every refusal and the accepted shapes; an e2e test
checks the refusals leave the previous configuration in place. The
e2e-full selection digests are refreshed from the current listing.

(cherry picked from commit 29e4486dce41197ed93f5253cdbabc57d27a4ddb)

* test(e2e): refresh e2e-full selection for the combined KMS/SSE fixes

* test: align two unit tests with the new KMS and bucket-encryption contracts

`scheduled_deletion_carries_a_deadline_and_can_be_cancelled` still
expects the state error (`InvalidOperation`) for cancelling a key that
is not pending deletion; only the Static backend's mutations moved to
`UnsupportedCapability`. The uninitialized-store PutBucketEncryption
test now sends a well-formed AES256 rule so it reaches the store lookup
instead of the new configuration validation.
2026-09-12 23:48:10 +08:00
Chris c447ad66e2 fix(ci): verify complete functional chain evidence (#7690)
* fix(ci): bind nightly lanes to one resolved source

* fix(ci): verify complete functional chain evidence
2026-09-12 12:09:58 +08:00
Zhengchao An eb96b402b1 feat(ci): measure queue and execution time by run attempt (#7689)
* feat(ci): measure queue and execution time by run attempt

* fix(ci): bound timing samples and recognize the old workspace lane

* fix(ci): sample completed runs for stable timing comparisons
2026-09-12 11:46:05 +08:00
Zhengchao An 01d8e4347f fix(ci): bind nightly lanes to one resolved source (#7688) 2026-09-12 11:26:12 +08:00
Zhengchao An 2909b1bfe1 fix(e2e): require a verified server binary for every e2e run (#7687)
* fix(ci): share quick checks and lint workflows

* fix(ci): install actionlint from its verified release

* fix(ci): reject dependencies on required quick checks

* feat(test): verify the E2E server build and source identity

* test(e2e): register verified Darwin test membership

* test(e2e): record verified Linux receipt test membership

* test(e2e): record compiled Darwin receipt test membership

* test(e2e): record compiled Linux receipt test membership

* test(e2e): record Darwin e2e-full membership after merging main

* fix(test): route scanner/heal evidence E2E runs through the verified server binary

The evidence runners built rustfs with plain cargo and then ran e2e_test directly, which now fails without a run receipt. They build through scripts/e2e_binary.py and run the e2e_test invocations under e2e_binary.py run; the obsolete rustfs.features stamp is removed.

* docs(e2e): run server-backed e2e commands through the verified binary wrapper

* test(e2e): record Linux e2e-full membership from the branch CI listing
2026-09-12 11:23:06 +08:00
GatewayJ 414176c47f fix(tables): reject reserved warehouse locations (#7671)
Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-12 11:22:23 +08:00
cxymds 3fd1ce414d fix: enforce S3 permissions for recursive force deletion (#7661)
* fix: enforce S3 authorization for recursive deletion

* fix: satisfy the s3s footprint guard

* fix: restore list versions policy compatibility (#7686)
2026-09-12 11:21:18 +08:00
cxymds 666dfd9f9f fix(s3): bound stalled UploadPart request bodies (#7659)
* fix(s3): bound stalled UploadPart request bodies

* fix(ci): preserve the S3S footprint ratchet
2026-09-12 11:21:03 +08:00
Hiroaki KAWAI 4d31621f0a test(kms): cover non-default Vault Transit paths (#7657)
* test(kms): cover custom Vault Transit metadata locations

* test(kms): include a non-default Transit mount in live coverage

* test(ecstore): tolerate cleanup quorum transition

* test(ci): restore ten-suite chain expectations

* test(ecstore): tolerate cleanup quorum transition in both causal polls

The unversioned duplicate-delete poll in batch_transitioned_delete_uses_free_version_per_item reads exact metadata while the same asynchronous free-version cleanup removes per-disk copies, so it can hit the same transient InsufficientReadQuorum as the versioned poll. Share one helper between both polls; every other error still fails immediately.

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-12 11:20:43 +08:00
cxymds d9e47d2813 fix(ci): align security workflow tests with chain (#7679) 2026-09-12 00:49:36 +00:00
hector d20059ab5a "feat(ci): add fault-tolerance degradation suite to the functional chain" (#7667)
Revert "feat(ci): add fault-tolerance degradation suite to the functional chain (#7663)"

This reverts commit 7c47c48e85.
2026-09-11 22:30:21 +08:00
Zhengchao An 509a0fa90c fix(ci): repair functional defaults and chain regression checks (#7664)
* fix(ci): default functional suites to nightly packages

* test(ci): follow the fault-tolerance chain handoff
2026-09-11 22:13:55 +08:00
hector 7c47c48e85 feat(ci): add fault-tolerance degradation suite to the functional chain (#7663)
Adds a fault-tolerance suite that verifies read/write behavior under
drive and node loss against the erasure-coding contract and snapshots
health-endpoint responses at every degradation tier. Scenarios derived
from product source (default_parity_count, erasure set sizing):

- A: single-node 4 drives (EC:2, read quorum 2): hide 1/2/3 drives
- B: multi-node 4x1 (one set of 4, EC:2): stop 1/2/3 nodes
- C: multi-node 4x4 (one set of 16, EC:4, read quorum 12): 1 node down
  lands exactly on the read-quorum boundary; 2 nodes down breaks it
- C2: multi-node 4x4 with RUSTFS_STORAGE_CLASS_STANDARD=EC:8 (read
  quorum 8, write quorum 9, lock majority 9): 2 nodes down puts reads
  inside the reported divergence window (read quorum met while the
  lock majority is broken)

By default a reads-refused-despite-met-read-quorum observation is
reported as known-divergence without failing the suite; the strict
input escalates it. Chain order becomes:
upgrade -> s3 -> kms -> tier -> storage -> heal -> pool -> security ->
replication -> fault-tolerance -> performance.
2026-09-11 21:55:39 +08:00
Jason Kossis 853ae63b6a fix(replication): correct peer joins and remote-state reporting (#7650)
* fix(replication): propagate verified peer deployment identities

* fix(replication): report actual remote peer state

* fix(replication): defer initial sync until all peers join

* test(replication): shut down TLS fixtures cleanly

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-11 15:12:04 +08:00
GatewayJ f02bc947cd fix(admin): expose OIDC account display fields (#7654)
Expose verified OIDC username and email claims as display-only metadata on self-account responses while preserving the virtual parent as the authorization identity.\n\nKeep rustfs-madmin public response structs unchanged by adding the optional wire fields through private handler response wrappers.
2026-09-11 05:58:31 +00:00
RJ Regenold 0cbc3ffe61 fix(storage): prevent readiness after native migration failures (#7652)
* fix(storage): prevent readiness after native migration failures

* fix(storage): skip unsupported IAM records before reading

* fix(storage): use stable typed migration metadata errors

* fix(storage): include migration record in startup errors

* test(storage): cover native migration startup failures

* test(storage): use array chunks in migration fixture

---------

Co-authored-by: RJ Regenold <214054+rjregenold@users.noreply.github.com>
Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-11 13:45:47 +08:00
Henry Guo b1cc286cac fix(ecstore): make directory mtime fixture portable (#7623)
* fix(ecstore): make directory mtime fixture portable

* style(ecstore): format mtime fixture assertion

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-11 07:43:44 +08:00
唐小鸭 50b31bc75b fix(s3): reject oversize single PUT early and map body errors to 4xx (#7635)
* fix(s3): reject oversize single PUT early and map body errors to 4xx

A single PutObject above the 5 GiB single-request ceiling was only
rejected after the client had streamed 5 GiB into s3s's read-time body
budget, and the resulting BodySizeLimitExceeded surfaced from the erasure
writer as 500 InternalError. A body whose connection hit EOF before
Content-Length bytes arrived (hyper's IncompleteBody) was also a 500.
SDKs retry 500s, so one oversize upload was resent from offset 0 five
times.

- PutObject and UploadPart reject a declared length above
  MAX_SINGLE_PUT_OBJECT_SIZE with 400 EntityTooLarge before reading the
  body; the constant moves to rustfs_config so the s3s limit and the
  admission check share one value.
- ApiError maps BodySizeLimitExceeded to EntityTooLarge and a hyper body
  EOF to IncompleteBody across both io::Error conversions.

Fixes #7596.

* test(s3): cover UploadPart admission, aws-chunked length, real s3s limit

- Poll-counting test body proves PutObject and UploadPart reject a
  declared size above the ceiling with zero body polls; exact-cap and
  zero-length parts pass admission.
- A STREAMING-* aws-chunked PUT whose framed Content-Length exceeds the
  cap is admitted when the decoded length is within it and rejected when
  the decoded length is over it.
- The display-based BodySizeLimitExceeded matcher is checked against the
  real error produced by the pinned s3s Body budget.
2026-09-10 15:06:19 +00:00
Zhengchao An 5cd58319ed fix(heal): preserve retryable batch failures during recovery (#7642)
* fix(heal): preserve retryable batch failures during recovery

* test(heal): pin prebuilt hooks binaries in ci
2026-09-10 13:09:02 +00:00
Zhengchao An c677590d50 chore(release): prepare 1.0.0-rc.6 (#7641)
* chore(release): prepare 1.0.0-rc.6

* ci: run offline enrollment boundary checks independently
2026-09-10 13:07:12 +00:00
cxymds 94d875ea07 feat: configure the console base path at build time (#7636)
Embed RUSTFS_CONSOLE_BASE_PATH into console routes, browser redirects, and startup URLs so OEM builds can use their own namespace. Preserve the default for an unset or empty build variable and keep route tests prefix-aware.
2026-09-10 11:32:55 +00:00
Zhengchao An 2f5db4e8f2 chore(release): merge release into main for rc.6 (#7638)
* test(heal): cover MRF manifest CAS legacy transition (#7443)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): enable release branch checks and repair test imports (#7441)

* test(scanner): structure heal release evidence lanes (#7442)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): require segment producer identities (#7444)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): expose recovery intent identity (#7445)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): summarize failed heal perf reports (#7446)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(common): retain unmatched MRF repair proofs (#7447)

Add a recorded verified-repair consumer that discharges only exact retained anchors for the requested bucket while leaving unmatched proofs in the event ring. This gives the future durable successor writer a fail-closed primitive before any tombstone or GC path is enabled.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): route replication read plan through boundary (#7448)

* test(scanner): cover flat-bucket quantum fairness (#7449)

Add a production-entry scanner cohort regression that combines a wide flat bucket with a small bucket under a fixed object budget. The test keeps partial budgeted rounds unpublished, verifies small buckets are only marked after real execution, and confirms a later unbudgeted round can publish the complete aggregate.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): reuse same admission request id (#7451)

Keep a retried heal start with the same request id from bypassing admission deduplication when the transport replay cache is unavailable but the manager still owns the task.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require perf summary evidence fields (#7453)

Fail closed when measured passing Scanner/Heal ABBA summaries omit W10/W11 foreground pressure, lock wait, or attempt-cost evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind release evidence fields (#7452)

Require explicit scanner/heal release evidence field contracts for the scoped ACK and mixed-version rollback gates.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): tag dirty usage producer identities (#7454)

Record production-facing segment invalidation producer identities when existing object-level dirty usage hooks observe PUT, CopyObject, DeleteObject/DeleteMarker, and CompleteMultipartUpload mutations. Keep the data non-authoritative and process-local so segment reuse activation still requires durable generation-window proof.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): gate release evidence bundles (#7457)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require hard evidence ABBA manifest (#7459)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(heal): add MRF committed snapshot writer (#7458)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover EC8+4 restart shard rebuild (#7461)

Retry heal-control RPCs once after transport auth rejects a stale replay-scope epoch, and add a distributed EC8+4 restart heal evidence case that rebuilds a replaced drive with exact shard/body assertions.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): echo measured release evidence in ABBA fake adapter (#7460)

Keep the synthetic ABBA test adapter aligned with the measured release evidence contract so result validation covers release_evidence drift.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require complete ABBA summary matrix (#7462)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: retain MRF replay journal after accepted/merged replays

Keep startup replay journal until a durable successor snapshot exists after Accepted/Merged admission.

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

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

* heal: reclaim superseded MRF snapshots after readback

Add a committed snapshot predecessor cleanup primitive that deletes only an older slot after the successor is read back as the current committed snapshot.

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

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

* heal: require exact owner for object receipts

Require object heal receipts to match the expected bucket incarnation before they can be recorded as positive repair evidence. This prevents stale or cross-incarnation receipts from clearing the wrong heal responsibility.

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

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

* heal: latch object receipt owner before repair

Capture the expected bucket incarnation before invoking object repair so a post-repair owner change cannot rewrite the responsibility that a storage receipt is allowed to prove.

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

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

* heal: replay committed MRF checkpoints durably (#7465)

Prefer committed MRF checkpoints during startup replay, retain accepted replay responsibilities until exact verified repair proofs arrive, and reclaim committed manifests only after discharge.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind hard evidence bundle provenance (#7467)

Require Scanner/Heal release bundle fields to carry source, run, window, timestamp, command, and artifact format provenance before a measured gate can pass.

Keep EC8+4 and performance gate fields tied to a single measurement window so unrelated artifacts cannot be stitched into a release approval.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: reject dry-run object repair receipts

Do not record positive storage repair receipts for dry-run object heal tasks, even if a producer accidentally returns a matching receipt.

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

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

* heal: reject cancelled object repair receipts

Do not record positive storage repair receipts once an object heal task has been cancelled, even if the receipt still matches the requested owner and object identity.

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

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

* test(heal): reject failed object repair receipts

Cover the receipt consumer path where a storage repair result carries both an error and a matching positive receipt. The failure may be recorded, but the receipt must not create repaired, healthy, or absent proof.

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

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

* test(heal): cover bucket object repair receipts (#7468)

Cover bucket and root heal sweeps recording authoritative object outcomes only when storage receipts match the latched bucket incarnation.

Verify unavailable or stale receipt ownership keeps object repair execution intact while leaving canonical outcome proof as Unknown.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add EC8+4 heal restart evidence (#7469)

Register and wire Scanner/Heal background target restart and crash evidence cases for the 3x4 EC8+4 topology.

Validate the observed data/parity geometry in scanner-heal evidence receipts so multi-drive runs cannot satisfy the gate without proving EC8+4 metadata.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: verify replacement pool metadata repair (#7471)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover quorum and mixed repair receipts

Add focused oracles for transient quorum results that carry a matching receipt and for mixed grace plus repaired receipt batches. Only the repaired object may produce positive proof.

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

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

* heal: verify admin recreate pool metadata (#7474)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* heal: preserve merged result for duplicate submits

Keep same-request-id replay receipts accepted for the receipt API, but preserve the legacy submit_heal_request duplicate admission result as Merged.

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

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

* test(heal): preserve EC84 restart semantics (#7478)

Keep the EC8+4 background restart lane on the graceful-stop path and assert clean-restart marker absence only for restart scenarios. This prevents the hard evidence gate from silently exercising the crash path when it claims restart coverage.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF snapshot torn successor recovery

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

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

* test(heal): write EC84 distributed restart oracle (#7481)

Bind the distributed EC8+4 restart evidence lane to a scanner/heal oracle artifact so release validation can consume the real nextest run instead of accepting only a passing test.

Require the registry to assert 8+4 erasure geometry for the three-node, four-drive case.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): harden MRF replay boundaries (#7483)

Reject journal records with unknown version-presence flags even when their CRC is valid, so rollback/future payloads cannot be accepted as known records.

Gate committed checkpoint cleanup by the writer owner captured from the replay source, preserving retained manifests from other owners inside the same sequence window.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): bind MRF manifest CAS dirsync recovery (#7482)

Cover the committed MRF manifest path through LocalDisk conditional CAS when the metadata directory fsync fails. The fixture proves the previous manifest anchor survives rollback, an unanchored first successor is removed, and the legacy MRF journal remains readable even while global durability is relaxed.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): derive evidence runner profile from registry (#7484)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require mixed-version evidence roles (#7485)

Tighten the Scanner/Heal release bundle checker so mixed-version, scoped-ACK, and rollback fields cannot reuse a generic versions list without proving the expected evidence role.

Require version lists to use source revision identities and include the tested source revision. Also require profile evidence fields to name the core profiling artifacts before release approval.

The release gate remains blocked until measured field evidence is present.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): publish committed MRF runtime checkpoints (#7490)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind profile artifacts in release evidence (#7487)

Require Scanner/Heal release bundles to attach every required profiling artifact to P1 profile evidence with relative paths, artifact formats, non-empty files, hashes, and optional per-artifact measurement-window checks.

Document the tightened release bundle profile contract and cover missing, tampered, and mismatched-window profile artifact regressions in the existing checker self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden measured ABBA evidence claims (#7491)

Reject measured Scanner/Heal ABBA manifests whose mixed-version evidence uses the same baseline and candidate source revision or binary hash.

Require crash fault modes and profile artifact names to match the exact supported sets, rejecting missing, duplicate, and unknown values.

Update harness fixtures and regression coverage for same-build mixed-version claims and exact-set release evidence fields.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): group scanner heal evidence payload (#7494)

Group the EC8+4 Scanner/Heal evidence writer inputs into a typed payload so the distributed e2e crate stays within the clippy argument limit without weakening the lint.

The evidence writer still validates the same S3 bodies, physical shard census, process restart PIDs, and node listings.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF replay bundle fields (#7486)

Bind Scanner/Heal release bundle evidence for MRF durable replay to replay counts, retained responsibility anchors, and successor snapshot publication evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): profile EC84 evidence case runs (#7495)

Give Scanner/Heal evidence cases explicit runtime profiles so EC8+4 background restart and crash cases use their own object count, object size, and partial-progress timeout defaults instead of inheriting the legacy 4x1 case assumptions.

Expose the runtime profile in plan-only output and cover every registry case in the script self-test.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): reject empty release evidence artifacts (#7496)

Require Scanner/Heal release bundle artifact paths to resolve to non-empty files before hashing them.

Cover empty hard-gate artifacts in the existing release bundle checker self-test and keep profile artifact size checking on the shared artifact boundary.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require G14 same-window field coverage (#7489)

Tighten the Scanner/Heal release bundle checker so G14 same-window evidence must name the EC8+4, multi-set, and multi-pool fields covered in that measurement window.

Keep the release gate blocked when same-window evidence omits one of the required G14 fields, without changing production runtime behavior.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require two-hour measured ABBA windows (#7493)

Reject measured Scanner/Heal release ABBA manifests and summaries whose evidence window is shorter than the W21 two-hour release requirement.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF idle checkpoint cleanup (#7497)

Add a runtime cleanup regression test that publishes both retained replay and runtime committed checkpoints, writes scoped and legacy journals, and verifies idle cleanup removes every recovery anchor from the registered local disks.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(e2e): ignore untracked files in build identity (#7498)

Align e2e_test build provenance with Scanner/Heal evidence receipts and server binary provenance by treating only tracked source changes as dirty.

This prevents unrelated untracked worktrees or evidence directories from causing compiled test identity mismatches before real evidence cases can run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF disk-full evidence fields (#7499)

Require the Scanner/Heal release registry and bundle checker to carry explicit G08 MRF capacity, disk-full, and replica-loss evidence fields before release approval can pass.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind release JSON artifact provenance (#7500)

Require scanner/heal release evidence JSON artifacts to repeat their measured source revision, run identity, measurement window, gate, and field identity inside the artifact payload. This keeps a refreshed outer bundle hash from accepting stale summary or profile JSON from another run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF rollback mirror filtering (#7501)

Add a regression oracle that keeps scoped-only MRF responsibilities in the authoritative runtime snapshot while omitting them from the v1 legacy rollback mirror.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require segment activation evidence (#7504)

* test(scanner): require segment activation evidence

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

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

* test(scanner): cover activation proof inputs

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF crash matrix cases (#7505)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require release bundle domain evidence (#7502)

Reject scanner/heal release bundles that omit field-level domain evidence for scoped ACK, durable intent, mixed-version, scheduler pressure, profile cost, and two-hour pressure lanes.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF retention GC evidence (#7506)

Require P4 retained-responsibility release bundle evidence to list the retained replay anchor and idle cleanup cases, prove a two-hour retention window, and record both idle cleanup and verified-proof discharge observations.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup GC soak evidence (#7507)

Require the P4 release bundle to carry measured cleanup/GC soak evidence for retained MRF replay responsibilities. The bundle now needs a two-hour cleanup window, exact cleanup case coverage, observed verified idle GC, and zero pending responsibilities or stale journals after GC.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF disk-full commit anchors (#7509)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): report port allocator bind errors (#7510)

Surface the scanned port window, attempt count, and last bind error when the e2e port allocator cannot reserve a localhost port. This keeps Scanner/Heal evidence failures actionable when the environment blocks binds before business assertions run.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(sse): resolve 1.0.0 SSE/KMS blockers and P1 findings (#7511)

* fix(sse): resolve bucket default encryption per request

PUT and the POST-object/extract path resolved a bucket's default
encryption with a hard-coded "no explicit SSE-C" flag, so the default was
layered onto a request that already carried an SSE-C header triple and
then tripped that request's own mutual-exclusion check. Every bucket with
default encryption refused SSE-C single PUTs with 400 InvalidArgument,
while CreateMultipartUpload on the same bucket succeeded because it
resolves SSE elsewhere. Both call sites now derive the flag from the
request headers, as COPY already did.

The bucket default's KMS key id was also inherited independently of the
effective algorithm, so an explicit AES256 request against an aws:kms
default bucket produced a self-contradictory algorithm/key-id pair and
was rejected. The key id is now inherited only when the effective
algorithm is aws:kms, matching the storage-layer resolver.

Refs backlog#2368 B1, B2.

* fix(sse): refuse SSE-KMS without a running KMS service

A write requesting aws:kms on a node with no KMS service fell back to the
node-local SSE-S3 provider: the data key was wrapped with
RUSTFS_SSE_S3_MASTER_KEY while the object metadata still recorded
aws:kms and the requested KMS key id. The stored object claimed a KMS
protection it never had, under a key that was never consulted, and no
signal distinguished it from a genuine SSE-KMS object.

The managed-encryption path now asks the resolved DEK provider whether it
wraps with a node-local master key and refuses SSE-KMS in that case:
InvalidRequest when KMS was never configured, ServiceUnavailable when a
configured service is not running. The check sits after the per-key
authorization gate so an unauthorized caller still receives AccessDenied
whatever the KMS runtime state is, and asks the provider rather than a
parallel availability signal because the provider is what actually wraps
the key. A missing master key no longer answers an SSE-KMS request with
an SSE-S3-worded configuration error.

The SSE-S3 local fallback is unchanged.

Refs backlog#2368 B4.

* fix(ecstore): restore and archive tiers in stored coordinates

Multipart restore addressed the remote tier in plaintext coordinates
while the copy-back reads the stored representation. Each part received a
misaligned slice of the remote object whose length still satisfied the
range, the hash reader and the completion size check, so the restore
reported success and silently replaced the object's bytes. Encrypted and
compressed multipart objects were both affected. Restore now accumulates
stored part sizes, passes the stored length to the hash reader alongside
the plaintext length, and validates against the stored size.

The copy-back digests stored bytes, so its computed MD5 is not the
object's public ETag. Restore now preserves the object ETag on both the
single-part and multipart paths, and gives each restored part its own
recorded part ETag rather than the object-level value.

Transition also handed the tier the object's SSE headers and its
RustFS-wrapped data key as request headers. Any S3 target rejected an
SSE-C archive outright, an SSE-KMS archive asked the target to encrypt a
second time under a key id it does not own, and the wrapped DEK left the
cluster. The archive request now strips every SSE header and encryption
marker with the predicate the replication path already uses; the local
xl.meta keeps all of it, so read-through and restore are unaffected.

Objects restored by an affected release are not detected or repaired
retroactively and must be re-restored from the tier.

Refs backlog#2368 B3, B5; backlog#2369 P7.1.

* fix(rio): lock the v1 nonce layout within a segment

Decrypting a v1 segment tried three historical nonce layouts per frame,
independently for every frame. The last of them exists for streams
written before 1.0.0-alpha.91, which reused a segment's part nonce for
every block in it; because block zero's derived nonce equals that base
nonce, a frame encrypted at index zero authenticated at any position. An
attacker able to rewrite the underlying shards could replay it and have
the forged plaintext returned with 200 and an unchanged length. Shard
integrity uses a keyed-hash-free checksum, which such an attacker can
recompute, so it is not a barrier.

A segment now locks onto whichever layout decoded its first non-zero-index
frame and rejects any later frame needing a different one. That leaves one
residual shape: a stream built purely from repeats of frame zero has no
later frame to disagree. New RUSTFS_ENCRYPTION_LEGACY_NONCE_FALLBACK
(default true, so pre-alpha.91 objects keep decrypting) drops the third
layout entirely when set to false, which closes it. Turning it off refuses
pre-alpha.91 objects, so migrate them first by rewriting in place.

Refs backlog#2369 P2.

* fix(kms): reload a service that failed to start

POST /rustfs/admin/v3/kms/reload short-circuited whenever the persisted
configuration matched the in-memory one byte for byte. A node whose KMS
failed to start keeps that configuration and sits in Error, so the
documented recovery call returned "reloaded successfully" while leaving
the node down. Peers reached the same path through the reload broadcast,
so a cluster that lost Vault during a rolling restart had no working
recovery route other than the node-local start endpoint. Reload now
short-circuits only for a service that is actually running, and otherwise
reconfigures, which starts a service that is not running.

The AWS backend also advertised key-version enumeration through
kms/status, which its own documentation says it cannot do; the capability
and its golden snapshot now say false.

Refs backlog#2369 P1, P7.3.

* docs: record the SSE and KMS changes for 1.0.0

The Unreleased changelog section carried no entry for any encryption work
merged since 1.0.0-rc.5, including three items with operational impact:
the config-secret variable whose absence persists secrets in cleartext
with only a warning, the v2 frame write switch and its rolling-upgrade
constraint, and per-key authorization making a public bucket incompatible
with SSE-KMS objects. Adds those plus this batch, including the SSE-KMS
refusal as a breaking change with both routes out.

Also corrects four places where documentation contradicted the code: the
cleanup register still called encrypted range seek opt-in after its
default flipped, the Helm README claimed vault_mount_path only applies to
Transit while the template also feeds the KV2 mount, the disaster-recovery
drill listed bundle contents for backends whose export is refused with
501, and the Chinese README capability table predated most of the feature
set. Documents the SSE-S3 local master key as a first-class operational
mode with its rotation dead end, and what the v1 frame layout does and
does not authenticate.

Refs backlog#2369 P5.

* fix(kms): classify data-path KMS failures by what the caller can do

Only "key not found" and a backend outage were classified; every other
KMS failure that reached the S3 data path fell through to
500 InternalError with a generic message. A disabled or pending-deletion
key, a denied KMS grant, an encryption-context mismatch, an unsupported
algorithm, a credential or timeout failure, and a capability the
configured backend does not have all looked identical to a server fault.
SDKs therefore applied exponential backoff to configuration errors no
retry can fix, and monitoring counted every one of them against the
server's own error rate.

Unusable-key and request-side failures now answer 400, a denied grant
403, transient backend failures 503, and a missing backend capability
501. Damaged, unreadable, or unknown-format key material keeps its 500:
it is a server-side integrity fault, and existing tests pin it.

The classifier is deliberately separate from the admin lifecycle
mapping, which answers 404 for a missing key because there a key id is
the resource being addressed; on the data path it arrives inside a
request header or a bucket default. Messages either name what the caller
asked for or stay generic, with deployment-side detail left on the error
source the way the storage-IO mapping already does.

Refs backlog#2368 B6.

* fix(kms): track and renew static Vault tokens

Token authentication hard-coded "this token carries no lease", so the
renewal task never started, the remaining-TTL gauge was never published,
and nothing looked wrong. `vault token create` grants a 768-hour TTL by
default, so a cluster that had been healthy for a month turned every KMS
call into a 403 and could not recover without a restart or a
reconfigure. Production configuration validation only rejects the
literal dev-token, so an ordinary expiring token reaches a whole cluster.

The source now reads `auth/token/lookup-self` at login and adopts what
Vault reports. A token with no expiry behaves exactly as before. An
expiring renewable one is picked up by the existing renewal loop and
renewed at half TTL like every other auth method. An expiring
non-renewable one warns with its remaining lifetime and publishes the
gauge, so the fail-closed window is visible before it arrives.

The probe never fails the login: a policy that omits lookup-self, or a
Vault that is briefly unreachable, warns and falls back to exactly the
previous behaviour rather than taking down a deployment that works
today. The scripted Vault test double answers the lookup out of band so
existing scripts keep describing only the protocol under test.

Refs backlog#2369 P3.

* feat(sse): report SSE-C requests that arrive without TLS

An SSE-C request carries the customer's AES key in a request header, so
AWS S3 and MinIO both refuse one that did not arrive over TLS. RustFS
accepted them on any transport: a plaintext hop hands the key to anyone
on the path, and since the object cannot be read without that same key,
the exposure lasts as long as the object does.

Refusing outright is the correct end state but not a safe default to
adopt inside a release window, because the project's own s3-tests and
e2e lanes and most staging deployments speak plain HTTP. This release
reports instead: each such request increments
rustfs_ssec_plaintext_requests_total and logs one warning per process, so
an operator can confirm nothing would break before the default flips.
RUSTFS_SSE_C_REQUIRE_TLS=true opts into the AWS 400 now.

The verdict is per connection rather than per deployment: the layer is
built with whether this listener terminated TLS, and additionally accepts
an https protocol forwarded by a proxy the trusted-proxy configuration
already vetted. It sits beside the rate limiter, after the layer that
makes a forwarded protocol trustworthy and after the request context, so
a rejection can echo the request id.

Refs backlog#2369 P7.2.

* fix(kms): say what a node-local backend means for a cluster

The Local backend keeps key material on each node's own disk and
generates its Argon2id salt per node, so two nodes derive different keys
from the same master_key and an object encrypted on one node cannot be
decrypted on another. Behind a load balancer that surfaces as
intermittent 500s on reads that succeeded moments earlier, with nothing
tying the symptom to the cause: the only signal was a generic
"development, testing and demos only" positioning warning that says
nothing about what actually breaks.

Configuring or reconfiguring Local while the deployment is distributed
now logs a dedicated event and appends the consequence to the configure
response, so the operator who made the change sees it. The product
decision to warn rather than refuse is unchanged.

Refs backlog#2369 P7.4.

* docs: record the remaining SSE and KMS changes for 1.0.0

Adds changelog entries for the KMS data-path status classification, the
Vault static-token lease probe, the SSE-C plaintext-transport report and
its switch, and the node-local backend warning.

Documents two things the backend security guide never stated: that SSE-C
belongs on a secure transport, with the counter and switch to plan the
change around, and that the Local backend cannot be shared by a
multi-node deployment because each node derives different keys from the
same master key.

Refs backlog#2368 B6; backlog#2369 P3, P5, P7.2, P7.4.

* fix(kms): report an unreadable key store as an outage on the S3 path

A backend now distinguishes a key store it could not read from a key
that is genuinely absent, but the S3 boundary collapsed the first one
back onto 500 InternalError through the fallthrough for integrity
faults. The distinction was therefore invisible to the client: a
temporary key-directory outage looked exactly like a permanently damaged
key record, and neither the status nor the metric said the request was
worth retrying.

An unreadable key store joins the retryable class and answers 503, next
to a backend error and a credential failure. Damaged, unreadable or
unknown-format key material keeps its 500.

Refs backlog#2368 B6; builds on rustfs/rustfs#7470.

* test(storage): cover MRF cleanup delete ENOSPC anchor (#7513)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require heal retry stats evidence (#7514)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add release bundle dry-run fixture (#7515)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): confirm recovery intent accept readback (#7516)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require MRF cleanup delete ENOSPC evidence (#7518)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require bounded retry window evidence (#7517)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): emit scanner heal G09 upgrade evidence (#7519)

Record measured Scanner/Heal G09 evidence artifacts from the upgrade compatibility lanes when a fresh evidence directory is provided.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): persist MRF rollback mirror boundary (#7520)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add G09 upgrade evidence runner (#7522)

* 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 Linux x86_64 runner for the Scanner/Heal G09 mixed-version and rollback upgrade evidence lanes.

The helper reads the pinned previous-release asset metadata from the upgrade workflow, verifies the downloaded binary, builds the current head, runs both ignored E2E tests, and fails unless the expected G09 JSON artifacts exist.

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>

* feat(scanner): implement V2 evidence preflights (#7523)

* feat(scanner): wire dirty usage producer identities

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

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

* test(scanner): add segment activation preflight proof

Keep scanner segment reuse behind a structured activation preflight so release evidence can prove the production gate remains disabled until every producer, generation, overflow, cold-oracle, and distributed invalidation check is satisfied.

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

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

* feat(scanner): expose distributed invalidation evidence

Record an explicit distributed segment invalidation evidence summary when remote dirty usage snapshots are bound to the current activity window and the authenticated scoped ACK capability probe succeeds.

Reject peer dirty usage snapshots that contradict the peer activity pending bit so scoped ACKs fail closed instead of clearing an unadvertised remote mutation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): assemble release evidence bundles (#7526)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

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

* fix(scanner): replay recovery intents while disabled (#7521)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): preflight G09 evidence disk space (#7527)

* 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)

* fix(targets): reject trailing batch items (#7508)

* test(scanner): preflight G09 evidence disk space

Fail the Scanner/Heal G09 upgrade evidence runner before downloading or building when the validation host does not have enough free space for a full raw evidence pass.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): respect cargo target dir in G09 runner (#7528)

* 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)

* fix(targets): reject trailing batch items (#7508)

* fix(scanner): respect cargo target dir in G09 runner

Write the RustFS feature stamp under the resolved Cargo target directory so remote validation hosts with CARGO_TARGET_DIR set can reuse the built PR-head binary.

Extend the runner plan/self-test path to cover relative target-dir resolution.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): add G09 upgrade evidence runner (#7525)

* 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(e2e): add G09 upgrade evidence runner

Add a Linux x86_64 runner that downloads the pinned previous release, builds the current RustFS binary, runs the mixed-version and rollback upgrade compatibility lanes, and verifies the required Scanner/Heal G09 raw evidence artifacts.

Document the runner and add a shell self-test for help, dry-run, SHA validation, and non-empty artifact directory guards.

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>

* test(scanner): emit G09 release bundle gate evidence (#7530)

* 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)

* fix(targets): reject trailing batch items (#7508)

* test(scanner): emit G09 release bundle gate evidence

Write a bundle-ready G09 gate descriptor from the Linux upgrade evidence runner and validate the single G09 gate with the shared release-bundle rules without approving the full Scanner/Heal release.

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: cui fliter <imcusg@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): reject invalid G09 runner test selection (#7531)

Validate the selected G09 evidence lane before process-substitution case expansion so --plan-only cannot turn an unknown --test value into an empty successful plan.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): keep G09 source binary path clean (#7532)

Keep checksum verification output out of the command substitution that resolves the previous-release binary for the G09 runner.

Also tolerate non-GNU sha256sum in local self-tests by falling back to shasum when GNU --check support is unavailable.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF replay proof cleanup (#7534)

Exercise the committed MRF replay checkpoint path with a real ECStore-backed bucket incarnation, retain the replay anchor until an exact verified proof arrives, then delete the proof-discharged checkpoint and prove a subsequent restart does not resurrect the replayed work.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): carry segment activation preflight evidence (#7535)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): surface cold segment reuse oracle (#7536)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): require hard evidence artifact payloads (#7537)

Require Scanner/Heal release bundle JSON artifacts to carry hard-domain evidence fields for mixed-version, crash, capacity, disk-full, replica-loss, and MRF cleanup gates. This prevents a descriptor from approving a hard gate while pointing at a generic measured artifact that lacks the boundary-specific oracle fields.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): bind producer evidence to dirty generations (#7538)

Record scanner segment producer identities with the dirty usage generation they invalidated, then expose a cycle-local producer evidence snapshot for segment reuse activation preflight.

Production activation remains fail-closed until durable producer identity and restart-gap proof are available.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): persist segment invalidation proof metadata (#7539)

* test(scanner): wait for EC8+4 heal control readiness (#7540)

* test(scanner): list full P4 release evidence fields (#7542)

Align the Scanner/Heal release requirement registry with the release bundle gate for P4 so the closure checklist advertises MRF scale, replay cost, retained responsibility, and cleanup/GC soak evidence together.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add W16 release evidence runner (#7543)

Add a W16 Scanner/Heal evidence runner that executes the recovery-intent crash-boundary and quota-authority lanes, emits measured G04/G12 JSON artifacts, and validates the resulting single-gate release descriptors.

Wire its shell self-test into script-tests and document the release evidence entry point.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix: address release acceptance regressions (#7541)

* feat(scanner): admit durable segment proof replay (#7544)

* feat(scanner): admit durable segment proof evidence

Validate complete set snapshot segment invalidation proof metadata against the current dirty usage generation window and scanner process epoch before clearing durable producer and restart-gap activation blockers.

Production segment reuse remains gated by the explicit activation flag.

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

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

* feat(scanner): replay segment proof from root snapshots

Carry segment invalidation proof metadata into root snapshot set states so a complete published baseline can replay the durable producer evidence recorded by each set cache.

Keep the field additive for older readers and leave incomplete or LKG set states unproven.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind MRF release artifacts to evidence kinds (#7545)

Require Scanner/Heal G07/G08/P4 JSON artifacts to declare the exact MRF evidence kind expected for each release-bundle field. This keeps generic measured JSON from satisfying durable replay, ENOSPC, replica-loss, scale, replay-cost, retained-responsibility, or cleanup-GC soak gates.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): enable segment reuse activation gate (#7546)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): run scoped fallback cases on dedicated stack (#7550)

Linux libtest defaults can abort these deep EC-store futures before the assertions run. Wrap the async scoped fallback cases in the repository's dedicated-stack test pattern so the default cargo test path exercises the oracle directly.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): roll out distributed e2e selection (#7549)

Refresh the committed e2e-distributed selection digest after the EC8+4 Scanner/Heal evidence case joined the profile, and document the registry-aware runner entry for the distributed case.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add W13 MRF evidence runner (#7547)

Add a W13 durable MRF evidence runner that emits measured G07, G08, and P4 JSON artifacts and release descriptors through the existing scanner/heal bundle gate.

The runner now executes the ignored MRF replay evidence test with an exact full test path, validates raw artifact kinds and gate decisions, prepares Linux tmpfs-backed ENOSPC roots for G08, and documents the Linux/long-soak boundaries.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): fix segment proof fixtures

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

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

* test(heal): reduce W13 evidence helper arguments

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

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

* ci(test): enable local KMS for s3 compatibility tests (#7553)

Configure the s3-tests harness with a local KMS key so SSE-KMS cases run in CI without relying on an external KMS service.

Also move the anonymous POST default SSE-KMS regression onto the shared local KMS test environment.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind segment proof to typed dirty suffix (#7564)

Track producer identity completeness per pending bucket and retain process coverage across durable acknowledgements. Admit a prior same-process segment proof only for an exact or strictly earlier fully typed generation window.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): validate scoped ACK clear receipts (#7563)

Fail closed when a signed scoped ACK response claims more cleared entries than its request contained, while preserving partial-clear semantics and zero-mutation capability probes.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind cold scoped cache reuse (#7565)

Record an explicit cold bucket reuse proof for scoped set scans and treat fully cold sets as valid cold-zero-walk reuse when the baseline and bucket incarnations are bound.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(s3): preserve delete marker read error headers (#7558)

* chore(release): sync configurable nightly build workflow from main (#7567)

feat(nightly): make the build branch configurable (NIGHTLY_BRANCH var + dispatch input) (#7557)

* docs(obs): correct metric export interval and explain alert timing (#7571)

* fix(heal): persist scoped admin admission handoff (#7569)

* fix(heal): persist root admission handoff

Persist administrator root heal ownership before queue publication so crash restart can replay an admitted control-plane start without relying on process-local replay cache state.

Protect queued durable root owners from priority displacement; callers must use forceStart cancellation semantics to replace them.

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

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

* fix(heal): persist admin heal handoff types

Extend durable admin heal admission handoff beyond cluster roots while preserving legacy root recovery records. Persist the heal type in schema v2, replay typed admin requests after restart, keep non-admin requests out of the durable control-plane handoff, and allow path cancellation to clear durable-only non-root records.

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

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

* fix(heal): preserve disjoint durable admin owners

Limit admin forceStart durable-owner cleanup to overlapping heal requests after the admin recovery journal begins storing non-root work. Add fail-closed and displacement tests so durable bucket owners cannot be silently dropped by unrelated replacement or priority pressure.

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

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

* fix(heal): persist admin terminal receipts

Persist terminal receipts for durable administrator heal starts before retiring their pending intent so restart can answer completed or cancelled scoped tasks without replaying them.

Retain pending work when terminal publication fails and keep timeout owners on checkpoint-only replay semantics.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): mark committed replication and tier mutations (#7568)

* fix(scanner): mark committed replication and tier mutations

Bridge ECStore committed replication status writebacks and lifecycle tier expiration cleanup into scanner dirty-usage producer identity tracking. Only successful replication metadata updates mark Replication; validate-only, superseded, and retry paths remain unmarked.

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

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

* feat(scanner): replay durable dirty producer records

Add a strict scanner dirty-usage producer replay schema that can restore typed producer coverage after restart without treating ordinary process-local mutations as durable evidence. Replay records validate schema, cache key format, generations, scopes, producer identities, duplicate buckets, byte limits, and entry limits before modifying dirty state.

Keep segment activation fail-closed unless the restored bucket state came from durable replay; subsequent local mutations clear the durable authority bit for that bucket. Also fix the release scanner clippy lint in scoped cold-reuse proof creation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): recover expansion buckets and resume retirement (#7574)

* fix(ecstore): recover partially created expansion bucket volumes

* fix(ecstore): resume retirement after startup safety checks

* fix(ci): align S3 KMS fixtures and default key expectations (#7573)

* fix(heal): prune retained root terminal receipts (#7576)

Bound durable root-heal terminal receipt retention to the completed-status TTL while keeping expired receipts authoritative enough to retire stale pending intents before deleting the receipt itself.

Add a budgeted background GC pass and fail closed on corrupt or non-terminal receipt records.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(storage): preserve retryable publication guard errors (#7561)

* fix(storage): preserve retryable publication guard errors

* fix(scanner): satisfy scoped cache clippy lint

Use bool::then_some for the scoped cold bucket reuse proof construction after merging the latest release branch.

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(health): report storage quorum in node readiness (#7566)

* fix: preserve scanner backlog during pool retirement (#7555)

* fix: hand off native scanner backlog before pool retirement

* test: initialize optional proof in data usage fixtures

* perf: bound parallel V3 pool metadata phase writes

* test: keep native backlog fault errors typed

* test: skip unchanged pool metadata in crash barriers

---------

Co-authored-by: cxymds <cxymds@gmail.com>

* test(scanner): bind profile artifact provenance (#7577)

* fix(scanner): persist dirty producer journal (#7580)

Persist scanner dirty producer replay records from committed replication and tier mutations, hydrate journal state on restart, and clear durable records only after acknowledged dirty generations.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: add scanner heal scheduler pressure evidence (#7584)

Add measured G10/P1/P3 scheduler-pressure release descriptor generation, ABBA scheduler/profile provenance, and focused self-tests for the release bundle gate.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: add scanner heal G14 multiset evidence (#7583)

Add EC8+4 multi-set and multi-pool scanner/heal release evidence coverage, including e2e registry cases, oracle checks, and a G14 descriptor assembler for measured case artifacts.

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(nightly): push assets with plain git on release — the build fleet has no gh CLI (#7586)

fix(nightly): push assets with plain git — the build fleet has no gh CLI

The sm-standard-4 runners used by the nightly build have no gh binary
(functional-chain workflows run elsewhere, on the jumpbox). The assets
publish step died on 'gh: command not found' at the credential-helper
setup, and the preceding clone failure had been masked by 2>/dev/null,
misleading the step into the orphan path. Swap clone and remote setup
to plain git with the token embedded in the URL; push semantics are
unchanged.

* fix(scanner): stabilize retired backlog survivor commits (#7585)

Treat an authoritative scanner pause-backlog commit as stable when every member in that commit stores the matching stable and committed record. Retired source replicas can remain missing in the observed topology while survivor membership is already stable.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(metrics): prevent duplicate drive reporting across nodes (#7582)

Co-authored-by: houseme <housemecn@gmail.com>

* fix(ecstore): avoid Windows batch-delete cleanup sharing conflicts (#7579)

Co-authored-by: houseme <housemecn@gmail.com>

* feat(ilm): inspect legacy transition version state (#7581)

* test(scanner): add release evidence ingestion gates (#7588)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix: reuse admin error boundary for legacy tier reconciliation (#7587)

* fix(test): stabilize release CI heal fixtures (#7589)

Keep heal admission-only tests off durable root recovery so mock storage does not require local metadata disks. Align forceStart retry assertions with replacement semantics and fix the e2e metric clippy collapse.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).

* test(scanner): add release bundle evidence descriptors (#7595)

* test(scanner): merge release evidence descriptors

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

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

* test(scanner): add authority and legacy release descriptors

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

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

* test(scanner): add scoped ack release descriptor

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

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

* test(scanner): require all release evidence fields

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

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

* test(scanner): add mrf release descriptor producer

Add a standalone W13 MRF descriptor producer for measured G07/G08/P4 raw artifacts, with fail-closed self-tests for missing responsibility cases, disk-full ENOSPC observations, synthetic artifacts, and short P4 cleanup soaks.

Also bind G12 quota path evidence to explicit case lists so W16 descriptors cannot pass without reset and settlement coverage.

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

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

* test(scanner): reuse mrf release descriptor producer

Route the W13 runner through the shared MRF descriptor producer so measured runs and offline re-packaging use the same fail-closed validation.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(ilm): backfill proven legacy transition state (#7591)

* feat(ilm): backfill proven legacy transition state

* fix(ilm): remove redundant reconcile test clone

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(ecstore): remove redundant transition clone (#7597)

Drop an unnecessary clone in the legacy transition state digest regression so release CI clippy lanes can run with warnings denied.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): seed current backlog members before committing (#7594)

* fix(scanner): seed current backlog members before committing

* test(app): run recursive force delete test on large stack

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind raw profile artifacts (#7598)

Require Scanner/Heal release profile wrappers to carry bundled raw profile artifacts plus the matching profile cost metrics.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): surface first failed object in status (#7599)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover retry budget across attempts (#7600)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden durable producer evidence gate (#7601)

Require Scanner/Heal G11 maintenance evidence to prove durable dirty producer journal replay, replay-state hydration, generation-bound ACK clearing, and invalid journal fail-closed cases before segment reuse can be activated.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): harden scoped ack evidence proof (#7602)

Require Scanner/Heal G03 scoped ACK evidence fields to carry their own measured provenance and concrete ACK, capability, and mixed-peer observations before release-bundle gate verification.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind status outcome raw provenance (#7603)

Require measured G05/G06/R-D raw status, compatibility, and disposition artifacts to share run identity before producing release bundle descriptors.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): retry fully recoverable batch failures (#7604)

Allow recursive bucket and cluster heal attempts whose retained batch failure is entirely retryable to use the existing bounded manager retry budget. Permanent or mixed batch failures remain terminal so heal outcome semantics stay strict.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): avoid movement debt for stale remote lease (#7605)

Classify a final remote scanner publication lease validation failure from a fresh activity snapshot instead of treating every validation error as data movement. If the peer session and movement generation still match the granted leases and publication is otherwise allowed, keep the publication rejected as an activity-baseline miss without creating pause backlog movement debt. Preserve DataMovement for expired leases, peer restarts, movement generation changes, and active publication blocks.

Tests cover namespace-only validation invalidation and the remote fence-loss cases that must still defer as DataMovement.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* chore(deps): refresh workspace dependencies (#7606)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bind G14 case artifacts (#7611)

Require Scanner/Heal G14 release bundle JSON wrappers to mirror their outer evidence and carry self-contained case artifact provenance.

Copy proof-json case artifacts into the generated G14 descriptor bundle so assembled release bundles can validate case file hashes after relocation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): retry contended decommission cancel target locks (#7612)

* test: align transition fixtures and KMS context expectations (#7610)

* test(scanner): stabilize W13 release gate evidence (#7613)

* feat(nightly): publish packages as assets of the rolling 'nightly' release (sync from release) (#7593)

feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).

* test(scanner): stabilize W13 release gate evidence

Treat only real raw-entry windows as replayed scanner enumeration in the restart diagnostic, so final completion rounds without raw entries are not fail-closed as raw replays.

Allow the EC8:4 multi-pool heal evidence case to select an outage object key that routes to an online pool while an entire target pool is down, preserving strict behavior for single-pool cases.

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

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

---------

Co-authored-by: hector <42570491+majinghe@users.noreply.github.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bound native backlog fixture resources (#7609)

* fix(scanner): bound native backlog fixture resources

Keep native scanner backlog restart fixtures within low file descriptor limits by reducing the native-only disk layout, making fixture shutdown drain background work, and avoiding long-lived ECStore retention from background loops.

Convert ECStore-backed background refresh/recovery/monitor tasks to upgrade weak owners only while doing work so completed test stores release their disk graph before the next native fixture starts.

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

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

* test(tier): refresh release-merged failure fixtures

Reuse the raw legacy-transition fixture helper so delete-all expiry actually exercises Unknown transitioned history metadata after the release merge.

Rewrite restore-failure disk fixtures through per-disk xl.meta snapshots so stale destination identities can be persisted without tripping ordinary metadata update guards.

Update the SSE KMS mismatch expectation to the InvalidRequest/context_mismatch behavior now returned by the rio-v2 path.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ci): align release E2E gate and repair regression tests (#7607)

* fix(scanner): harden heal release gates (#7614)

* test(scanner): reject synthetic heal evidence inputs (#7617)

Harden the Scanner/Heal checkpoint restart and G14 EC evidence producers so release descriptors cannot be assembled from marked fixture, dry-run, or synthetic case inputs.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): collect status outcome raw evidence (#7618)

Normalize live Scanner/Heal status/outcome observations into the measured raw artifacts consumed by the G05/G06/R-D release descriptor producer.

Reject synthetic observations, incomplete required cases, and reused run/window identities before raw artifacts can enter the release bundle flow.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): bind scoped ack confirmation to generation (#7619)

Require lost scoped dirty-usage ACK reconciliation to observe a clean peer activity generation that covers the requested ACK generation. Apply the same guard in the scanner aggregation path so a stale clean activity snapshot cannot discharge pending maintenance after an uncertain ACK response.

Refs rustfs/backlog#2427

Refs rustfs/backlog#2281

Refs rustfs/backlog#2240

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test: report tier reference proof setup failures without hanging (#7608)

* test(scanner): plan Linux evidence validation (#7620)

Add a Scanner/Heal Linux release-evidence planner that emits a machine-readable execution manifest for the remaining measured validation lanes.

The planner can run only lightweight preflight checks and keeps plan-only output distinct from measured release evidence.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): select G14 replacement drive by evidence (#7622)

Choose the replacement disk from the target node by the presence of complete pool metadata instead of assuming the first configured drive is the scanner metadata holder. This keeps the G14 multi-set harness aligned with multi-drive EC layouts and adds clearer diagnostics when multi-pool outage writes fail closed.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): report Linux evidence plan status (#7624)

Add a status mode for the Scanner/Heal Linux evidence planner so operators can identify missing or incomplete artifacts before final bundle assembly.

The status output stays plan-only and never reports release approval.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add G14 multi-pool background heal evidence (#7628)

test(scanner): align G14 multi-pool outage evidence

Treat the localhost multi-pool topology as whole-pool loss when the target node is down. If that topology cannot admit the outage object while the pool is offline, defer that object write until the pool rejoins and record the oracle marker instead of failing before the real crash/restart evidence runs.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): verify deferred multipool outage evidence (#7629)

Validate the G14 multi-pool oracle field that records down-window outage PUT refusal and requires the deferred post-rejoin outage object to be accepted and checked through S3.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): allow metadata snapshots at read quorum (#7627)

fix(ecstore): admit metadata snapshots at read quorum

Allow guarded bucket metadata snapshot existence checks to use read quorum so degraded erasure sets can continue Object Lock snapshot reads without weakening bucket mutation or object write quorum.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* chore(deps): refresh release dependencies (#7632)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): use pool lock quorum for set writes (#7630)

Use the pool-wide namespace lock client domain for every set in a pool so degraded EC8+4 multi-set writes are gated by node-level lock quorum instead of the narrower per-set endpoint host slice.

Keep namespace-lock domain deduplication tied to both the pool namespace and shared clients, and add regression coverage for three-locker degraded writes plus cross-pool domain separation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): keep G14 multi-set restart graceful (#7633)

test(e2e): keep multi-set restart graceful

Include the EC8+4 multi-set restart scenario in the graceful interruption lane so the unclean-shutdown marker assertion matches the scenario semantics.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(e2e): record deferred G14 outage writes (#7637)

Include optional outage-write diagnostics in G14 evidence oracles so deferred multi-pool outage writes bind their down-window refusals and post-rejoin acceptance.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): bound G14 multi-pool evidence size (#7640)

Keep the multi-pool evidence runner within the registry object budget while retaining deferred outage-write diagnostics for release-gate validation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: cui fliter <imcusg@gmail.com>
Co-authored-by: hector <42570491+majinghe@users.noreply.github.com>
2026-09-10 18:24:20 +08:00
Zhengchao An 5c17012fe2 feat(console): support a configurable console URL prefix (#7634)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-10 17:48:12 +08:00
唐小鸭 aeaba86d73 fix(replication): close the pre-stable convergence gaps from backlog#2367 (#7626)
* fix(replication): total-order rule sort and honor V1 top-level Prefix

Rule matching had two defects from the pre-GA replication audit
(rustfs/backlog#2367 C-1 and C-2):

- The actionable-rule sort compared same-destination rules by priority but
  answered Equal for any other pair, which is not a total order; the
  standard library sort panics on such comparators once a slice exceeds the
  insertion-sort threshold, so an object matching more than 20 enabled rules
  across two or more targets could panic the PUT or DELETE task. Rules now
  sort by priority descending with destination and id as tie-breakers, and
  filter_target_arns preserves that order instead of draining a HashSet.

- A V1 rule written without a <Filter> carries its prefix at the top level;
  that field was never read, so <Prefix>logs/</Prefix> matched every object.
  ReplicationRuleExt::prefix now falls back to it, with a <Filter> keeping
  precedence. The existing prefix fixtures were built this way and had been
  asserting nothing.

* fix(admin): advertise data-usage and listen capabilities to rc

The rc client gated `rc du` and `rc watch` on a pinned contract that
matched server versions by the string prefix `1.0.0-rc.`; a server that
reports `1.0.0` no longer matches, and the dynamic `advertised` list did
not carry either name, so `rc du` against a GA server fails with an
unsupported-capability error (rustfs/backlog#2367 E-2).

Advertise `admin.data-usage` from the admin route inventory like the IAM
entries, and `listen_notification` for the bucket `?events=` extension
route the admin router dispatches. The client merges advertised entries
ahead of its pinned contract, so no version sniffing is needed.

* fix(site-replication): stop notifying the local site on remove and rotate

The pending-remove and pending-rotation notification loops skipped the
local site by endpoint only, while finalization identifies it by
deployment id or endpoint. The reconcile tick resolves the local peer from
the node's own listen address (and a handler from the request Host), so
`remove --all` dialed the site's registered endpoint, waited out the
request timeout against the lifecycle lock it was holding, and answered
`Partial: failed to notify 1 peer(s)` for a removal that had succeeded
(rustfs/backlog#2367 A-4, backlog#2195 item 3).

Both loops now iterate the peers still awaiting notification through one
helper that applies the finalization identity.

* fix(site-replication): promote and settle IAM retries without a tick of slack

Two retry-queue behaviours kept an IAM change from converging for ten to
twenty minutes after a peer came back (rustfs/backlog#2367 A-1 and A-3,
backlog#2305):

- The lightweight 30-second pass filtered its reachability probe to bucket
  ops, so a backed-off IAM or bucket-metadata snapshot waited for the
  600-second tick to notice the peer. It now probes every backed-off class
  and still replays only bounded bucket ops; promotion is a state flip the
  heavyweight tick acts on.

- Backoffs are multiples of the tick interval, so a failure stamped δ
  seconds after a tick was 600 − δ old at the next tick and slipped a whole
  extra interval. The heavyweight drain now evaluates backoff halfway to its
  next tick.

- An IAM entry first created by a non-deletion failure (the add bootstrap's
  snapshot send, the drain's own replay, an import-iam schedule) was never
  stamped `deletions_recorded`, so a later recorded deletion could not
  settle it and it escalated to the marker only `replicate repair` clears.
  Entries created by this binary now start recorded; a row persisted by an
  older binary keeps the escalation semantics.

* fix(site-replication): reload peer node caches after bucket wiring writes

Every S3 bucket-config write ends by asking the other nodes of the cluster
to reload the bucket's metadata; the site-replication writers never did.
On a multi-node site the node that ran the pairing (or applied a peer's
bucket-meta item) rewrote the bucket targets and the derived replication
rules on disk, while every other node kept serving its cached copy for up
to the 15-minute refresh. A `resync start` routed to such a node reported
every freshly wired bucket as `Config not found` and a bucket whose
operator target the pairing had replaced as `recorded remote target no
longer exists` (rustfs/backlog#2367 A-5, backlog#2195 item 2; functional
SITE-105).

Add one best-effort reload helper in the site-replication hooks and call it
after the bucket setup, versioning, peer bucket-meta apply, removed-peer
cleanup, make-with-versioning, and endpoint-refresh writes; the ensure
helpers now report whether they wrote so unchanged passes stay silent. The
resync manifest and start now read the persisted wiring instead of the
node-local cache, matching the target read the start path already did.

The new four-node e2e pairs two clusters and starts a resync through a
non-coordinator node right after pairing; it also covers an IAM user
created on a non-coordinator node converging to the peer site.

* test(e2e): cover delete-marker replication from a multi-node source

The functional suite reported delete markers created on a 3-node source
never reaching the target (rustfs/backlog#2195 item 4, REP-105). The report
was a probe defect, but the shape had no coverage: the existing
delete-marker e2e runs a single-node source. Pin it against a four-node
source replicating to a four-node peer and to a single-node target, with
the write and the delete issued through different nodes.

* ci(e2e): refresh the distributed selection for the new replication cases

Four distributed cases were added (two site-replication, two delete-marker
replication). The linux digest is derived from the last CI listing of the
lane (34 cases, matching the previous pin) plus the four new names; the
darwin digest is the local listing, which selects the same 38 cases.
2026-09-10 16:43:13 +08:00
Henry Guo 8f150d1d8e fix(ecstore): stop pruning at nonempty directories (#7616)
* fix(ecstore): stop pruning at nonempty directories

* test(ecstore): release pruning fixtures before temp cleanup
2026-09-10 14:31:01 +08:00
hector da762e0b02 feat(nightly): publish packages as assets of the rolling 'nightly' release (sync from release) (#7593)
feat(nightly): publish packages as assets of the rolling 'nightly' release (#7592)

Replace the assets-branch scheme with a proper GitHub Release on
rustfs/auto-testing: a single 'nightly' release whose deb/rpm assets
are replaced in place on every build. This is the standard channel —
visible on the repo's Releases page, stable download URLs, no git
history growth (release assets live outside the repository).

- New scripts/release/publish_nightly_assets.sh: resolves-or-creates
  the 'nightly' release via the REST API, deletes same-name assets,
  uploads rustfs-nightly-latest.{deb,rpm}, then PATCHes the release
  body with the build provenance (ref@sha, run link, sizes, SHA256).
  Plain curl + python3, no gh CLI (the build fleet has none — #7586).
- The workflow step shrinks to invoking the script; full flow
  exercised end-to-end against the real release with probe files
  (create / upload / overwrite / download round-trip / body update).
2026-09-09 21:47:50 +08:00
唐小鸭 27d66c159f fix(kms): prevent transient health failures from latching status (#7578)
Keep backend health checks from overwriting the running service lifecycle state so subsequent admin checks and probe-based readiness can recover without a restart.

Add a regression test that fails on the original implementation after backend recovery and verifies the service instance and version remain unchanged.

Validation: 39 focused resilience, lifecycle, concurrency, and service manager tests passed; one existing live AWS test remained ignored. cargo fmt --all --check and git diff --check passed.

Thanks to @stevapple for reporting the issue and providing a detailed diagnosis and reproduction.

Fixes #7554
2026-09-09 11:05:26 +00:00
hector e546ae9c62 fix(nightly): push assets with plain git — the build fleet has no gh CLI (#7572)
The sm-standard-4 runners used by the nightly build have no gh binary
(functional-chain workflows run elsewhere, on the jumpbox). The assets
publish step died on 'gh: command not found' at the credential-helper
setup, and the preceding clone failure had been masked by 2>/dev/null,
misleading the step into the orphan path. Swap clone and remote setup
to plain git with the token embedded in the URL; push semantics are
unchanged.
2026-09-09 16:39:59 +08:00
JaySon 35b5cfcf8d docs: fix invalid docker-buildx.sh usage example in README and README_ZH (#7570)
docs: fix invalid docker-buildx.sh usage in README and README_ZH

Replace the docker-buildx.sh --build-arg RELEASE=latest example, which
the script never accepted as a CLI flag (--build-arg is only used
internally for docker buildx build), with the supported invocations:

- bare ./docker-buildx.sh for the default local build
- ./docker-buildx.sh -p linux/amd64 for a single-platform local build

Also update the surrounding comments to reflect single-platform local
builds and add the multi-arch example comment accordingly.
2026-09-09 16:39:25 +08:00
cxymds d7c2fc7587 fix(ecstore): apply read quorum to bucket validation (#7556)
* fix(ecstore): apply read quorum to bucket validation

* fix(e2e): remove needless borrow in quorum test
2026-09-09 14:50:28 +08:00
Zhengchao An 7db206466b test(ci): reserve runner capacity for bounded rollback probe (#7560) 2026-09-09 14:24:57 +08:00
hector e549252ac6 feat(nightly): make the build branch configurable (NIGHTLY_BRANCH var + dispatch input) (#7557) 2026-09-09 14:24:44 +08:00
Zhengchao An 17807b05fb fix(ecstore): preserve internode error context when cloning (#7548) 2026-09-09 14:24:23 +08:00
cxymds 789f1832a4 fix(rebalance): align activation locks and preserve retryable causes (#7551) 2026-09-09 03:25:05 +00:00
Zhengchao An a722fa80d5 fix(ci): require every selected validation lane before merge (#7529) 2026-09-09 05:16:48 +08:00
Zhengchao An 9ecb500cbf test(odm): wait for pull counters before status assertions (#7533) 2026-09-08 17:35:43 +00:00
cui fliter 8462b3492b fix(targets): reject trailing batch items (#7508) 2026-09-08 15:32:24 +00:00
Zhengchao An ac44f8968e fix(ci): bind performance runs to selected inputs (#7512) 2026-09-08 23:16:54 +08:00
唐小鸭 46907c05cf 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.
2026-09-08 14:58:41 +00:00
cxymds 73957d0faf fix(ecstore): preserve online writes during pool retirement (#7472)
* fix(ecstore): reconcile identical scanner backlog replicas

* fix(ecstore): type invalid decommission requests

* chore(ecstore): tighten typed-error ratchet baseline

* fix(ecstore): fence late writes to retiring pools

* fix(ecstore): share healthy pool capacity during decommission

* fix: allow active multipart uploads to drain

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-08 12:30:45 +00:00
Zhengchao An 7e0c67111b fix: continue manual transition past in-flight objects (#7476) 2026-09-08 17:35:36 +08:00
Zhengchao An 0008cbcb29 fix(kms): distinguish key directory outages from missing keys (#7470)
* test(kms): cover directory outages and missing keys on main

(cherry picked from commit 8fc1f0c41df4111a8e055bc12e559cb36d2fc41e)

* fix(kms): preserve directory availability errors on main

(cherry picked from commit c2a8e476f77e89f4b0a708c5cc9f0f8c7a0b14da)
2026-09-08 17:34:55 +08:00
Zhengchao An 0db77be5c6 fix: attest multi-pool bootstrap per creator and contain lock RPC storms (#7473)
* fix(ecstore): attest fresh multi-pool bootstrap per pool creator

A fresh deployment whose pools have their first endpoint on different nodes could never publish its initial pool.bin: each node held fresh-bootstrap proof only for the pool it formatted, combine_across_pools collapsed the deployment-wide proof to None, the elected writer never wrote a pending identity, and startup died with "no durable bootstrap identity or pool.bin replica is available" once the init retry budget ran out.

Track first-hand bootstrap authority per pool. The first pool's creator mints the pending cluster identity on the pool it created, every other creator copies that nonce-bound identity onto the pool it formatted first-hand (a scoped identity write that only ever touches pools the process holds first-hand proof for), and the elected writer publishes pool.bin once it holds first-hand proof for pool 0 and every pool replica carries the same pending identity. Fresh + None is still never promoted, corrupt or disagreeing replicas still fail closed, an initialized deployment never reopens bootstrap for an expansion pool, and an elected restart without first-hand proof still cannot reuse a complete pending set.

Startup classification no longer latches the pool-metadata write gate for the two transient outcomes a healthy bootstrap passes through (a non-elected node waiting for the elected writer, the elected writer waiting for the other creators); recover_pool_meta_transaction never clears write_blocked, so a non-elected node that started before pool.bin existed stayed write-blocked for the life of the process. Genuine recovery-required states still latch.

Refs rustfs/backlog#2375, rustfs/backlog#2338

* fix(lock): contain remote lock RPC timeout storms

A lock RPC deadline evicted the shared internode HTTP/2 channel and re-dialed it unconditionally, so one slow lock endpoint produced a cluster-wide RST_STREAM / GOAWAY too_many_resets / reconnect loop (rustfs#7363).

The remote lock client now keeps a per-peer channel history: a timeout evicts only when the peer has completed no lock RPC for two deadlines, evictions and transport-failure re-dials are rate limited per peer (RUSTFS_OBJECT_LOCK_RPC_EVICTION_COOLDOWN_MS, default 5 s), and a timed-out request is detached instead of cancelled, bounded per peer by RUSTFS_OBJECT_LOCK_RPC_DETACHED_LIMIT (default 256) and by the internode RPC timeout; a lock granted after its caller gave up is released immediately. Unlocks that fail the quick retries continue on a deferred 1/2/4/8/16 s schedule before the server lease reclaims them. New rustfs_remote_lock_* metrics cover timeouts, evictions, suppressed evictions, detached streams, late completions and late releases per peer; docs/operations/lock-rpc-storm-protection.md documents the policy.

Refs rustfs/backlog#2375, rustfs#7363

* ci: refresh nightly test selection digests

The replication nightly membership guard expected the 68-test digest from #7422 while the current listing has 71 tests (additions only: test_bucket_replication_sse_c_compressed_passthrough from #7366, matrix_mint_own_version_ids_addresses_mutations_through_the_ledger and matrix_removed_replication_config_abandons_pending_purge from #7368), and the cluster fault lane expected 50 tests while #7374 added test_cluster_root_heal_recovers_remote_shards_after_background_target_crash. Both lanes have failed before running a single test since 2026-09-07. Bind the Linux digests to the listings from scheduled run 34187469350 and the Darwin e2e-nightly digest to the matching local listing.

Refs rustfs/backlog#2375
2026-09-08 17:05:14 +08:00
Zhengchao An 72e85210f1 test(ilm): isolate manual transition backpressure (#7463) 2026-09-08 13:55:24 +08:00
Zhengchao An 0a9f0f59a7 fix(ecstore): preserve erasure writer error identity (#7455)
* test(ecstore): pin erasure writer error identity

* fix(ecstore): preserve reduced erasure writer errors

* test(ecstore): size error fixtures for their shard payloads

* chore(ecstore): tighten formatted error baseline
2026-09-08 04:39:50 +00:00
Zhengchao An e65788f2e8 fix(replication): keep multipart tests behind storage boundary (#7450) 2026-09-08 02:57:55 +00:00
Zhengchao An 3adfe65193 fix(replication): preserve compressed multipart payload integrity (#7440)
Read compressed multipart replicas from one decoded stream and verify complete coverage before publication.
2026-09-08 07:47:04 +08:00
Zhengchao An 66b2a0f907 fix(ecstore): bind target mutations to the listener instance (#7437)
* fix(ecstore): bind target RPC mutations to their startup instance

* test(ecstore): cover user source ownership during target rename

* fix(ecstore): count user sources in target namespace ownership

* fix(ci): use test-domain facade in delete-marker regression

* test(e2e): refresh observed release membership on both platforms

* fix(log-analyzer): track storage probe failures

(cherry picked from commit f5b6cbd5d3)

* test(e2e): drain PUT tail before checking inline disk layout

* fix(ecstore): bind scanner leases to namespace generations (#7438)

* test(ecstore): retain stale-lease fixture roots until cleanup

* fix(ecstore): bind scanner leases with drain-safe fixtures
2026-09-08 07:36:49 +08:00
houseme 585b5e1c52 test(scanner): measure heal pacing and cache cost (#7433)
* test(scanner): measure heal pacing and cache cost

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

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

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:22:10 +08:00
houseme bc468a4986 test(scanner): report heal release gate status (#7432)
* test(scanner): report heal release gate status

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

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

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:22:04 +08:00
houseme 6610ee58dc fix(scanner): retain raw enumeration quantum (#7431)
* fix(scanner): retain raw enumeration quantum

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

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

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:57 +08:00
houseme c1d9a075de test(heal): cover admin lock timeout progress (#7427)
* test(heal): cover admin lock timeout progress

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

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

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:51 +08:00
houseme 807f85cbd7 test: summarize scanner heal perf artifacts (#7426)
* test: summarize scanner heal perf artifacts

Add a quiet Scanner/Heal performance artifact summarizer that normalizes ABBA report verdicts, key regression metrics, cache-cost profile records, and provenance hashes for CI or PR handoff.

Document the summary command in the scanner benchmark runbook and cover measured, synthetic, pending, and invalid cache-cost paths with focused Python tests.

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

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

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:45 +08:00
houseme f46e230340 feat(heal): publish verified MRF repair events (#7424)
* feat(heal): publish verified MRF repair events

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

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

* feat(common): add durable MRF proof matching (#7429)

Add a fail-closed MRF repair proof adapter that only consumes anchors when the durable anchor and verified proof share the same full identity, ingress lease, and bucket incarnation.

Legacy replay intents without leases cannot become dischargeable anchors, so the current retained journal behavior remains unchanged until a durable writer and producer proof source are connected.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(heal): rearm MRF replay leases before admission (#7435)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(error): merge equivalent api message branches

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-08 07:21:39 +08:00
唐小鸭 d7e88529c9 fix(kms): report a missing KMS key as 400 KMS.NotFoundException (#7423)
* fix(kms): report a missing KMS key as 400 KMS.NotFoundException

A PutObject whose resolved SSE-KMS key (request header or bucket default
rule) does not exist in the KMS answered 500 InternalError with a generic
message: KmsError::KeyNotFound fell through to the default arm of the
StorageError-to-ApiError mapping. S3 reports this client mistake as 400
KMS.NotFoundException; the mapping now does the same and names the key.
s3s has no status for a custom code, so the ApiError-to-S3Error conversion
supplies it.

The legacy create-key aliases behind /minio/admin/v3/kms/key/create ignored
the key-id query parameter that mc sends, creating a key under a generated
id instead of the requested name. The alias now honors key-id (and its
keyId/key spellings) alongside the name tag, and refuses a request whose
two sources disagree.

Refs: rustfs/backlog#2330 (KMS-312, KMS-110)

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:32 +08:00
houseme 7dc15b8ca6 feat(heal): expose admission observability (#7418)
* feat(heal): expose admission observability

Track heal admission outcomes and bounded lock-phase latency through the existing operations snapshot so distributed E2E gates can assert duplicate, forceStart, and displacement behavior without relying on logs.

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

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

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:26 +08:00
cxymds 6b05fb6b42 feat(observability): expose pool write-block diagnostics (#7417)
* feat(observability): expose pool write-block diagnostics

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

* fix(heal): cleanup consumed MRF replay journals

Do not retain Accepted or Merged replay intents as startup anchors after they have been handed to the heal manager. Only refused or still-pending replay records keep the journal on disk until a successor snapshot can persist them.

This keeps successor snapshots limited to the pending queue, which lets successful replay remove both authoritative and legacy journal paths and restores the crash-boundary tests around successor flush.

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

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
(cherry picked from commit d5b8f49c9d)

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-08 07:21:19 +08:00
houseme 5da56288be fix(scanner): preserve recovery intents while disabled (#7428)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 23:19:10 +00:00
houseme 9ca9b3481b fix(scanner): gate segment reuse activation (#7430)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:59:52 +00:00
Zhengchao An 8641043b71 ci: refresh replication nightly selection (#7422)
* ci: refresh replication nightly selection

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:59:27 +00:00
Zhengchao An d6bb61d420 fix(s3): reject unsigned x-amz headers on presigned requests (#7425)
* fix(s3): reject unsigned x-amz headers on presigned requests

A SigV4 presigned URL only binds the headers listed in X-Amz-SignedHeaders, but the handlers applied every x-amz-* request header regardless. The holder of a presigned PutObject URL signed with SignedHeaders=host could add x-amz-tagging, x-amz-storage-class, x-amz-website-redirect-location, ACL, metadata, Object Lock or SSE headers and have them applied (GHSA-g8w9-qw9q-fghr). Reject such requests at the S3 access boundary with 403 AccessDenied and the AWS message "There were headers present in the request which were not signed"; x-amz-cf-id stays tolerated for CloudFront. SigV2 and header-signed SigV4 requests are unchanged.

Regression tests are named after the advisory (unit tests in rustfs/src/auth.rs, e2e in crates/e2e_test/src/presigned_negative_test.rs with a signed-tagging positive control); the security smoke floor rises to 20 and the e2e selection digests are refreshed for the two new cases.

* fix(s3): apply presigned signed-header rule to custom routes and harden parsing

Move the GHSA-g8w9-qw9q-fghr check to the first statement of S3Access::check, apply it in S3Router::check_access so admin, console, STS and extension routes that never reach the access hook enforce the same rule, read X-Amz-SignedHeaders with the exact key the verifier uses and treat a duplicate as signing nothing, and log the rejection as a warn event with the repository field shape. Add presigned GET, unsigned x-amz-copy-source and unsigned Content-Type e2e cases plus a router unit test; raise the security smoke floor to 26 and refresh the selection digests.

* docs(testing): list the full GHSA-g8w9 regression set

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:46:02 +00:00
houseme 4d1807c17f refactor: share object version limit constants (#7420)
* refactor: share object version limit constants

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

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

* fix(error): merge equivalent api message branches

Combine the MaxVersionsExceeded and internal IO message branches so Clippy no longer flags identical if blocks while preserving the existing response messages.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:29:37 +00:00
Zhengchao An 10967d0815 fix(log-analyzer): track storage probe failures (#7434)
* fix(log-analyzer): track storage probe failures

* fix(error): merge equivalent api message branches

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 20:32:06 +00:00
Zhengchao An 7c85c72fd1 docs: scope agent guidance and consolidate review workflows (#7419) 2026-09-07 23:58:05 +08:00
Zhengchao An 27d167f7b6 ci(upgrade): run rc.5 multipart layout checks (#7421)
Wire the existing rc.5 multipart upgrade and diagnostic baseline tests into the upgrade matrix.
2026-09-07 23:58:02 +08:00
houseme 1a5e2b6256 test(scanner): bind segment proof generations (#7416)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 23:43:42 +08:00
houseme 474fcf78fb fix: align object version limit handling (#7415)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 23:43:19 +08:00
houseme 752d4a81ab test(scanner): prove restart quantum stages (#7414)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 23:22:02 +08:00
houseme 0686277ee4 fix(heal): retain MRF replay anchors until successor proof (#7413)
Keep replayed MRF records crash-replayable after manager admission until a later durable successor proof can tombstone them. Queue-full and transient replay submission failures now also preserve the old journal anchor instead of allowing cleanup to erase the only recovery source.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 23:21:47 +08:00
cxymds 7373a5902e fix(ci): use test-domain facade in delete-marker regression (#7412) 2026-09-07 23:21:30 +08:00
cxymds e95a0ed6d9 fix(s3): allow delete-marker metadata during delete preflight (#7411) 2026-09-07 22:31:44 +08:00
houseme f0e0f5307d feat(ecstore): expose pool meta write gate status (#7399)
* feat(ecstore): expose pool meta write gate status

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

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

* fix(server): keep cluster snapshot on storage facade

Map the pool metadata write gate status inside the cluster snapshot collector without naming rustfs_ecstore from the outer runtime module. This keeps the snapshot behavior unchanged while satisfying the architecture migration boundary.

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

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

* test(scanner): align scoped maintenance expectation

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

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

* chore: update error other format baseline

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:31:25 +08:00
cxymds 6855640192 fix(rebalance): preserve explicit stop intent and real failures (#7410) 2026-09-07 22:30:51 +08:00
cxymds f11697d6e2 fix(pool): publish durable decommission capacity revisions (#7409) 2026-09-07 22:26:29 +08:00
Zhengchao An 736b6a366c fix(storage): bound multipart admission wait below SDK write timeouts (#7408)
A multipart UploadPart queued for a foreground write permit is not read while it waits, so the client's socket write stalls for the whole wait and the client's own write timeout decides the outcome; botocore reports that as ConnectionClosedError. Lower the default queue wait from 30 s to 10 s so the part receives SlowDown before mainstream SDK timeouts, and stop forcing a 4 MiB SO_RCVBUF on the API listener (new RUSTFS_HTTP_SOCKET_RECV_BUFFER_BYTES restores a fixed size) so a queued connection no longer lets up to 8 MiB of unread body accumulate in kernel memory.

Fixes #7385.

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-07 22:25:06 +08:00
cxymds 7b40b9503b feat(observability): diagnose node-local S3 write failures (#7407) 2026-09-07 22:24:50 +08:00
houseme e22b879996 test(scanner): cover cohort overflow tail fairness (#7403)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:20:12 +08:00
houseme b0dac1ac24 feat(scanner): admit verified segment invalidations (#7402)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:19:47 +08:00
houseme 90e3ed701e test(e2e): prebuild scanner heal evidence binaries (#7400)
* test(e2e): prebuild scanner heal evidence binaries

* test(e2e): widen scanner heal crash evidence window

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:19:32 +08:00
cxymds fd92853ac4 fix(ecstore): recover interrupted pool metadata writes (#7387)
* fix(ecstore): recover interrupted pool metadata writes

* chore(ci): update error format ratchet baseline

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

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-07 22:17:51 +08:00
houseme bc0f608431 test(scanner): align scoped maintenance oracle (#7404)
Keep the production scoped scanner test from mutating an unselected bucket during the dirty-only cycle. That cycle intentionally reuses the clean baseline, while the later Deep and full maintenance cycles still mutate cold storage and prove a full walk refreshes it.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 22:14:29 +08:00
Zhengchao An f20a575994 ci(star-history): bump repo-visuals-action to v1.4.0 (#7406)
Pin the star history workflow to the freshly released v1.4.0 of
overtrue/repo-visuals-action, which adds the chart-layout input and a
refreshed editorial chart/contributor-wall style. The workflow keeps its
existing inputs, so chart-layout stays at the editorial default.
2026-09-07 21:43:13 +08:00
houseme cc91483fac test(heal): preserve MRF replay identity matrix (#7398)
Add an MRF pipeline regression that replays a single authoritative journal containing same-object records that differ by kind and erasure-set scope while a stale legacy mirror is present. The test proves restart replay admits each authoritative responsibility independently and never merges the stale mirror epoch.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 20:20:21 +08:00
houseme c8fc50ada2 feat(heal): accept verified object repair receipts (#7397)
Introduce an object heal receipt wrapper so storage owners can report a positive repaired or verified result without changing legacy heal consumers.

Task-level object heal now records only receipts that match the requested object identity, version, pool, set, and carry a bucket incarnation. Legacy and mismatched receipts still fall back to unknown outcome accounting.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:55:29 +08:00
houseme 9badf3939c fix(admin): report pool metadata write blocks (#7396)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:55:20 +08:00
houseme c71c686e2a fix(scanner): bind scoped set reuse to bucket incarnations (#7395)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:55:10 +08:00
houseme 45f57ca57a test(e2e): allow scanner heal evidence port ranges (#7394)
* test(scanner): wire crash evidence runner

Add the background target crash release-evidence case and require its oracle to carry process-crash-restart evidence with the unclean shutdown marker.

Add a single-case runner that records begin/list/run/finish receipts, validates the concrete case, and confirms the release gate remains blocked by pending mixed-version and release lanes.

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

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

* test(scanner): align crash evidence feature identity

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

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

* test(scanner): harden crash evidence runner

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

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

* test(e2e): allow scanner heal evidence port ranges

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:54:57 +08:00
houseme 265f429358 test(scanner): require heal pressure evidence metrics (#7392)
Require scanner/heal ABBA adapters to emit foreground pressure, heal lock wait p99, and heal attempt counters before a measured W10/W11 run can be accepted.

Report per-leg pressure ratios, lock p99 samples, and attempt cost per healed object so synthetic harness runs remain evidence-contract validation rather than performance proof.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:54:48 +08:00
houseme 47f640f23e test(scanner): prove segment activation gates (#7388)
Extend the segment observation fixture with durable activation prerequisites and add scanner oracles for cold segment zero-walk and distributed invalidation fallback.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:30:43 +08:00
houseme 237c96dd3b test(heal): cover transport receipt replay after peer restart (#7390)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:30:34 +08:00
houseme 647fe1a292 test(heal): cover MRF crash replay gaps (#7389)
Add a Unix process-kill fixture for the authoritative successor fsync boundary where the scoped journal is durable while the legacy mirror still contains the startup epoch. Also extend heal-control transport replay coverage so a duplicate producer after a lost response receives the canonical merged receipt without creating another task.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:30:25 +08:00
houseme 0c6314babc fix(scanner): consume raw page owner resume oracle (#7391)
Treat persisted raw page owner entries as a validated set, not a read_dir-order prefix, so restart scans can consume committed owner pages without recounting them against the raw enumeration budget. Commit checkpointed partial pages, validate owner parent/generation/digests before the skip oracle, and fail closed on duplicate/corrupt page state.

Extend the real scanner restart driver to enforce fixed raw-entry and object budgets across fresh OS processes, and report owner-index coverage in each worker round.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:30:15 +08:00
houseme fc76099280 test(scanner): cover scoped ack loss confirmation (#7393)
Add a scoped dirty-usage ACK confirmation fixture for transport failures after the send attempt. The test confirms that same-instance clean activity can reconcile a lost response, while peer restart and concurrent dirty usage stay pending.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 19:30:06 +08:00
唐小鸭 899f81f3ad fix(replication): resolve drifted replicas via a target version ledger (#7368)
* fix(replication): resolve drifted replicas via a target version ledger

A replication target that mints its own version ids (Wasabi, AWS S3)
never answers to the source uuid, so every version-addressed mutation
after the initial PUT failed forever: permanent version deletes answered
NoSuchVersion every heal cycle, and tag / retention / legal-hold updates
re-PUT the object, minting one more target version per update
(rustfs/backlog#2340).

Record the id the target assigned as a per-target ledger on the source
version (replication-target-version-<arn>, written through the existing
status writeback) and resolve every later mutation through it: version
deletes DELETE the ledger id, metadata updates go through the
metadata-only Object Lock and tagging APIs. Replicas written before the
ledger existed are located by exact key and ETag, minus the candidates
other generations of the key already claim through their own ledgers; an
ambiguous remainder is refused with a backoff instead of guessed, since a
wrong pick would destroy a live generation. A fresh write never consults
content identity. NoSuchVersion on a version-addressed DELETE counts as
purged.

The fake target gains the Wasabi shape (404 NoSuchVersion on an unknown
id, per-version Object Lock APIs) and the matrix covers the three
mutation classes plus the same-bytes generation case.

* fix(scanner): drop the unused Digest import

Same one-line change as rustfs/rustfs#7366 (main is red with it under -D warnings); carried here so the stacked PRs' merge commits compile until that fix lands.

* fix(admin): probe replication-check mutations by the assigned version id (#7373)

On a target that mints its own version ids the DeleteMarker and
VersionDelete phases of ?replication-check were skipped: they addressed
the source id, which such a target never had. The replication worker now
addresses the id the target assigned (the target-version ledger), and
the probe already holds that id from its own PUT, so run both phases
against it. VersionFidelity keeps failing with the mismatch code and the
target stays FAILED; the phases report whether ledger-addressed purges
work against this endpoint (rustfs/backlog#2340).

* fix(replication): abandon purges to targets the bucket no longer names (#7377)

* fix(admin): probe replication-check mutations by the assigned version id

On a target that mints its own version ids the DeleteMarker and
VersionDelete phases of ?replication-check were skipped: they addressed
the source id, which such a target never had. The replication worker now
addresses the id the target assigned (the target-version ledger), and
the probe already holds that id from its own PUT, so run both phases
against it. VersionFidelity keeps failing with the mismatch code and the
target stays FAILED; the phases report whether ledger-addressed purges
work against this endpoint (rustfs/backlog#2340).

* fix(replication): abandon purges to targets the bucket no longer names

A permanent version delete whose replication keeps failing stays in
xl.meta as a PENDING purge, hidden from listings, until every target
confirms it. Once the operator removes the replication configuration or
the rule naming that target nothing ever confirms it: the heal path
derived its delete decision from the configuration (the decision string
is not persisted) and skipped the version forever, so DeleteBucket
answered BucketNotEmpty for a residue the client could neither list nor
remove (rustfs/backlog#2340).

Owe a version purge to the targets its purge state names, let the heal
path through without a configuration, and have the delete worker settle
a target the configuration no longer names as abandoned: the purge is
reported complete locally through the normal writeback, the replica on
the former target is left alone, and the event
replication_purge_abandoned plus a counter are the record.

* fix(admin): send replication-check marker creation without a version id

Running the DeleteMarker / VersionDelete phases on a target that mints
its own version ids exposed two probe-shape bugs on real Wasabi:

- the DeleteMarker phase put the assigned version id on its DELETE. A
  RustFS peer reads the source-deletemarker header and creates a marker,
  but a generic S3 target executes it as a permanent delete of the probe
  version, so VersionDelete then answered NoSuchVersion. Use the same wire
  shape as live delete replication: no versionId on a marker creation.
- cleanup treated NoSuchVersion on the version the VersionDelete phase had
  already removed as a failure (RustFS/MinIO answer 204 there).

Also gate the no-configuration heal pass-through for pending purges on a
purge state that actually names targets, so a purge without a recorded
target keeps the ordinary skip (scanner unit test), and merge origin/main
(#7365 settles the pool-metadata probe test that failed in CI).

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-07 11:23:32 +00:00
cxymds 37bda24e1c fix: reject unsupported pool expansion with actionable errors (#7360)
* fix: reject unsupported pool expansion with actionable errors

Report singleton-pool and persisted-topology constraints before misleading startup retries. Preserve single-node multi-drive admission and existing parity policies, and cover format preservation plus operator recovery guidance for issue #6186.

* fix: keep pool layout errors typed

Preserve actionable pool layout diagnostics without adding generic formatted errors. Tighten the shrink-only baseline and assert that both typed payloads survive the I/O boundary.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-07 11:07:42 +00:00
houseme 4c2a0cdf9a chore(scanner): stage Scanner/Heal follow-up slices (#7374)
* fix(scanner): remove unused digest import

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

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

* feat(scanner): add raw page owner index (#7375)

* feat(scanner): add raw page owner index

Add a serializable raw enumeration page owner index for scanner resume work.

The index exposes unsupported, building, and ready states, validates committed page identity by recomputing digests, and uses generation checks for CAS-style page commits.

Focused tests cover small-budget restart progress, page digest/source drift rejection, corrupt deserialized state, CAS failure, precommit crash, empty sources, and invalid entry boundaries.

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

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

* feat(scanner): persist raw page owner resume state (#7379)

Wire the scanner raw enumeration partial-cache writer to the raw page owner index so interrupted bucket walks can retain validated page-builder state across scanner restarts.

Keep complete owner sources terminal-only, add partial-source ingestion for in-progress raw directory reads, and validate the persisted page index through bucket checkpoint preparation.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): fence segment producer observations (#7381)

Require the segment observation fixture to carry source, incarnation, key-format, baseline, process epoch, generation-window, gap, overflow, and producer-coverage proof before accepting a narrowed proposal. Keep the diagnostic path fixture-only and remove its ordinary stderr output.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): isolate pool metadata read probes (#7367)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover MRF crash successor matrix (#7369)

* test(heal): cover MRF crash successor matrix

Add process-boundary MRF replay coverage for the successor snapshot window after a retained startup journal is flushed but before cleanup deletes it. Extend the mixed authoritative/legacy reader fixture with a scoped v2 journal epoch to pin the no-merge contract.

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

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

* test(heal): cover service-kill MRF replay (#7380)

Add a Unix process fixture that waits after publishing the pending MRF successor snapshot, then is terminated by the parent before restart replay.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(heal): cover transport-lost start receipts (#7371)

Add gRPC transport fault fixtures for heal-control start admission. The tests distinguish pre-admission transport loss from post-admission response loss, then verify exact envelope retries reuse the canonical receipt while fresh forceStart requests create distinct tasks.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* test(scanner): add crash-restart heal evidence case (#7370)

* test(scanner): add crash-restart heal evidence case

Add a distinct W21 background target crash case to the scanner/heal evidence registry and oracle path.

Keep the existing restart lane on graceful process restart, keep the crash lane on hard kill, and make the wiring checker reject evidence/oracle mismatches.

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

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

* test(scanner): support older Python wiring checks

Let the scanner/heal evidence wiring checker run under Python 3.9/3.10 by falling back to tomli and chunked SHA-256 hashing when the Python 3.11 standard APIs are unavailable.

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): reject stale raw page source seeds (#7382)

Do not prefill a resumed raw page owner with previously indexed entries when starting a new raw directory observation pass. The next pass must observe the same prefix again before the page index can advance; otherwise the index is discarded fail-closed.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* fix(scanner): defer raw page revalidation until observed (#7384)

A resumed raw page owner index must not prefill entries from older cache state, but it also must not discard a valid multi-entry index before the current raw directory pass has observed enough entries to prove identity. Track the persisted index floor and only run the strict owner identity check once the current pass reaches that floor.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 18:13:39 +08:00
houseme b370e746a0 fix(io-metrics): simplify early eviction check (#7383)
Replace a needless bool return with the equivalent predicate so clippy can pass with warnings denied.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 18:12:00 +08:00
Zhengchao An 95e6c89c1c fix(replication): carry the compression layout through SSE-C passthrough (#7366)
* fix(scanner): drop the unused Digest import

* fix(replication): carry the compression layout through SSE-C passthrough (#7372)

* fix(replication): queue an in-flight version only once (#7376)
2026-09-07 18:07:33 +08:00
houseme aafa7e2b7f test(ecstore): align heal capacity admission regression (#7386)
Align the suspended-owner heal regression with read-only pool metadata admission semantics. The quorum-boundary case now asserts that heal fails the current capacity admission without latching the global pool metadata writer.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 17:52:23 +08:00
houseme 5211b56277 fix(ecstore): isolate pool metadata read probes (#7365)
* fix(ecstore): isolate pool metadata read probes

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

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

* fix(readiness): surface blocked pool metadata writes

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

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

* fix(scanner): remove unused digest import

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 16:42:39 +08:00
GatewayJ c9c6bb7a24 fix(oidc): support workload discovery and JWKS negotiation (#7348)
* fix(oidc): support workload discovery and JWKS negotiation

* test(oidc): cover existing Console authorization code flows

* test(oidc): cover admin validation and document workload setup
2026-09-07 06:38:04 +00:00
houseme ad0c44dc63 fix(ecstore): keep pool meta read probes retryable (#7359)
Evaluate read-only pool metadata planning probes through an isolated write-state clone so transient unreadable replicas fail the current admission without permanently latching the shared writer gate.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 14:31:25 +08:00
houseme 2d6f9417ff feat(scanner): persist raw enumeration cancellation cursor (#7358)
Return a partial data-usage cache when raw filesystem enumeration is cancelled before object progress can be written. The partial cache now carries a validated raw enumeration cursor for V2 checkpoint scans while keeping the snapshot incomplete and clearing older frontier/checkpoint metadata.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 14:27:59 +08:00
houseme 638da7a2e3 fix(scanner): confirm lost dirty usage acknowledgements (#7357)
When the remote dirty-usage ACK response is lost, re-probe scanner activity once and accept the ACK only if every target host still reports the same scanner instance with no dirty usage pending. Duplicate targets, restarted peers, unverified activity, and concurrent writes remain pending.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 14:27:53 +08:00
houseme 7c7b48aaca test(heal): cover MRF replay across process restart (#7355)
Add an ECStore-backed child-process fixture that publishes the MRF journal in one OS process, then verifies a restarted manager can replay the complete snapshot while retaining the journal when bounded admission accepts only a prefix.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 14:16:31 +08:00
houseme c40988dae7 feat(scanner): replay recovery intents at startup (#7354)
Discover durable scanner recovery intents during startup and sequentially re-drive accepted or running usage full-rebuild work. Disabled scanner startup also replays existing durable intents without enabling the normal scanner loop.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 14:16:19 +08:00
houseme 481c1b7939 chore(deps): refresh scanner heal continuation lockfile (#7351) 2026-09-07 13:31:49 +08:00
houseme 26d827f946 test(heal): cover set bulkhead scheduler isolation (#7347)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:54:55 +08:00
houseme 376cb037a0 test(scanner): classify raw enumeration replay (#7339)
Record bounded first/last raw entry markers in the restart diagnostic worker and have the driver classify repeated raw windows when retained coverage does not advance.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:54:49 +08:00
houseme 64829c704d fix(heal): retain MRF replay journal until retry anchor (#7340)
Keep the startup journal on disk when replay cannot fully re-arm or the heal manager refuses a replayed intent with Full/QueueFull. The next live snapshot can still advance the journal after the retry anchor is durable again.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:54:38 +08:00
houseme 17ba30f648 fix(ecstore): roll back failed CAS directory fsync (#7343)
Restore the previous control-file bytes, or remove a newly created file, when the Unix compare-and-update path reaches the rename but then fails to fsync the parent directory. This keeps failed metadata CAS publications from advancing recovery anchors.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:54:28 +08:00
houseme 21015cfac8 feat(scanner): accept durable recovery intents (#7344)
* feat(scanner): accept durable recovery intents

Add a CAS-backed scanner usage recovery intent record for async full rebuild admission. The admin reset endpoint can now persist and replay idempotent intent acceptance before returning 202, and a read-only status route exposes the durable request state.

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

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

* feat(madmin): add scanner recovery intent helpers (#7345)

Expose madmin helpers for accepting and querying asynchronous scanner usage-state full-rebuild recovery intents. Keep the legacy synchronous helper unchanged and pin the new request/response wire contract with focused client tests.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

* feat(scanner): execute recovery intents asynchronously

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:54:04 +08:00
houseme c04cd089e9 test(scanner): parameterize heal evidence oracles (#7346)
Allow the scanner/heal release evidence e2e helper to bind the active case identity and oracle file through an explicit descriptor instead of hard-coding the background target restart artifact. Extend the checker self-test so a single run can finish multiple registry-declared oracles while release gates remain pending until every required lane is complete.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:53:54 +08:00
houseme bffdf0809f feat(scanner): add raw enumeration cursor metadata (#7349)
Add a durable raw enumeration cursor shape to scanner usage metadata and validate it against bucket identity, source, bounds, version, and page digest before preserving it across checkpoint preparation.

Keep empty cursor metadata omitted so existing pinned .usage-cache.bin bytes stay unchanged, while legacy readers still ignore the additive field when it is present.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 12:53:32 +08:00
Zhengchao An 19a69ee897 fix(ecstore): hide and reclaim delete residue in prefix listings (#7342)
The never-versioned listing fast path emitted any non-empty directory
without xl.meta as a prefix, which surfaced the data dirs deleted
versions leave behind as phantom folders that HEAD, listing and
recursive remove could not touch. Probe such directories for the
delete-residue shape before emitting them, and let any complete empty
first page of a prefix listing trigger the existing fail-closed orphan
purge so ordinary browse and remove traffic reclaims committed residue.
2026-09-07 04:42:14 +00:00
Zhengchao An 1499295393 fix(admin): probe set drives concurrently for storage info (#7338)
The admin storage walk probed a set's drives one after another, each
bounded by the disk_info timeout, so a few drives still recovering
after a power cut pushed the local snapshot past the peer probe budget
and healthy peers rendered as unknown. Probe all drives at once so the
walk costs one timeout at most, and add a test-only probe delay hook to
pin that bound.
2026-09-07 04:35:01 +00:00
Zhengchao An 4c4dcb6f5e fix(storage): queue multipart parts for foreground write permits (#7337)
Multipart parts shared the 250 ms direct-PutObject wait on the foreground
write permit pool, so SDK-default concurrency (many parts per upload in
flight at once) was rejected wholesale with SlowDown at stock settings.
Keep the pool that bounds in-flight bodies, but let parts wait in a
bounded queue with their own timeout before body ingest, report the
queue depth in the ForegroundWrite admission snapshot, and document the
foreground write admission environment variables.
2026-09-07 04:13:30 +00:00
Zhengchao An 672087ec0d ci(docker): move latest on every prerelease until first stable tag (#7341)
Since rc.1 the latest tag has been frozen at 1.0.0-beta.12 because the
docker workflow only allowed alpha/beta prereleases to update latest
(#2732 dropped the rc case). Before 1.0.0 GA, latest is expected to
track the newest test build.

- Prereleases (alpha/beta/rc) now update latest as long as no stable
  vX.Y.Z tag exists on origin, so the rule retires itself at GA.
- Channel tags (alpha/beta/rc) are now always added for prereleases;
  the previous if/elif skipped the channel tag whenever latest was
  created, which is why no :beta tag was ever published.
2026-09-07 11:25:40 +08:00
houseme 9dbeae1b45 chore(deps): refresh scanner batch baseline (#7336)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 11:04:34 +08:00
cxymds 6018dd372f perf(ilm): reduce transition transaction mutations (#7320)
* perf(ilm): reduce transition transaction mutations

* test(ilm): rename transition kill points

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-07 02:40:57 +00:00
houseme d633a635ec feat(madmin): add scanner reset client helpers (#7333)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 10:28:42 +08:00
houseme fbd30a6f43 test(scanner): cover concurrent cycle reset convergence (#7335)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 10:28:37 +08:00
houseme a32360198c test(scanner): cover scoped ack resolver fallback (#7332)
* test(scanner): cover scoped ack resolver fallback

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

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

* test(scanner): reduce scoped resolver helper args

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-07 10:28:19 +08:00
GatewayJ 05efc584b0 perf(s3-tables): avoid duplicate registration prefix scans (#7310)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-07 09:55:42 +08:00
cxymds a70c96d520 feat(ilm): execute legacy recovery dispositions (#7304)
* feat(ilm): execute legacy recovery dispositions

* feat(ilm): retry retained transition recovery (#7308)

* fix(admin): use gateway errors for recovery retries

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-07 01:46:43 +00:00
Zhengchao An b0f68f5d0a test(upgrade): read and replicate rc.5 multipart layouts (#7334) 2026-09-07 09:37:21 +08:00
Zhengchao An df554439b0 fix(heal): fall back to set-wide format for directory-backed targets (#7331)
fix(heal): fall back to set-wide format for directory-backed replacement targets

Since #7018 `renew_disk` routes an unformatted local endpoint through the automatic replacement heal, which requires the target to be an independently mounted disk. Directory-backed deployments (the operator set `RUSTFS_UNSAFE_BYPASS_DISK_CHECK`, which the startup disk-independence check already requires for endpoints sharing a device) can never pass that admission, so a runtime-wiped or replaced directory disk stayed unformatted forever: the heal task failed with "replacement target is not a stable mounted disk" and the auto-scan kept deferring the endpoint. This broke the Issue #1533 contract and the `heal_erasure_disk_rebuild_test` lane on main since 2026-09-02.

When the disk-check bypass is set and the replacement target fails admission, the erasure-set heal now logs a warning and downgrades to the ordinary set-wide `heal_format` path that predated replacement admission, and the auto-scan no longer defers such endpoints. The mount admission itself is unchanged and still cannot be bypassed by any environment variable; deployments without the bypass keep failing closed.

The endpoint-blackhole heal scenario now probes whether `iptables` can read the OUTPUT chain and logs an explicit skip when the host lacks `CAP_NET_ADMIN` (containerised runners report "Permission denied" from the nf_tables backend even under sudo); `RUSTFS_E2E_REQUIRE_NET_FAULT_INJECTION=1` turns that into a failure for lanes that provision the capability. The CI full-gate job surfaces the missing capability as a workflow warning, and the runtime-wipe fixture retries `remove_dir_all` on the listing race macOS surfaces as `DirectoryNotEmpty`.

Refs rustfs/backlog#2357.
2026-09-07 09:25:44 +08:00
唐小鸭 3b404e56c0 fix(replication): forward single-part object checksums as headers (#7313) 2026-09-07 01:17:24 +00:00
Zhengchao An ff00872922 docs(ilm): restore transition gate doc comment after handler reorder (#7325)
Commit 086ee8e48 moved authorize_recovery_admin_request above authorize_transition_admin_request and left the transition gate's doc comment attached to the recovery gate, which returns a hashed actor rather than the masked access key the comment describes. Move the comment back onto the transition gate and give the recovery gate its own accurate description. No behavior change.
2026-09-07 08:59:20 +08:00
唐小鸭 4d1ce9618a fix(scanner): pass dirty scopes to distributed scope resolution (#7329) 2026-09-07 07:42:17 +08:00
houseme a2bad0953f test(scanner): collect heal evidence oracles from registry (#7328) 2026-09-06 21:44:01 +00:00
唐小鸭 14c99a994c fix(filemeta): keep data dir of a version awaiting purge replication (#7307) 2026-09-07 05:30:44 +08:00
houseme f6c2a9bfe0 test(scanner): cover overflow service cohort rotation (#7324) 2026-09-07 05:29:59 +08:00
houseme 640d7e0e3c test(heal): cover MRF snapshot recovery bounds (#7327) 2026-09-07 05:29:25 +08:00
houseme 409ac3de66 test(scanner): cover reset cleanup process crash boundaries (#7326) 2026-09-07 05:29:14 +08:00
唐小鸭 2a63fcbea6 fix(replication): stop duplicate re-drives on own-version-id targets (#7323) 2026-09-07 05:29:00 +08:00
houseme f4049598e4 feat(scanner): send scoped dirty usage acknowledgements (#7322) 2026-09-07 05:28:50 +08:00
GatewayJ 1651541d38 fix(s3-tables): preserve encoded paths in client signatures (#7309) 2026-09-07 05:28:06 +08:00
cxymds 85849788af chore(tier): remove remaining blanket lint allowances (#7306) 2026-09-07 05:27:29 +08:00
Henry Guo 975983abdd feat(scanner): reuse clean local bucket prefixes (#7208) 2026-09-07 05:26:56 +08:00
唐小鸭 cf1c45eb91 test(replication): pin directory-marker null version and replication (#7315) 2026-09-06 19:26:09 +00:00
唐小鸭 536c283716 test(ilm): realign source-marker assertions with handler order (#7321) 2026-09-07 02:10:13 +08:00
houseme a04ddc237e chore: remove Go heal outcome compatibility fixture (#7319)
* del go code

* chore: record ILM lifecycle validation on go-del (#7318)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 23:49:56 +08:00
houseme 7de6ac82e1 test(scanner): cover reset cleanup across store reopen (#7317)
Add a real ECStore reopen regression for scanner usage-state reset cleanup boundaries. The fixture seeds each partially completed cleanup state, recreates the store, then verifies the reset resumes without rewriting the bootstrap intent or deleting unrelated metadata.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 23:40:31 +08:00
唐小鸭 7993e11058 docs(s3): record object-key path-segment validation as intentional (#7314) 2026-09-06 23:27:10 +08:00
houseme 37ead2f69c test(ilm): align feature matrix lifecycle assertions (#7311)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 23:27:01 +08:00
houseme 1b1e590df7 test(scanner): verify quota state across reset and owner restart (#7303)
* test(scanner): verify quota state across reset and owner restart

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

* fix: restore ILM transition and lifecycle validation (#7312)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 23:26:01 +08:00
houseme 086ee8e48a fix(scanner): require root publication proof before dirty ack (#7297)
* fix(scanner): require root publication proof before dirty ack

Bind ACK expectations to validated scan candidates and confirm the actual primary-root revision and readback. Retain saved outcomes and dirty responsibility when stronger evidence is unavailable. Isolate CAS attempt confirmation and invalidate proof after scope mutations.

Revalidate observed candidate reuse before issuing a new publication proof, preserve the exact validated authoritative baseline work digest, and settle fixture commit tails before stable maintenance scans. Keep scoped ACK production disabled.

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

* fix: restore ILM transition and lifecycle validation (#7316)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 23:25:40 +08:00
Zhengchao An 22bff27aee fix(storage): derive multipart identity from stored parts (#7305) 2026-09-06 22:04:15 +08:00
houseme 8a20498705 fix(heal): retain completed reports during clock rollback (#7299)
Treat a negative wall-clock age as zero without bypassing count or byte eviction. Cover canonical and alias queries, terminal outcomes, exact TTL expiry, and capacity limits during rollback.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 21:28:22 +08:00
houseme c3e6d90c6f docs(heal): clarify start retries and execution budgets (#7300)
Distinguish control requests, cumulative task execution and object retries. Explain ambiguous responses and bounded envelope replay without promising HTTP idempotency or changing runtime policy.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 21:27:37 +08:00
Zhengchao An d2af8f073e test(s3): exclude invalid lifecycle filter vector (#7295)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-06 21:21:12 +08:00
Zhengchao An 2baba1bda7 fix(scanner): preserve verified maintenance digest (#7293)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-06 21:20:58 +08:00
Zhengchao An bf2ca9113f test(scanner): settle fixture writes before activity baseline (#7290) 2026-09-06 21:20:46 +08:00
Zhengchao An f474ea30d4 fix(admin): preserve decommission readiness error (#7285) 2026-09-06 21:20:32 +08:00
cxymds 002ac9544c feat(ilm): add immutable legacy recovery exports (#7283)
* feat(ilm): add immutable legacy recovery exports

* feat(ilm): add legacy recovery disposition records (#7292)

* fix(ilm): stabilize legacy recovery decode errors

* fix(admin): route recovery auth errors through gateway

* fix(ilm): resolve recovery disposition clippy errors

* fix(ilm): remove redundant recovery test clones
2026-09-06 21:20:16 +08:00
Zhengchao An d74d970e24 fix(ecstore): retain namespace ownership during multipart commit (#7282) 2026-09-06 21:20:02 +08:00
Zhengchao An cc15eae479 fix(lifecycle): allow multiple filter predicates without And wrapper (#7298) 2026-09-06 21:09:11 +08:00
dependabot[bot] c130d00d4b chore(deps): bump golang.org/x/net from 0.54.0 to 0.55.0 in /scripts/compat/heal-outcome in the go_modules group across 1 directory (#7296)
chore(deps): bump golang.org/x/net

Bumps the go_modules group with 1 update in the /scripts/compat/heal-outcome directory: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.54.0 to 0.55.0
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-06 19:13:21 +08:00
dependabot[bot] e49d9cdea2 chore(deps): bump the go_modules group across 1 directory with 2 updates (#7288)
Bumps the go_modules group with 1 update in the /scripts/compat/heal-outcome directory: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.37.0 to 0.52.0
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.52.0)

Updates `golang.org/x/net` from 0.39.0 to 0.54.0
- [Commits](https://github.com/golang/net/compare/v0.39.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-06 18:56:54 +08:00
houseme f5778d8b97 chore(deps): refresh concurrency and protocol dependencies (#7289)
chore(deps): update Crossbeam, Redis, DER, and ipnet

Refresh the shared concurrency, Redis client, DER decoding and IP network dependencies while retaining existing feature selections and the hotpath pin.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 18:51:12 +08:00
Zhengchao An 9da42a899a test(connect): refresh offline enrollment e2e chain (#7294) 2026-09-06 18:30:04 +08:00
Henry Guo 3d46ed312a feat(scanner): reuse complete observed scan candidates (#7206) 2026-09-06 17:52:52 +08:00
Zhengchao An 3496277e7c test(ecstore): drain source writes before corrupting shards (#7291) 2026-09-06 17:51:07 +08:00
cxymds 0b72f39023 fix(tier): avoid re-fencing published mutations (#7274) 2026-09-06 16:57:03 +08:00
Zhengchao An 30a0937a7d fix(ecstore): retain single-delete physical namespace ownership (#7287) 2026-09-06 16:56:24 +08:00
houseme 5b962b6c58 feat(heal): expose compatible canonical v3 outcomes (#7256)
* feat(heal): expose compatible canonical v3 outcomes

Serialize the existing canonical heal outcome, retain the v3 summary vocabulary, and reject or conservatively adapt contradictory peer success responses. Preserve progress and bounded result cursors without treating legacy storage responses as repair proof.

Add optional SDK outcome and cursor support with shared Rust fixtures, pinned legacy Go decoder and mc polling checks, and explicit compatibility limits.

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

* fix(madmin): box heal stop task status outcome

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:46:42 +08:00
houseme 0ee5408b94 feat(scanner): preserve bounded bootstrap admission fairness (#7260)
feat(scanner): retain bounded bootstrap admission fairness

Keep a leader-local bounded cohort across scanner retries and preserve
waiting bucket priority during dirty arrivals and capacity overflow.
Order source permits in the dispatcher without changing result identity,
parent budgets, explicit cycle timing, or persistent coverage evidence.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:45:57 +08:00
houseme cb3100a252 fix(scanner): visit compacted subtrees during deep scans (#7270)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:45:17 +08:00
houseme 3a4afe9b38 fix(heal): bound cross-page object retry delays (#7273)
Retain failed identities in an execution-local count and byte bounded window so healthy later pages can advance. Preserve retry jitter, deadlines, terminal accounting and pressure pacing, with deterministic head-of-line and capacity regressions.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:44:59 +08:00
houseme 71859ff83c fix(scanner): resume committed cleanup when scanning is disabled (#7281)
Run one supervised cleanup attempt for an existing operator reset, with
strict phase and revision checks under the original leader lock. Keep v3
reset authorization and responses unchanged, report deferred status, and
bound probe and shutdown waits without aborting in-flight reset ownership.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:44:45 +08:00
houseme cb72df269a fix(heal): retain hints without verified repair receipts (#7275)
Stop task completion and legacy notices from discharging scanner retry hints. Preserve existing hints and their retry due time across admission observations, bound retry scheduling, and synchronize changed batches once even on cancellation.

Exercise the production MRF consumer, manager, event channel and scanner ledger. Document producer durability gaps without enabling successor activation or garbage collection.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-06 16:44:30 +08:00
Zhengchao An d7b7d1835f test(admin): validate repair futures with the default stack (#7280) 2026-09-06 15:43:13 +08:00
houseme b92392a04a ci: give target repair tests larger stack (#7272) 2026-09-06 15:38:05 +08:00
Zhengchao An a40ec8a6f3 fix(odm): preserve progress when list-through is disabled (#7278)
* test(odm): cover disabled list-through continuation progress

* test(odm): cover literal cache tags in disabled list cursors

* fix(odm): preserve progress when list-through is disabled
2026-09-06 15:24:59 +08:00
Zhengchao An c99efd9477 test(admin): box remote target repair scenarios (#7277) 2026-09-06 15:23:30 +08:00
Zhengchao An 27d593fa35 test(odm): bypass loopback proxies in native list fixtures (#7276) 2026-09-06 15:22:50 +08:00
Zhengchao An 0137d14064 test(odm): refresh verified Linux E2E selection (#7271) 2026-09-06 14:48:41 +08:00
houseme 1dddf357cd test(scanner): add bounded cache cost microprofile (#7261) 2026-09-06 14:14:07 +08:00
houseme 395ba797fc fix(admin): bound peer probe retries to one round deadline (#7257) 2026-09-06 14:13:47 +08:00
houseme 51893abfbf feat(heal): pace running admin work at safe boundaries (#7255) 2026-09-06 14:13:35 +08:00
RustFS 92c17af8e3 ci(e2e): run distributed e2e on ubuntu-latest (#7253) 2026-09-06 14:13:21 +08:00
cxymds f17f31a3df feat(ilm): persist recovery controls for legacy tier journals (#7252) 2026-09-06 14:13:09 +08:00
Zhengchao An 1a88870809 fix(odm): preserve cursor compatibility and native source semantics (#7238) 2026-09-06 14:12:56 +08:00
唐小鸭 760c9d65be fix(replication): close IAM snapshot, marker purge and broadcast gaps (#7195) 2026-09-06 14:12:25 +08:00
houseme 08283d1fdc test(scanner): verify default scoped entry fallback walks (#7241) 2026-09-06 14:11:45 +08:00
houseme 6a323c3e91 test(heal): cover start retry and deadline outcome contracts (#7242) 2026-09-06 14:11:24 +08:00
Zhengchao An 1650f3c2a6 test(odm): verify global disable across restarts (#7268)
* test(odm): cover global disable across restarts

* test(odm): accept omitted V1 next marker

* test(odm): gate backfill GET until the crash completes

* test(odm): remove unused fault action import

* test(odm): assert GET gate suspension without network races

* test(odm): refresh verified Darwin E2E selection
2026-09-06 13:54:59 +08:00
Zhengchao An 38611d2510 fix(admin): compile metadata test helper only in tests (#7266)
fix(admin): compile metadata update facade only for tests
2026-09-06 13:28:58 +08:00
houseme 1a459d650f chore(deps): update flake.lock (#7264)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e8be781' (2026-08-29)
  → 'github:NixOS/nixpkgs/17de0b9' (2026-09-04)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/996e9b0' (2026-08-29)
  → 'github:oxalica/rust-overlay/c361047' (2026-09-05)
2026-09-06 13:19:50 +08:00
Zhengchao An 5aef1796cc fix(odm): reject ambiguous native source dot segments (#7263)
* fix(odm): reject ambiguous native source dot segments

* docs(odm): align native provider limitations with implementation
2026-09-06 13:11:35 +08:00
Zhengchao An a8aaadb886 fix(replication): preserve concurrent remote target changes (#7262)
* test(replication): cover concurrent remote target writes

* fix(replication): merge remote target writes under transactions

* test(replication): preserve concurrent repairs during target updates

* fix(replication): retain removal guards after request cancellation

* test(replication): enable loopback in ordinary target fixtures

* test(replication): isolate remote target mutation scenarios

* refactor(admin): keep target writes within existing boundaries

* test(replication): assert removed target cache state

* test(replication): inspect target cache before client refresh
2026-09-06 12:51:44 +08:00
Zhengchao An 71f1dcf859 fix(admin): follow recovery facade and error boundaries (#7259) 2026-09-06 12:35:54 +08:00
Zhengchao An d44244f60f fix(admin): preserve metadata during export and target repair (#7258)
* fix(admin): reject incomplete metadata backups

* fix(admin): repair remote targets from locked disk state

* test(admin): fence target repair against source changes

* fix(admin): report unreadable XML in metadata exports

* test(admin): enable loopback in target repair fixtures

* refactor(admin): remove unused metadata getter forwards

* test(admin): box direct target repair futures

* test(admin): box target repair scenarios at env boundary
2026-09-06 12:18:00 +08:00
Zhengchao An d3884ed3ea test(odm): use fixed-size Azure request chunks (#7254) 2026-09-06 12:10:39 +08:00
Zhengchao An 282d6d5efe fix(odm): decode encoded Azure blob names exactly once (#7251)
* fix(odm): decode encoded Azure blob names exactly once

* test(odm): cover Azure encoded name transport matrix

* test(odm): keep Azure cursor continuation query stable
2026-09-06 11:10:40 +08:00
RustFS f0d865728c ci(e2e): use tmpfs for distributed pool isolation (#7207)
* test(e2e): add distributed 4x4 validation

* ci(e2e): use tmpfs for distributed pool isolation

sm-standard-4 is an ARC pod without usable loop devices, so
mount -o loop fails with ENOENT before any pool filesystem is
attached. Sized tmpfs still gives each pool a distinct st_dev
and independent 1G statfs capacity.

Co-authored-by: RustFS <hello@rustfs.com>

* test(e2e): prove operations overlap data movement

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-06 10:52:12 +08:00
cxymds 3df39ef4d7 fix(scanner): separate write retries from movement backlog (#7249) 2026-09-06 10:51:46 +08:00
Zhengchao An b0c73c1224 fix(ecstore): retain namespace owners through local disk completion (#7245)
* fix(ecstore): retain namespace owners through local physical tails

(cherry picked from commit a2f242463316e87604feadbdac5e4148140e72c0)

* test(ecstore): expose stale fsync group cleanup

* fix(ecstore): capture complete fsync worker guard

(cherry picked from commit 1dc90bb836e20ea9ee45d0a629a9201e20d231c4)

* fix(ecstore): preserve successor fsync group registration

(cherry picked from commit c7dfaad90526052e56c57dafffa4813bdcde46ca)

* test(ecstore): mark physical owner fixtures as inline

* test(ecstore): wait for namespace owner release before asserting

The namespace owner tests decided that ownership had ended when the Weak probe stopped upgrading or when the mutation lease could be reacquired. Both signals fire before the owner guard's Drop decrements the pending counter: Arc releases its strong count before running Drop, and the lease drops its locks before its owner field. The rio-v2 lane hit that window in undo_fresh_version_keeps_physical_namespace_owner_after_timeout.

Extend every drain wait to also require namespace_commits_pending() to be false, so the assertions observe the completed release instead of racing it.
2026-09-06 10:51:38 +08:00
houseme 54c11ef28b test(scanner): bound segment observation diagnostics (#7240)
* test(scanner): bound segment observation diagnostics

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

* test(scanner): observe committed fixture changes during walks

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

* test(scanner): validate segment fixture metadata and off state

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:51:30 +08:00
houseme e99c41a9bf test(scanner): verify restart evidence against tested builds (#7232)
* chore(deps): refresh scanner heal batch dependency baseline

Regenerate compatible lockfile selections before the next implementation
batch. Cargo upgrade leaves direct requirements unchanged.

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

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* chore(deps): refresh profiling dependencies for the next batch

Update hotpath and its macro crate to the compatible patch release before
the next dependency-ready implementation tasks.

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

* fix(deps): preserve supported hotpath focus expressions

Keep the profiler runtime before its regex-lite compatibility regression.
Track the opt-in validation required to remove this constraint in backlog.

Refs rustfs/backlog#2302.

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

* test(scanner): add bounded ABBA validation harness

Refs rustfs/backlog#2266 and rustfs/backlog#2240.

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

* test(scanner): verify real restart evidence before release gates

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

* fix(test): bind scanner evidence to execution and build identity

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

* docs(test): use the nextest workspace report directory

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

* fix(test): reap ABBA leaders only after process-group cleanup

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

* fix(test): preserve inclusive ABBA thresholds

Use decimal boundary comparisons for ABBA ratio checks and cover exact documented p99, throughput, and P1 limits.

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:51:22 +08:00
houseme fddecf0afe test(scanner): diagnose raw enumeration across restarts (#7228)
* test(scanner): diagnose raw enumeration across process restarts

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

* test(scanner): observe the canonical synthetic disk path

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

* test(scanner): reject unobserved enumeration budget evidence

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

* test(scanner): remove redundant disk path clone

* fix(app): keep list-through header import test-only

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:51:14 +08:00
houseme 55fd73ed48 feat(heal): add pending legacy MRF migration staging (#7219)
* chore(deps): refresh scanner heal batch dependency baseline

Regenerate compatible lockfile selections before the next implementation
batch. Cargo upgrade leaves direct requirements unchanged.

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

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* chore(deps): refresh profiling dependencies for the next batch

Update hotpath and its macro crate to the compatible patch release before
the next dependency-ready implementation tasks.

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

* fix(deps): preserve supported hotpath focus expressions

Keep the profiler runtime before its regex-lite compatibility regression.
Track the opt-in validation required to remove this constraint in backlog.

Refs rustfs/backlog#2302.

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

* feat(heal): add explicit committed MRF snapshot reader

Refs rustfs/backlog#2263 and rustfs/backlog#2240.

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

* docs(heal): register legacy MRF inspection cleanup

State the compatibility removal condition on the source marker and in
the architecture cleanup register.

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

* test(heal): cover ambiguous complete legacy MRF replicas

Cover complete subset replicas, differing sets and unknown scope in both
disk orders, preserving all original journal evidence during inspection.

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

* feat(heal): stage conservative legacy MRF migration evidence

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

* fix(heal): reject incomplete migration lineage before staging

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

* fix(heal): validate migration retries against complete lineage

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

* fix(heal): keep reused MRF migration slots retryable

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

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

* test(heal): cover source-change retry on reused MRF slots

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

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

* fix(heal): retain unmatched migration manifest evidence

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:51:04 +08:00
houseme 1ae80c41ec feat(heal): record canonical object and task outcomes (#7218)
* chore(deps): refresh scanner heal batch dependency baseline

Regenerate compatible lockfile selections before the next implementation
batch. Cargo upgrade leaves direct requirements unchanged.

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

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* chore(deps): refresh profiling dependencies for the next batch

Update hotpath and its macro crate to the compatible patch release before
the next dependency-ready implementation tasks.

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

* fix(deps): preserve supported hotpath focus expressions

Keep the profiler runtime before its regex-lite compatibility regression.
Track the opt-in validation required to remove this constraint in backlog.

Refs rustfs/backlog#2302.

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

* feat(heal): record bounded canonical object and task outcomes

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

* fix(heal): preserve cancellation and retry only failed listing pages

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

* fix(heal): preserve compatible listing EOF outcomes

Keep truncated heal listings without continuation tokens as complete compatibility EOFs and assert the canonical task outcome.

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

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

* fix(heal): drop test locks before awaits

Limit synchronous mock mutex guards to pre-await scopes in canonical outcome tests.

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 10:50:56 +08:00
GatewayJ 9fc9b5e69c fix(ecstore): align platform and test helper compilation (#7214)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 10:50:48 +08:00
Zhengchao An 61e0edce16 test(odm): pin gcs error classes and require the backend contracts in ci (#7250)
* test(odm): pin gcs source status-to-error-class mapping

The native GCS backend classifies every failure from the HTTP status
alone, because GCS states its error code in a body this backend never
reads. Only NotFound is negative-cached and only a retryable class may be
re-sent, so cover 401/403 -> AccessDenied, 429/503 -> Throttled,
500/502 -> ServerError and 404 -> NotFound over both HEAD and GET.

* ci(odm): require the source-backend contract tests in test-and-lint

The shared contract tests already run in ci/test-and-lint, but only
because gcs is a rustfs default feature; nothing failed if that
selection went away. Pin the S3, Azure and native GCS contracts in the
core required-test manifest so a lost selection fails the lane.
2026-09-06 10:45:09 +08:00
houseme 2b5c739343 fix(scanner): require complete publication coverage (#7176)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* fix(scanner): require complete publication coverage

Refs rustfs/backlog#2261 and rustfs/backlog#2240.

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

* fix(ci): keep s3s footprint ratchet tight

Route bucket list-through test-only S3 wire types through the app storage facade again so the PR does not add a direct s3s-importing file.

Retighten the s3_error! footprint baseline to the current lower count.

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

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

* fix(odm): expose store context after relocation

Expose the ECStore instance context through a narrow accessor so relocated on-demand migration backfill code no longer reaches into private storage fields.

Declare the faster-hex dependency used by the relocated native HTTP source implementation.

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

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

* test(scanner): stabilize usage and ODM regressions

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:43:43 +08:00
Zhengchao An 5dca076efe fix(connect): sync protocol fixture consumers (#7167)
* fix(connect): sync protocol fixture consumers

* fix(connect): preserve enrollment validation order

* fix(connect): satisfy base64 length lint

* fix(connect): restore signature validation order

* fix(connect): preserve signature precedence across chain parsing

* fix(connect): preserve signature error classification

* test(ci): provide log path in workflow harness
2026-09-06 10:12:51 +08:00
RustFS 07833379b4 test(e2e): add distributed cluster regression coverage (#7158)
* test(e2e): add distributed 4x4 validation

* test(e2e): prove operations overlap data movement

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 10:12:06 +08:00
Zhengchao An 6655272c90 fix(ilm): reject invalid retention counts and validate lifecycle filters (#7132)
* fix(ilm): reject invalid retention counts and validate lifecycle filters

`NewerNoncurrentVersions` had no lower bound at PUT, and evaluation read a
negative count through `usize::try_from(...).unwrap_or(usize::MAX)`. An
HTTP-accepted rule therefore retained (almost) everything and silently
stopped expiring versions — the one outcome a retention rule must never
produce by accident.

Reject a negative count during validation, and stop reading one as
"retain everything" anywhere it can still arrive from older persistence
or an import: evaluation takes no action for such a rule and says so in a
diagnostic, the batch limit path yields no event, and `Evaluator::eval`
reports a typed corruption error to callers that can surface one.

A count-only noncurrent expiration is a MinIO extension, not an AWS form.
It used to be rejected as an actionless rule and was never executed. It
is now accepted and honoured with the semantics MinIO gives it: the
newest N noncurrent versions are kept and every older one is due as soon
as it became noncurrent. Zero keeps the meaning the batch limit path has
always given it — no count constraint — so a zero-count rule with no age
condition still has no action.

`LifecycleRuleFilter` is an all-`Option` DTO, so the schema constraints
were not checked anywhere: validate at most one top-level predicate, an
`And` that combines at least two, no repeated tag key, tag key/value
limits, non-negative sizes, and `ObjectSizeGreaterThan <
ObjectSizeLessThan`. An empty filter stays valid — AWS documents it as
"every object in the bucket".

Schema-shape violations are reported with a distinct `ErrorKind` so the
S3 boundary answers them with `MalformedXML`; rejected values keep the
`InvalidArgument` this path has always returned.

backlog#2201

* fix(ilm): satisfy lifecycle clippy checks

* fix(ilm): fail closed on invalid lifecycle rules

* fix: initialize optional migration source fields

---------

Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-06 10:11:05 +08:00
cxymds 0a5d4cef0e fix(tier): drain cleanup before tier removal (#7213)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 10:10:48 +08:00
cxymds 941fae61a0 fix(ilm): persist bounded transition recovery controls (#7200)
* fix(ilm): persist bounded transition recovery controls

* test(ilm): keep expiry sentinel within control range

* fix(ilm): repair recovery control CI regressions

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 10:10:24 +08:00
Zhengchao An 03fa62cc7d fix(admin): keep exporting past unreadable configs and allow explicit target repair (#7247)
* test(ecstore): pin MinIO array-shaped targets blob as unreadable

* fix(admin): mark unreadable configs instead of aborting export

* feat(admin): opt-in replacement of unreadable bucket targets
2026-09-06 10:03:32 +08:00
cxymds b59dea826f fix(ci): enable test utilities in migration gate (#7246) 2026-09-06 01:21:11 +00:00
houseme 081a8b61d8 test(scanner): add bounded ABBA validation harness (#7181)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* test(scanner): add bounded ABBA validation harness

Refs rustfs/backlog#2266 and rustfs/backlog#2240.

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

* test(scanner): harden ABBA threshold evaluation

Handle exact threshold comparisons without binary floating point boundary drift and mark unstable P1 walk controls inconclusive.

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

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

* fix(scanner): reject invalid ABBA evidence and boundary drift

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-06 07:27:46 +08:00
houseme 159dc13548 fix(scanner): bind resumable scans and cache publication coverage (#7210)
* chore(deps): refresh scanner heal batch dependency baseline

Regenerate compatible lockfile selections before the next implementation
batch. Cargo upgrade leaves direct requirements unchanged.

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

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* chore(deps): refresh profiling dependencies for the next batch

Update hotpath and its macro crate to the compatible patch release before
the next dependency-ready implementation tasks.

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

* fix(deps): preserve supported hotpath focus expressions

Keep the profiler runtime before its regex-lite compatibility regression.
Track the opt-in validation required to remove this constraint in backlog.

Refs rustfs/backlog#2302.

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

* fix(scanner): require complete publication coverage

Refs rustfs/backlog#2261 and rustfs/backlog#2240.

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

* fix(scanner): retain scoped partial coverage across dirty plans

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

* fix(scanner): keep stable snapshot rescan behavior

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

* fix(scanner): verify coverage receipts and scan strength

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

* test(scanner): use valid modification times in checkpoint fixtures

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

* fix(scanner): keep maintenance cycles outside dirty bucket scopes

Force complete bucket scope for deep scans and scheduled maintenance while
preserving the existing planner for verified ordinary dirty work.

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

* fix(scanner): refresh scope safety independently of idle backoff

Inspect maintenance on multi-disk startup and refresh changed or failed
evidence even when explicit bitrot configuration disables idle backoff.

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

* fix(scanner): bind bucket cache reuse to scan work requirements

Carry stable scan mode and full-maintenance requirements in the existing
opaque bucket digest before local and remote cache admission. Different
requirements cannot replay a same-cycle Normal cache after root delivery
failure; matching requirements remain reusable for the same intent.

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

* fix(scanner): fence set snapshot reuse with the scan work proof

Prevent same-cycle set publication from replacing freshly scanned maintenance
results with an older Normal aggregate. Recognize uniform completed
maintenance baselines when planning later ordinary dirty-bucket work.

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

* test(scanner): reproduce same-cycle dirty aggregate replay

Cover a Normal-to-Normal retry with a new dirty bucket generation after
bucket persistence and root delivery failure.

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

* fix(scanner): fence same-cycle caches with full activity coverage

Keep structural baseline identity separate from the full activity coverage
required by bucket admission and set publication. Require complete set
coverage proofs while retaining revision CAS and epoch regression checks.

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

* test(scanner): supply explicit coverage in publication fixtures

Keep the confirmed-empty namespace fixture authoritative under the required
coverage contract and qualify the bucket cache metadata test type.

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

* test(scanner): verify joint checkpoint coverage metadata

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

* fix(scanner): satisfy cache prefix sort lint

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 07:27:18 +08:00
Zhengchao An bde4b78a9f test(odm): run multipart race on large stack (#7239) 2026-09-06 04:46:44 +08:00
Zhengchao An c861fe3a57 ci(e2e): install network fault-injection tools (#7244) 2026-09-06 04:37:40 +08:00
Zhengchao An e1608fbd9c test(odm): exercise overflow and invalid cursors reliably (#7236) 2026-09-06 03:09:39 +08:00
Zhengchao An eb1b17802c test(odm): provide the source region in access fixture (#7235) 2026-09-06 02:34:56 +08:00
Zhengchao An 112f70914d fix(build): scope migration helpers to their features (#7234)
* test(odm): keep listing header import test scoped

* fix(build): gate GCS-only migration HTTP helpers
2026-09-06 02:24:04 +08:00
Zhengchao An ea9aa53fd8 docs(odm): record upgrade limits in release notes (#7233) 2026-09-06 02:09:44 +08:00
Zhengchao An dd368f0f5b fix(odm): fence source work against bucket recreation (#7231)
* fix(odm): fence backfill checkpoints by bucket incarnation

* fix(odm): bind source work to the bucket incarnation

* fix(odm): retain checkpoint fences through owned commit tails

* docs(odm): explain application service and incarnation boundaries

* test(odm): probe lifecycle fence after checkpoint waiter aborts

* fix(odm): defer source identity errors past local reads

* docs(metadata): clarify MinIO target recovery limits

* fix(odm): keep source-free reads independent of capture errors

* fix(odm): retain one source policy snapshot across lookup

* test(odm): name recorded metadata hook snapshots
2026-09-06 02:05:43 +08:00
Zhengchao An 6d8606412e fix(odm): compile relocated instance-bound backfill service (#7230) 2026-09-06 01:43:14 +08:00
Zhengchao An 037354cec0 fix(build): declare relocated migration service dependencies (#7229) 2026-09-06 01:37:02 +08:00
Zhengchao An 30ab919bb3 fix(ci): route v1 listing test types through application bridge (#7227) 2026-09-06 01:35:05 +08:00
Zhengchao An 8fc1c9281e refactor(odm): move migration orchestration into application (#7226)
* refactor(odm): move migration orchestration into application

* style(odm): format relocated listing test imports
2026-09-06 01:31:58 +08:00
Zhengchao An 14cef91423 fix(admin): add isolated bucket metadata diagnostics (#7225) 2026-09-06 01:30:18 +08:00
Zhengchao An c9acc33720 test(odm): verify rc5 rollback configuration recovery (#7224)
* test(odm): verify rc5 rollback configuration recovery

* ci(e2e): run the ODM rollback recovery scenario
2026-09-06 01:28:54 +08:00
Zhengchao An 955d491174 feat(build): make native GCS backends optional (#7223)
* feat(build): make native GCS backends optional

* test(odm): cover native Azure runtime credentials
2026-09-06 01:27:36 +08:00
Zhengchao An 1c4e9f1b65 fix(odm): order initial installation against configuration removal (#7222)
* fix(odm): retain removal generation before initial install

* fix(odm): reserve generations only for configured buckets
2026-09-06 01:26:57 +08:00
houseme a9f01dbbdb fix(ecstore): restore odm source contract tests (#7215)
* fix(ecstore): restore odm source contract tests

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

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

* test(ci): initialize replication evidence in chain test

Run the replication workflow's evidence initialization before the chain handoff self-test executes the suite step. This keeps the test model aligned with the workflow-provided LOG_FILE and TMPDIR values.

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

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

* fix(odm): distinguish missing GCS buckets from object misses (#7221)

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-06 01:26:08 +08:00
Zhengchao An 35aefbb2a5 fix(s3): keep ListObjects v1 local during migration (#7220)
* fix(s3): keep ListObjects v1 local during migration

* test(s3): use the v1 listing request DTO directly
2026-09-06 01:25:44 +08:00
Zhengchao An 8fb335cf19 test(e2e): pin upgrade compatibility to rc.5 and cover bucket configuration (#7217)
test(e2e): prove bucket config survives rc.5 upgrade and rollback

Add two upgrade-compatibility scenarios pinned to the on-demand-migration
series' on-disk surfaces: BucketMetadata's 44 -> 46 msgpack keys, the
fail-closed bucket-config reads of rustfs#7172, the encryption-gated PUT
path of rustfs#7183, and the default-on migration module of rustfs#7089.

The upgrade case writes versioning, SSE-S3 default encryption, a validated
replication target plus rule, lifecycle, tags, quota, a public access block,
a bucket policy and an object lock configuration with the pinned previous
release, then asserts each one reads back unchanged on the current build,
that list-remote-targets still reports the target, that writes to the
encrypted and plain buckets keep their encryption posture, that every
pre-upgrade object including a multipart one is byte-identical, and that an
unconfigured bucket reports no migration and still answers NoSuchKey.

The rollback case is the reverse: the current build writes the 46-key blob
and the previous release must decode it by skipping the two unknown keys.
2026-09-06 01:08:22 +08:00
Zhengchao An 8f763fb1a2 fix(ci): run existing script contracts in quick checks (#7203)
* fix(ci): share quick checks and lint workflows

* fix(ci): install actionlint from its verified release

* fix(ci): reject dependencies on required quick checks

* fix(ci): run existing script contracts in quick checks
2026-09-06 00:50:06 +08:00
Zhengchao An a6b5da64f2 fix(ci): serialize performance on shared functional VMs (#7204) 2026-09-06 00:15:03 +08:00
Zhengchao An 1210428b6d fix(ci): publish immutable nightly package candidates (#7202) 2026-09-06 00:14:37 +08:00
Zhengchao An d5426f59ec fix(ci): isolate functional evidence and preserve every result (#7201)
* fix(ci): preserve reported functional suite failures

* fix(ci): isolate functional evidence and preserve every result

* fix(ci): exclude sensitive scratch files from suite artifacts
2026-09-06 00:14:22 +08:00
houseme f54323b062 chore(deps): preserve scanner and heal validation compatibility (#7209)
* chore(deps): refresh scanner heal batch dependency baseline

Regenerate compatible lockfile selections before the next implementation
batch. Cargo upgrade leaves direct requirements unchanged.

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

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* chore(deps): refresh profiling dependencies for the next batch

Update hotpath and its macro crate to the compatible patch release before
the next dependency-ready implementation tasks.

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

* fix(deps): preserve supported hotpath focus expressions

Keep the profiler runtime before its regex-lite compatibility regression.
Track the opt-in validation required to remove this constraint in backlog.

Refs rustfs/backlog#2302.

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

* fix(rustfs): complete list-through source config

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

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 23:50:43 +08:00
hector cc5060ac20 ci(functional): fix dashboard report upload argv overflow and security checkout clobbering (#7212)
Two fixes for the functional test chain:

1. Report upload fails with 'jq: Argument list too long' when the base64
   report is passed through '--arg content' (pool reports exceed the OS
   argv limit; last night's pool run lost its Step Results report this
   way). Write the base64 payload to a temp file and load it in jq via
   --rawfile instead. Applied uniformly to all nine suite workflows
   that share this upload step.

2. The security workflow cloned rustfs/auto-testing into the workspace
   and then ran actions/checkout at the workspace root for the OIDC
   live gate script, which wiped the auto-testing clone and killed the
   suite with 'chmod: cannot access auto-testing/rustfs-security-test.sh'.
   Check out the repository into the rustfs-repo/ subdirectory instead
   and point RUSTFS_SECURITY_OIDC_LIVE_SCRIPT there.

Co-authored-by: rustfs-ci <ci@rustfs.com>
2026-09-05 23:06:28 +08:00
Zhengchao An 188f380b3b feat(ecstore): add native azure blob and gcs migration sources (#7211)
* feat(ecstore): add a native azure blob odm source backend

* feat(ecstore): add a native gcs odm source backend and one backend contract

* fix(ecstore): refuse an empty azure account key at client build

* fix(ecstore): probe gcs sources with the listing permission

* fix(app): drop a redundant match guard on the sse config lookup

* fix(ecstore): drop stale rename commit duplicates from local.rs

* test(ecstore): use the sanctioned placeholder key in the gcs fixture
2026-09-05 22:06:30 +08:00
Zhengchao An e2a921bc16 fix(storage): harden ODM and scanner publication (#7187)
* fix(storage): harden ODM and scanner publication

* fix(app): simplify absent SSE configuration matching

* test(heal): settle PUT rename tails before disk-wipe fixtures

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* fix(ci): satisfy new clippy lints

* style(scanner): order merged test imports

* fix(scanner): invalidate bucket work after namespace completion

* fix(scanner): fence cached snapshots by scan execution

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 13:47:12 +00:00
houseme 447f3c704b feat(heal): add explicit committed MRF snapshot reader (#7179)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* feat(heal): add explicit committed MRF snapshot reader

Refs rustfs/backlog#2263 and rustfs/backlog#2240.

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

* docs(heal): register legacy MRF inspection cleanup

State the compatibility removal condition on the source marker and in
the architecture cleanup register.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 13:33:41 +00:00
Zhengchao An d915f9565e fix(ci): preserve reported functional suite failures (#7199) 2026-09-05 13:26:23 +00:00
Zhengchao An 55ad7508b9 fix(tier): persist coordinator intent before waking refresh (#7171) 2026-09-05 13:24:00 +00:00
Zhengchao An 33fd056000 fix(ecstore): release heal disk snapshot before nested reads (#7189)
* fix(ecstore): release heal disk snapshot before nested reads

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* fix(app): simplify absent SSE configuration matching

* fix(tests): satisfy new clippy lints

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 12:49:40 +00:00
RustFS af2e9df821 fix(lifecycle): correct expiration and transition evaluation (#7169) 2026-09-05 12:42:22 +00:00
cxymds 0a92a7d98c fix(tier): bound remote transition requests (#7147)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 12:41:11 +00:00
Zhengchao An c589fd2439 fix(dev): install a lightweight formatting commit hook (#7198) 2026-09-05 12:22:56 +00:00
Zhengchao An 3e5d4ebb09 fix(ecstore): release multipart disk snapshot before nested reads (#7184)
* fix(ecstore): release multipart disk snapshot before nested reads

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* fix(app): simplify absent SSE configuration matching

* fix(tests): satisfy new clippy lints

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 12:20:50 +00:00
houseme 3677871468 chore(deps): bump zstd to 0.14 (#7173)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 12:16:46 +00:00
Zhengchao An cc1ec6b992 fix(ci): share quick checks and lint workflows (#7194)
* fix(ci): share quick checks and lint workflows

* fix(ci): install actionlint from its verified release

* fix(ci): reject dependencies on required quick checks
2026-09-05 12:03:59 +00:00
Zhengchao An 9e2545244c fix(odm): bound empty pagination chains with staged tokens (#7197)
* fix(odm): add staged cross-request pagination progress budgets

* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* fix(app): simplify absent SSE configuration matching

* fix(tests): satisfy new clippy lints

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 11:44:49 +00:00
Zhengchao An f053862aad docs: request concrete behavior evidence in pull requests (#7196) 2026-09-05 18:56:26 +08:00
houseme e8a7f4bc4a fix(ecstore): remove duplicate local rename implementation (#7190)
* fix(ecstore): remove duplicate local rename implementation

Keep the canonical commit module after concurrent storage changes merged.
The control-write and rollback changes are already present there.

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

* fix(ci): satisfy new clippy lints

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 10:44:01 +00:00
houseme 7ba5cd6888 chore(deps): refresh SDKs and verify clock skew behavior (#7174)
chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 09:06:52 +00:00
houseme acfeef55ab feat(scanner): add bounded incarnation-scoped ACK receiver (#7182)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* feat(scanner): add bounded incarnation-scoped ACK receiver

Refs rustfs/backlog#2265 and rustfs/backlog#2240.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 08:54:55 +00:00
Zhengchao An 0d1b312673 fix(ci): require fresh successful scheduled validations (#7192) 2026-09-05 16:49:58 +08:00
houseme 42c32381b6 fix(scanner): make reset cleanup safely reentrant (#7180)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* fix(scanner): make reset cleanup safely reentrant

Refs rustfs/backlog#2264 and rustfs/backlog#2240.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 08:45:53 +00:00
houseme e6bf2a4646 fix(admin): report partial background heal coverage (#7178)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* fix(admin): report partial background heal coverage

Refs rustfs/backlog#2035 and rustfs/backlog#2240.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 08:45:45 +00:00
houseme cf9688898d fix(heal): retain completed task progress (#7177)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* fix(heal): retain completed task progress

Refs rustfs/backlog#2262 and rustfs/backlog#2240.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 08:39:26 +00:00
Henry Guo 2d159635ed feat(scanner): plan dirty bucket cache refreshes (#7146)
* feat(scanner): plan dirty bucket cache refreshes

* fix(scanner): route peer snapshot through storage boundary

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 16:32:02 +08:00
Zhengchao An 2f02d1d2d8 fix(ci): preserve security suite failures and isolate reports (#7188) 2026-09-05 16:26:13 +08:00
Zhengchao An 8ae8fb7eea fix(ecstore): drain control writes and preserve uncertain rollback (#7163)
* fix(ecstore): drain durable control-plane write tails

* fix(ecstore): retain PUT staging after incomplete rollback

* fix(ecstore): drain backfill checkpoint before confirmation

* fix(ecstore): retain per-disk rename rollback outcomes

* fix(ecstore): retain indeterminate rename recovery evidence

* test(ecstore): mark rollback fixtures as inline data

* test(ecstore): match sealed context fixture map type

* fix(ecstore): preserve known preflight rename rejections

* test(ecstore): cover observed rename outer failures

* test(ecstore): count decommission faults across retry restarts
2026-09-05 08:16:53 +00:00
cxymds bbd7b9ef17 fix(site-replication): bound and order outage recovery (#7148)
* fix(site-replication): wake retry drain after peer recovery

* fix(site-replication): replay configure after bucket make

* fix(site-replication): serialize retry replay state

* fix(site-replication): persist destructive retry intents

* fix(site-replication): bound retry recovery rounds

* fix(site-replication): keep recovery replay live

* fix(site-replication): preserve retry ordering

* fix(site-replication): bound retry coordination

* fix(site-replication): serialize topology replay

* fix(site-replication): fence distributed retry state

* fix(site-replication): bound outage retry drain

* fix(site-replication): drop unsafe delete retry intents

* fix(site-replication): order bucket mutation replay

* fix(site-replication): harden outage retry replay

* fix(site-replication): fence destructive peer delivery

* fix(site-replication): avoid peer edit retry deadlock

* fix(site-replication): fence retry error classification

* fix(site-replication): classify connect timeouts

* fix(site-replication): close recovery review races

* test(site-replication): cover timeout endpoint text

* fix(site-replication): close destructive recovery gaps

* fix(site-replication): fence recovery revisions

* fix(site-replication): replay bucket metadata on recovery

* fix(site-replication): preserve s3gate boundary

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-09-05 15:55:54 +08:00
Zhengchao An 15e9bc5ed0 test(ecstore): count injected faults across retry restarts (#7170)
test(ecstore): count decommission faults across retry restarts
2026-09-05 07:54:56 +00:00
Zhengchao An 882d9ca8a4 refactor(ecstore): isolate metadata quorum decisions (#7165)
* refactor(ecstore): isolate metadata quorum decisions

* test(ecstore): match sealed context fixture map type

* test(ecstore): count decommission faults across retry restarts
2026-09-05 07:46:13 +00:00
cxymds 19a29a7027 test(s3): add Snowball tar-codec compatibility fixtures (#7157)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 07:41:48 +00:00
houseme 2e4ab045b6 test(scanner): add durable checkpoint diagnostics (#7175)
* chore(deps): refresh SDKs and pin clock skew regression coverage

Refresh compatible dependencies for Scanner/Heal V2 batch 1 and verify
the production S3 retry/signing path with a deterministic clock.

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

* test(scanner): add durable checkpoint diagnostics

Refs rustfs/backlog#2260 and rustfs/backlog#2240.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-05 07:34:29 +00:00
Zhengchao An cbfd5b92f4 refactor(ecstore): isolate local object rename commit (#7166)
* fix(ecstore): drain durable control-plane write tails

* fix(ecstore): retain PUT staging after incomplete rollback

* fix(ecstore): drain backfill checkpoint before confirmation

* refactor(ecstore): isolate local object rename commit

* refactor(ecstore): remove moved quota fence import

* fix(ecstore): retain per-disk rename rollback outcomes

* fix(ecstore): retain indeterminate rename recovery evidence

* test(ecstore): mark rollback fixtures as inline data

* test(ecstore): match sealed context fixture map type

* test(ecstore): match sealed context fixture map type

* fix(ecstore): preserve known preflight rename rejections

* test(ecstore): cover observed rename outer failures

* test(ecstore): count decommission faults across retry restarts
2026-09-05 07:19:29 +00:00
Zhengchao An 971f9acdf4 fix(ecstore): reject stalled ODM pagination before merging pages (#7164)
* fix(odm): reject non-progressing listing cursors

* docs(odm): clarify folded source probe pagination

* test(odm): match SDK bucket-root listing requests

* test(ecstore): match sealed context fixture map type

* test(odm): use app facade for listing wire types

* fix(odm): resolve pagination Clippy failures
2026-09-05 07:17:25 +00:00
cxymds a6589c19e3 chore(tier): remove stage-a blanket lint allowances (#7153)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 06:43:05 +00:00
hector 0885c721fe ci(upgrade): support manual runs between any two release versions (#7145)
The workflow_dispatch inputs already accept arbitrary release tags, but
the run failed late and unclearly when a tag had no .deb asset, and the
from_version default pointed at 1.0.0-rc.4-preview.1, whose release
ships no .deb at all - so scheduled runs died on a 404 while installing
the old package.

- Add a fail-fast preflight that resolves each requested tag via the
  GitHub release API and verifies the rustfs_<tag>_amd64.deb asset
  exists before the suite starts, with an actionable error message
  otherwise (e.g. 1.0.0-rc.4 ships only zip/sbom assets).
- Change the from_version default to 1.0.0-rc.3, the newest release
  that actually ships a .deb asset.
- Reword the from_version/to_version descriptions so manual triggers
  state the .deb-asset requirement and the nightly fallback.
- Pass PF_TESTING_GH_TOKEN as GH_TOKEN to the suite step for the gh api
  release lookups, matching the other functional workflows.

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 06:39:47 +00:00
Zhengchao An eaf5159d0f ci: refresh Linux full E2E membership after test additions (#7156)
Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-05 06:27:59 +00:00
Zhengchao An 2477e31059 test(ecstore): require core regressions in the existing CI lane (#7162)
* test(ecstore): require core invariant tests in existing CI lane

* test(ci): require a fresh core JUnit report

* test(ecstore): match sealed context fixture map type
2026-09-05 06:22:48 +00:00
Zhengchao An d8c3b1bb26 fix(app): fail closed on an unreadable bucket encryption config (#7183)
The object write path read the bucket default encryption configuration
with `.ok()`, which made "this bucket has no default encryption" and "the
encryption configuration cannot be read" the same value. A bucket whose
encryption blob is damaged therefore stored plaintext objects the
operator had mandated be encrypted, with nothing returned to the client
and nothing in the object to tell those writes apart afterwards.

PUT, COPY and the snowball extract path now share one resolver: an
absent configuration still writes plaintext exactly as before, and every
other outcome refuses the write, carrying the accessor's typed error so
a damaged blob surfaces as a deterministic InternalError while a
transient metadata read failure surfaces as the retryable
ServiceUnavailable. A missing bucket and a cold metadata cache both
still resolve to "no configuration", so neither becomes a refusal. This
matches `prepare_sse_configuration` in `storage::sse`, the resolver the
multipart writer has always used, which fails closed on this lookup.
2026-09-05 14:13:59 +08:00
cxymds a3b8183be9 test(ecstore): narrow barrier re-export cfgs (#7152)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-05 06:12:45 +00:00
cxymds 4dbc58887a fix(tier): probe legacy transition version state (#7138) 2026-09-05 06:00:14 +00:00
Zhengchao An 123967e729 fix(ecstore): fail closed on an unreadable bucket-targets blob (#7172)
* fix(ecstore): correct sealed-credential test helper parameter type

The helper took a HashMap that nothing imports, so the ecstore test target did not compile.

* fix(ecstore): fail closed on an unreadable bucket-targets blob

An undecodable bucket-targets.json was replaced by an empty BucketTargets,
so every replication target of that bucket disappeared, replication stopped,
and no caller saw an error. A missing secretKey alone triggers it, because
Credentials has no struct-level serde(default).

parse_all_configs now retains the failure instead: the raw bytes stay and the
typed field stays None, which BucketMetadata::bucket_targets_unreadable reads
as "exists but cannot be read" — the same distinction the fabricated marker
draws for bucket metadata as a whole. One corrupt sub-config still never fails
the metadata load, so an unreadable bucket cannot take down its neighbours or
the node.

BucketTargetSys records such buckets and answers every targets query with the
new BucketRemoteTargetsUnreadable, leaving any snapshot from an earlier
readable load in place so in-flight replication is not torn down. The
replication heal queue reports Missed rather than scheduling against an empty
target set, and the admin listing surfaces the fault instead of an empty list.

Refs: rustfs/backlog#2282

* fix(ecstore): report corrupt permissive bucket configs as invalid

Audit of the remaining parse_all_configs branches. Policy, versioning, object
lock and replication already fail closed at their accessors; encryption,
public access block and quota did not, and for those three "absent" is exactly
the state that grants something — plaintext storage, anonymous access,
unbounded capacity. They now report a stored-but-undecodable payload as
invalid rather than as ConfigNotFound, matching the guard the versioning and
object-lock accessors already use. The quota enforcement path already refused
such a payload; only the metadata read path was misreporting it.

The branches left degrading, and the concrete reason each is safe, are
recorded in the table on parse_all_configs.

Refs: rustfs/backlog#2282
2026-09-05 13:02:23 +08:00
Zhengchao An 4b0d597d4d test(ecstore): fix sealed context fixture map type (#7161)
test(ecstore): match sealed context fixture map type
2026-09-05 11:54:24 +08:00
Zhengchao An 13e6424e99 docs(architecture): settle remote credential sealing threat model (#7168)
* docs(architecture): settle remote credential sealing threat model

* docs(architecture): index sealing ADR threat-model scope
2026-09-05 11:20:58 +08:00
cxymds b33693fc19 feat(tier): fence legacy state reconciliation (#7144) 2026-09-05 01:42:24 +00:00
Zhengchao An 9ed1d46090 docs(ecstore): define generation authority and recovery boundary (#7159) 2026-09-05 09:29:00 +08:00
cxymds 6eb60f8e72 feat(tier): add durable probe intent protocol (#7151)
* feat(tier): add durable probe intent protocol

* test(tier): remove redundant intent clones
2026-09-05 09:06:05 +08:00
cxymds 8dd3cabd41 test(ecstore): stabilize transition generation fixture (#7140) 2026-09-05 00:46:19 +00:00
Zhengchao An e648f683bf fix: use BTreeMap for deterministic encryption context serialization (#7154)
SealScope::encryption_context() returned a HashMap whose key order is
non-deterministic. The FakeSealer test round-trips the context through
JSON serialization, and HashMap's random iteration order caused the
prefix comparison to intermittently fail with 'encryption context mismatch'.

Switch to BTreeMap which guarantees stable key ordering.
2026-09-05 07:21:25 +08:00
cxymds 193b1b7d3f test(ecstore): stabilize sealed context encoding (#7149) 2026-09-05 07:18:37 +08:00
hector 3b920c7999 ci: render step results and version in workflow reports (#7141)
* ci(upgrade): render an upgrade matrix in the report; fix from default

The upgrade report only ever showed the requested deb URLs and a case
table. The nightly chain runs died installing the OLD package (default
from_version 1.0.0-rc.4-preview.1 has no .deb asset on its release, and
release 1.0.0-rc.4 ships none either), leaving an empty Total: 0 report
with no indication of what was upgraded.

- Default from_version is now 1.0.0-rc.3 (ships rustfs_1.0.0.rc.3_amd64.deb).
  Matches the auto-testing default from PR #32.
- The report generator also parses the [UPG-TOPO] lines the suite now
  emits and renders an 'Upgrade Matrix' section: per topology and KMS
  backend, the versions actually in place before/after (captured via
  'rustfs --version' on the node) and the aggregated result. When the
  suite dies before any topology completes, the matrix says so instead
  of silently showing nothing.

* fix(ci): use English headers in the upgrade matrix table

* ci(heal,pool): render step results and version in the reports

The heal and pool-expansion reports were only a raw log tail: no
structured indication of which steps passed, no overall verdict, and no
version information for the cluster under test.

The suites now emit machine-readable lines (auto-testing PR):
  [HEAL-STEP] <n> <desc> PASS|FAIL     [POOL-STEP] <n> <desc> PASS|FAIL
  [HEAL-VERSION] <ver> (node <n>)      [POOL-VERSION] <ver> (node <n>)
  [HEAL-RESULT] PASS|FAIL <detail>     [POOL-RESULT] PASS|FAIL <detail>

Both report generators parse them and emit a '## Step Results' section
before the log tail: the version captured in place via 'rustfs --version'
on a node, the overall verdict, and a per-step table. When a run dies
before any step reports (old script or early crash), the table shows a
NOT RUN placeholder row instead of silently showing nothing.
2026-09-05 02:28:14 +08:00
cxymds 5f8b097172 fix(tier): bound distributed mutation latency (#7150) 2026-09-05 02:28:08 +08:00
cxymds 445114577f docs(ilm): approve bounded recovery disposition (#7155) 2026-09-05 02:28:02 +08:00
Henry Guo 923bde6904 feat(scanner): prepare scoped bucket cache scans (#7136)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-09-04 15:09:58 +00:00
cui fliter 10ccf7c31a fix(version): avoid deriving RustFS version from runtime working directory (#7118)
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-09-04 23:03:11 +08:00
Zhengchao An 146003a426 refactor(odm): separate source backends from namespace mapping (#7129)
refactor(odm): isolate read-only source backend contract
2026-09-04 21:40:07 +08:00
cxymds 65ed86f76e test(ecstore): run checkpoint publication test on large stack (#7134) 2026-09-04 12:39:18 +00:00
cxymds 3da3bcd9ee fix(tier): preserve credentials during backend validation (#7127)
* fix(tier): preserve credentials during backend validation

* test(tier): make clone redaction assertion clippy-clean
2026-09-04 12:24:02 +00:00
Zhengchao An 20e4fd7de6 feat(ecstore): add the sealed remote credential seam (#7137)
* feat(ecstore): add the sealed remote credential seam

Replication targets, remote tiers and on-demand migration sources will all
seal their stored secrets through one envelope rather than three
(rustfs/backlog#2168, design in docs/architecture/remote-credential-sealing-adr.md).

Adds the versioned envelope, the seal scope that binds a ciphertext to the
store, owner and field it belongs to, the sealer registration point, and the
fail-closed error type. ECStore still has no rustfs-kms dependency: the binary
installs a sealer the way it installs the event dispatch hook.

Nothing is wired to a consumer yet, so no stored format changes.

* docs(ecstore): name the event dispatch hook by module, not by symbol

The architecture guard keeps EVENT_DISPATCH_HOOK references inside the
event-notification owner module; the module doc cited the symbol only as an
example of the hook shape, so cite its file instead.
2026-09-04 12:11:49 +00:00
houseme da28f8c843 chore(deps): refresh hotpath, mimalloc, and kafka (#7135)
* chore(deps): refresh hotpath and mimalloc

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

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

* chore(deps): bump rustfs kafka crates

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

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

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-09-04 12:10:16 +00:00
Zhengchao An 507447da12 fix(restore): reject SELECT restore and keep typed S3 errors (#7113)
* fix(restore): reject SELECT restore and keep typed S3 errors

RestoreObject accepted `Type=SELECT` requests, but the restore path can
only write the retrieved bytes back to the source key: `put_restore_opts`
built SELECT output options and `restore_transitioned_object` then PUT
them over the source bucket/object. On an unversioned bucket that dropped
`x-amz-restore`, user metadata and tags from the live object; on a
versioned bucket it published a bogus latest version. Nothing was ever
written to `OutputLocation.S3`, yet the response still carried a
fabricated `x-amz-restore-output-path`.

Reject SELECT at the API boundary with a typed NotImplemented, before any
guard or metadata write, and fail closed in `put_restore_opts` as the
backstop for any other caller.

Every other RestoreObject failure was collapsed into a `Custom` error
code, which serializes as a generic retryable 500: a missing key or
version, a malformed version-id, an object that was never transitioned,
an illegal `Days`, and authorization or storage failures all looked the
same to a client. Map them to their S3 identities instead — NoSuchKey,
NoSuchVersion, InvalidArgument, InvalidObjectState, InvalidRequest,
MalformedXML — by preserving `StorageError` through `post_restore_opts`
and letting `ApiError` do the mapping. The intentional 409
RestoreAlreadyInProgress and 503 SlowDown behaviour is unchanged, and
request validation now runs before any lock is taken.

backlog#1341, backlog#2205

* test(restore): give the typed-error regression the ecstore test stack

`execute_restore_object_maps_failures_to_typed_s3_errors` builds a real
ECStore fixture, and under nextest each test runs in a spawned thread with
libtest's 2 MiB stack. On Linux CI that overflowed: the test aborted with
SIGABRT / "fatal runtime error: stack overflow" while every other test in
the run passed.

Add it to the `ecstore-base-stack` filter in both the default and ci
profiles, alongside the other `package(rustfs)` tests that drive the same
store fixture. 4 MiB matches what the deeper multipart and access
roundtrips already use.
2026-09-04 12:04:28 +00:00
cxymds 81014fd233 docs(ilm): approve legacy tier state reconciliation (#7142) 2026-09-04 19:53:36 +08:00
cxymds ff3c5a4989 fix(ilm): drain tier-delete recovery pages (#7133) 2026-09-04 10:46:19 +00:00
cxymds 4d226998e2 fix(ilm): chunk large tier-delete dispatches (#7123)
* fix(ilm): chunk large tier-delete dispatches

* fix(ilm): bound tier-delete chunk dispatch stack use
2026-09-04 10:30:32 +00:00
Zhengchao An f6bed1a73a fix(ci): track nightly test selection per platform (#7128) 2026-09-04 18:25:34 +08:00
Zhengchao An 7ff578ff20 fix(ilm): notify on batch noncurrent version expiry (#7116)
The batch `NewerNoncurrentVersions` expiry path took a lifecycle event
argument and ignored it: after `delete_objects` committed it only evicted
the cache and scheduled replication deletes, so a successful noncurrent
version expiry was invisible to notification subscribers while the
equivalent current-version path emitted a lifecycle expiration event.

Emit that event from the batch path too, reusing the existing lifecycle
audit sink and event contract. Only entries that actually mutated
something are announced, and cache eviction and replication scheduling
keep their existing order and admission — the event is derived from the
committed result and a send failure never rolls back a delete.

"No error" is not enough to prove a mutation: the disk layer skips an
absent version and reports success, so a batch entry for a version that
was already gone came back indistinguishable from a committed delete.
The delete plan already resolves whether the source exists, so carry that
`source_missing` result on `DeletedObject` and let the lifecycle path
stay silent for versions it did not remove.

backlog#2202
2026-09-04 18:25:10 +08:00
Henry Guo f16a30b231 feat(scanner): expose authenticated dirty bucket snapshots (#7122)
* feat(scanner): add peer bucket dirty snapshots

* fix(scanner): keep dirty snapshot errors stable

* test(protos): satisfy dirty snapshot clippy

* fix(scanner): satisfy dirty snapshot clippy

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-09-04 18:23:13 +08:00
Zhengchao An 6f717b2752 fix(filemeta): accept MinIO RFC1123 restore expiry-date (#7115) 2026-09-04 09:02:03 +00:00
cxymds 3654c147e2 fix(ilm): defer aborted dispatch cleanup to recovery (#7126) 2026-09-04 08:49:14 +00:00
Zhengchao An a9373288f2 docs(architecture): decide how remote credentials are sealed at rest (#7131)
Replication targets, remote tiers and on-demand migration sources each store a
remote secret in clear text under .rustfs.sys today. Record one design for all
three before any of them invents its own: seal only the secret fields, into an
added field so an old reader finds the credential absent rather than a
ciphertext it would sign with, unseal at remote-client construction, and reach
KMS through an installed hook because ECStore does not depend on rustfs-kms.

Covers the envelope format and its encryption context, the mixed-version
compatibility matrix, the rollout gate, rotation ownership and the fail-closed
rules.
2026-09-04 16:30:17 +08:00
cxymds fdb28e611f fix: bound tier free-version recovery sweeps (#7125) 2026-09-04 08:12:01 +00:00
Zhengchao An 5f739944a3 fix: resolve clippy warnings in bucket_list_through.rs (#7121) 2026-09-04 14:14:26 +08:00
Zhengchao An 2bc88418f5 fix(list): satisfy clippy in the source listing merge (#7117) 2026-09-04 12:06:34 +08:00
Zhengchao An 9d10d69a6d test(odm): add the scheduled provider interop lane (#7114)
* test(odm): drive the migration cases from an env-named source

The ODM e2e suite only ever migrates from the in-process fake source, so
path-style addressing, region handling, ETag shape and list pagination on
real implementations stay untested. OdmInteropEnv resolves the source from
RUSTFS_ODM_INTEROP_*, seeding into a per-run source_prefix so a shared real
bucket can host concurrent runs and every seeded key is removed afterwards.
A named provider with a missing variable is an error, never a silent
fallback to the fake source.

interop_test holds the four cases that run against either source, and the
e2e-odm-interop profile is the lane that selects them; e2e-full excludes
them, so its committed selection is unchanged. wait_until_odm_engaged
replaces the fake source's journal probe for the readiness wait, since a
real source keeps no journal.

* ci(odm): add the scheduled provider interop lane

on-demand-migration-interop.yml runs the interop cases against a pinned
MinIO container with a 5,000-object backfill - past the fake source's 4,096
version and journal caps - and the three-case minimum against AWS, R2 and
GCS when their ODM_INTEROP_* secrets exist, skipping with a summary note
when they do not. Each provider gets one JSON report merging the per-case
entries with the nextest JUnit, which stays authoritative for what ran.

Report-only and never required: it depends on third-party endpoints and on
secrets a fork does not have.
2026-09-04 10:05:40 +08:00
Zhengchao An 9863f4848d feat(tier): report cluster tier stats and count tier requests (#7110)
`GET /v3/tier-stats` answered from whichever process received the
request, returning that node's rolling 24-hour transition counters as
if they were cluster totals, and the `TierRequestsSuccess` and
`TierRequestsFailure` metric names had no producer at all.

The body now separates the two quantities a tier carries. Stored
inventory comes from the persisted scanner usage snapshot, which is
already cluster-wide; rolling activity is summed over every member
through a new read-only `TierDailyStats` peer RPC. Rings are merged
rather than added, so an idle node's expired hours age out, and each
node counts only its own committed transitions, so a retry is counted
once. Coverage travels with the numbers: `activity.status` names the
reporting members and the ones that could not be asked, timed out, or
answered with a ring this build refuses to merge, and per-tier
inventory is absent rather than zero when the snapshot has no
accounting. The version 1 body stays reachable at `?format=legacy`.

Tier request counters are recorded at the two seams every remote
request passes through, so a new provider is counted by construction,
with a closed operation/outcome label set that can never grow a tier
name, endpoint or object key.

Closes rustfs/backlog#2207

Co-authored-by: cxymds <cxymds@gmail.com>
2026-09-04 09:40:03 +08:00
Zhengchao An 7dfc2ee5f0 feat(odm): merge the source listing into ListObjectsV2 (#7112)
* feat(odm): merge the source listing into ListObjectsV2

Adds policy.list_through: ListObjectsV2 merges the local and source
listings into one ordered page so clients see the whole namespace during
an on-demand migration. Local entries win a key both sides hold,
CommonPrefixes are unioned under a delimiter, and the continuation token
is an opaque versioned envelope carrying both cursors.

A source listing failure or an open breaker follows policy.source_error:
propagate answers 424, not_found answers from local state and marks the
response x-rustfs-on-demand-migration-list: local_only. Source listings
are capped at 10 per second per bucket.

* test(odm): refresh the e2e-full darwin selection digest

The list-through e2e module adds seven cases to the merge lane.
2026-09-04 09:08:35 +08:00
cxymds 80c88a9031 fix(ilm): delete historical null versions by exact identity (#7109) 2026-09-04 08:14:47 +08:00
Zhengchao An 3005efe845 fix(odm): declare source retry policy and time out a stalled inline read (#7111)
* fix(odm): declare the remote client retry policy per consumer

The SDK retry policy was an inherited default: one logical call could cost
three wire requests, so the migration breaker counted logical calls on top
of a threefold amplification against a source that was already failing.

Make it an explicit RemoteS3EndpointSpec field. Replication targets declare
today's standard three attempts and keep their behaviour; the on-demand
migration source and its admin probe declare a disabled policy, so one
counted failure is exactly one source request and pull.rs owns the only
retry budget.

* fix(odm): count a stalled inline source as a source timeout

The inline tee wraps its source body in the idle guard, but the tee turns a
stalled source into an ordinary body read error, so the write-back reported
it as a local write failure. Hand commit_inline the guard so the pull is
counted under source_timeout instead.

The background pump now enforces the idle budget through the same guard
rather than a second copy of the timeout loop.

* test(odm): cover a stalled source body end to end

The fake target can now deliver a GetObject body in slices with a pause
between them, so the inline abort can be driven by a stalled source instead
of a truncated one. Two fault cases drop the workarounds they carried for
the SDK's retries: the scripted fault count and the observed source request
count now have to agree.

The operations guide records the retry and idle-timeout guarantees.
2026-09-04 02:24:53 +08:00
Henry Guo 3a914b429d fix(scanner): preserve explicit cycle retry cadence (#7107)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-03 22:30:46 +08:00
Henry Guo 8a8616b8f7 test(heal): cover background target restart (#7103)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-03 12:36:30 +00:00
houseme 62e66baf89 feat(scanner): add storage seam (#7105) 2026-09-03 20:31:59 +08:00
唐小鸭 53cabe9274 fix(replication): send an integrity header on Object Lock PUTs (#7097)
* fix(replication): send an integrity header on Object Lock replication PUTs

AWS S3, MinIO and most compatible targets reject a PutObject that carries
x-amz-object-lock-* headers unless it also carries Content-MD5 or an
x-amz-checksum-* header. Since rustfs#6895 the replication client sends
plain signed payloads with no SDK checksum, so every replicated object
with a retention period or legal hold failed against such targets.

TargetClient::put_object now decides per request through the pure
rustfs_replication::object_lock_put_integrity: a plaintext single-part
object whose source ETag is its MD5 gets Content-MD5 derived from the
ETag (no body pass, framing unchanged); a multipart-layout ETag, managed
SSE or SSE-C passthrough falls back to an SDK CRC32; a forwarded source
checksum or an unlocked PUT is left alone.

The outbound target matrix flips its two KnownFailing(rustfs#7082) cells
to Completed and every Completed cell now asserts that a locked
PutObject carried an integrity header.

Fixes rustfs#7082.

* test(e2e): keep the matrix expectation table clippy-clean under -D warnings

The CI lint runs cargo clippy --all-targets -- -D warnings. With every cell
green the single-arm match tripped match_single_binding and the unused
KnownFailing variant tripped dead_code, and the target-client tests tripped
field_reassign_with_default. Drive the expectation table from a
KNOWN_FAILING_CELLS constant (so the variant stays live and adding a red
cell is a one-line entry), build the test options as struct literals, and
refresh the e2e-repl-nightly selection digest for the renamed table test.
2026-09-03 20:26:44 +08:00
cxymds 0f272ddb14 docs(ilm): document persistence recovery contracts (#7106) 2026-09-03 20:05:46 +08:00
cxymds 0181a583a6 fix(ilm): recover orphaned restore generations (#7104) 2026-09-03 20:03:51 +08:00
Zhengchao An 0713a723cd fix(odm): close three on-demand migration follow-ups from the e2e matrix (#7101) 2026-09-03 19:40:54 +08:00
cxymds a6cb34c7a4 fix: fence transition transaction recovery (#7095) 2026-09-03 10:38:30 +00:00
cxymds 703766677b fix(ci): provision a pinned client for tier tests (#7098) 2026-09-03 18:01:36 +08:00
Zhengchao An f7c51271ee test(rustfs): run RestoreObject generation-guard test on a large stack (#7100)
The scenario keeps the shared gating ECStore, a live RestoreObject
authorization hook and the RestoreObject usecase future alive at once, which
left the 2 MiB test-thread stack a few KiB from overflowing on Linux; pinning a
single future was not enough. Run the whole scenario through the existing
run_large_stack_test helper instead.
2026-09-03 09:40:05 +00:00
houseme 0e6ee3bf62 feat(scanner): coordinate usage and workload boundaries (#7093)
* test(scanner): wire usage and heal rebuild gates

* docs(scanner): define usage authority protocol

* docs(heal): clarify scanner and ecstore boundaries

* refactor(scanner): split metrics from contracts

* feat(scanner): use shared workload snapshots

* fix(ecstore): recheck capacity before decommission drain
2026-09-03 17:02:43 +08:00
houseme 3ab7a1921f chore(deps): refresh mimalloc and codec usage (#7096) 2026-09-03 16:11:24 +08:00
cxymds 86ebcb325c fix(replication): fence stale metadata status writeback (#7083)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-03 08:05:58 +00:00
唐小鸭 8023cf3e26 test(e2e): add the outbound target matrix and #7082 postmortem (#7092)
test(e2e): add the outbound target matrix and the replication checksum postmortem

Defense work for rustfs#7082, the regression rustfs#6895 introduced while
fixing rustfs#6853: a fix for one target class changed a client default for
every target class and nothing in tree modeled the other classes.

- docs/postmortems: timeline, root cause, why four defense layers missed
  it, and the SOP for changing any outbound client default; AGENTS.md and
  the adversarial compatibility lens point at it; the two env knobs from
  rustfs#6895 are documented in docs/operations.
- fake_s3_target: reject_aws_chunked_uploads, require_checksum_for_object_lock
  (Content-MD5 always verified), create_bucket_with_object_lock with a
  GetObjectLockConfiguration handler, and a TransportSnapshot on every
  journal record.
- replication_target_matrix_test: six object shapes against four target
  modes with an explicit expectation table; the two rustfs#7082 cells are
  pinned KnownFailing and fail with an XPASS message once the fix lands.
  Wired into e2e-repl-nightly, excluded from e2e-full.
2026-09-03 14:57:05 +08:00
cxymds 54a7e9f307 fix(ecstore): stabilize decommission config and retry tests (#7091) 2026-09-03 14:45:26 +08:00
cxymds 5e58b1d3a2 test(ecstore): stabilize subquorum free-version fixture (#7090) 2026-09-03 14:06:11 +08:00
cxymds df30dff1a7 feat(storage): complete Snowball and decommission follow-ups (#7039)
* feat(storage): complete Snowball and capacity follow-ups

* fix(ecstore): clarify V3 capacity gate guidance

* fix(ecstore): keep target contention retryable

* fix(ecstore): preserve typed target lock errors

* fix(ecstore): harden decommission recovery

* fix(ecstore): close decommission recovery races

* fix(ecstore): fail closed on multipart cleanup gaps

* fix(ecstore): model capacity mutation parameters

* fix(ecstore): settle checkpoint capacity retries
2026-09-03 03:58:59 +00:00
Zhengchao An d011ec9952 feat(odm): enable on-demand migration by default (#7089)
* feat(odm): enable on-demand migration by default

The module switch RUSTFS_ON_DEMAND_MIGRATION_ENABLED now defaults to true,
so the feature is reachable without an opt-in; setting it to false still
keeps the module out of the read path entirely. A bucket without an
on-demand-migration.json is never resolved by the runtime and makes no
source call, so the flip changes nothing for unconfigured buckets.

The admin plane now reads the switch through the predicate published by
module_switches.rs instead of its own duplicated env constant; the
behaviour (an environment read per call) is unchanged.

* test(e2e): wire three on-demand migration cases into e2e-smoke

The PR smoke lane gains one case per user-visible contract: a GET miss
that pulls and persists, a HEAD miss that answers from the source and
stores nothing, and the admin config/status pair that must redact the
source secret. The HEAD case did not exist outside the nightly
real-source lane, so it is added to get_basic_test.

Measured on darwin: the lane goes from 168 tests in 101.98 s to 171
tests in 101.92 s, since the three cases overlap the lane's existing
work. The darwin selection digests for e2e-smoke and e2e-full are
regenerated; the e2e-full linux digest still needs a Linux runner.

* docs(changelog): record the on-demand migration feature
2026-09-03 11:40:54 +08:00
Sergey Shlukov 1747ed0292 fix(ecstore): stop scan_dir emitting entries past a limit hit inside a subdirectory (#7049)
* fix(ecstore): stop scan_dir emitting entries past a limit hit inside a subdirectory

scan_dir's flush loop recurses into a pending subdirectory when the
current sibling entry's page limit is reached mid-recursion, but kept
writing the current (later-sorting) entry regardless. gather_results
then builds the next page's continuation marker from that later entry,
which permanently skips the still-unscanned tail of the subdirectory
on resume instead of just deferring it to the next page.

Add a limit re-check right after the flush loop, before the current
entry is written, so scan_dir stops cleanly at the true last-written
key. Reproduces and fixes the rc.5 recursive ListObjectsV2 data-loss
report (7826/7881 keys, contiguous 55-key block silently dropped).

Adds scan_dir_does_not_emit_entries_past_a_limit_hit_inside_a_subdirectory.

* fix(ecstore): re-check the page limit on every dir_stack flush iteration

The flush loop that drains dir_stack can pop and recurse into more than
one pending subdirectory per outer iteration (whenever more than one
stack entry sorts below the current sibling entry). The limit re-check
added in the previous commit only ran once, after the whole flush loop
exited - so if the first recursive scan_dir call already exhausted the
page limit, the loop's next pop+recurse still went ahead and scanned
(and emitted entries for) another subdirectory beyond where the page
was supposed to stop.

Confirmed against production data: a bucket with ~1.17M objects under
one prefix still cut a recursive ListObjectsV2 listing short (825 of an
expected much larger next page, IsTruncated=false) even with the first
fix deployed, at a two-level-nested subdirectory. Move the check inside
the while loop so it runs before every pop, not just once after.

---------

Co-authored-by: Claude Agent <agent@local>
2026-09-03 02:43:58 +00:00
Zhengchao An 74be040c62 feat(ecstore): add the on-demand migration backfill job (#7087)
* feat(ecstore): add on-demand migration backfill job core

Add the background backfill job for on-demand migration
(rustfs/backlog#2159): a durable checkpoint under
buckets/<bucket>/on-demand-migration-backfill.json saved by If-Match
compare-and-set every 1000 keys or 10 s, a 60 s owner lease renewed by
every save, a recovery pass that takes over expired leases (or jobs this
node owned before a restart) and cancels jobs whose config changed, and a
main loop over the source ListObjectsV2 pages with the skip_existing
policy, dry runs, bounded outstanding pulls and wait-on-full enqueueing.

The pull queue gains per-job completion reports so the job can count
pulled/failed keys (hashes only), and pull permits become two-tier so an
online miss is never queued behind a backfill pull.

* feat(admin): expose on-demand migration backfill job

Wire the ODM-12 backfill job (rustfs/backlog#2159) to its operators:
POST /v3/on-demand-migration/{bucket}/backfill?op=start|cancel and
GET .../backfill return the checkpoint document, GET .../status gains a
backfill summary, and the recovery loop plus the process-wide runner are
installed at startup. Backfill control reuses
Set/GetBucketOnDemandMigrationAction and is recorded in the route policy,
the registration matrix and the admin route snapshot.

Add the rustfs-madmin wire types and client methods with golden fixtures
shared by the server tests, the backfill_* metric descriptors and their
collector, and three e2e scenarios: a full backfill across list pages,
cancellation, and resuming from the persisted continuation token after a
server restart.
2026-09-03 08:52:58 +08:00
Zhengchao An df4fdef1d8 docs(odm): add the on-demand migration operations guide (#7086)
* docs(odm): add the on-demand migration operations guide

Positioning against replication, site replication, tiering and the
replication read-proxy; the admin API walkthrough; the full config field
table with defaults and bounds; the semantics table with the test that
pins each row; provider presets and least-privilege source permissions;
integrity, ETag and metadata mapping; protections, error codes,
observability with PromQL, troubleshooting, known limitations and
security notes.

Adds docs/architecture/background-services-inventory.md with the
write-back pipeline, backfill job and backfill recovery loop rows, and
registers it in the architecture index.

Refs rustfs/backlog#2160, rustfs/backlog#2147.

* docs(odm): note migration sources on the replication egress guard

Refs rustfs/backlog#2160.
2026-09-03 08:48:31 +08:00
Zhengchao An 759a395fec test(e2e): cover on-demand migration faults, concurrency and interactions (#7085)
* test(odm): add the fault, concurrency, interaction and real-source e2e

Twenty-one scenarios for on-demand migration: source failures and the
circuit breaker, single-flight and the pull-concurrency limits, how a
pulled object composes with encryption, Object Lock, quota, events,
replication, delete markers and the disable switch, and two cases against
a real second RustFS as the source.

Every assertion carries a source-request-count dimension so a case cannot
pass by serving the client while quietly re-reading the source.

* ci(odm): route the slow on-demand-migration e2e to the nightly lane

The fault, concurrency and real-source modules wait out the 30 s breaker
window, drive 100-deep bursts, or start extra RustFS processes, so they
join e2e-nightly and are subtracted from the e2e-full merge budget. The
e2e-smoke selection is unchanged.
2026-09-03 08:28:16 +08:00
Zhengchao An d04611ba38 feat(object): serve GET misses from the on-demand migration source (#7084)
feat(rustfs): serve GET misses from the migration source

Wire the on-demand migration read-through into the GET path, after the
local read and the replication proxy have both missed (rustfs/backlog#2156).

A source HEAD supplies size, validators and metadata. Conditional headers
are evaluated locally against it and never forwarded, so a source 304/412
cannot be mistaken for a source failure. An object within inline_max_bytes
is teed: the primary streams to the client while the secondary commits the
local copy in a background task, so a client disconnect still stores the
whole object and a failed write-back never touches the client stream.
Range reads and larger objects stream straight through and queue a
background pull per policy. Concurrent misses of one key share the
singleflight slot: the leader tees, followers re-read local after it
commits or degrade to passthrough after first_byte_ms.

Version reads, partNumber reads, anti-loop marked requests and a respected
local delete marker keep their original 404. Source answers carry
x-rustfs-on-demand-migration: source; local hits are untouched, and the
local hit path gains no await or lock.
2026-09-03 07:21:51 +08:00
唐小鸭 37344a84da fix(replication): quote the ETag in the single-PUT size guard message (#7075)
The guard added in #7021 fails a >5 GiB single-PutObject replication up
front instead of streaming the body to a target that must reject it. Its
message asserted a conclusion: "was not written as multipart on the
source ... re-upload it with multipart". That text is only as right as
the transport decision feeding it, and until #7047 that decision was
wrong for multipart objects carrying a full-object checksum. On 1.0.0-rc.5
a 768-part object was misrouted to the single-PUT path, and the new
default-level error line told the operator to re-upload as multipart an
object whose own ETag ended in -768.

State the evidence instead of the conclusion. The message now quotes the
ETag the decision was read from and says what was read from it (no
part-count suffix), so an operator can check the line against the
object's listing. A misroute then reads as a visible contradiction --
a suffixed ETag on a single-PUT line -- and the message says that case is
a transport-selection defect to report, not something to fix by
re-uploading. A missing or empty ETag is printed as <none> rather than
hidden.

The routing itself is already fixed by #7047; this changes only what the
guard says when it fires.
2026-09-03 07:03:49 +08:00
Zhengchao An c1070f1447 fix(ci): refresh Linux full E2E selection (#7069)
Co-authored-by: houseme <housemecn@gmail.com>
2026-09-03 07:03:36 +08:00
唐小鸭 8bf569899a fix(s3): report FULL_OBJECT checksum type for multipart objects (#7060)
* fix(ecstore): persist merged checksum type for full-object multipart

complete_multipart_upload built the object-level checksum record from a
ChecksumType copied before the MULTIPART / INCLUDES_MULTIPART flags were
merged in. ChecksumType::merge takes &mut self, so the merge updated the
local variable while the copy already inside the Checksum struct stayed
behind. The composite branch rebuilt the Checksum from the merged type
and was unaffected; the full-object branch never rebuilt it, so those
flags never reached disk.

rustfs_rio::read_checksums only sets its multipart flag and only emits
the "x-amz-checksum-type" = "FULL_OBJECT" entry inside its MULTIPART
branch, so a full-object multipart object read back as non-multipart with
no type entry, and GetObject and HeadObject answered with no
x-amz-checksum-type header at all where AWS returns FULL_OBJECT.

Hand the full-object branch the merged type instead of rebuilding the
Checksum: the value must stay the running merge produced by add_part,
because hashing the concatenated part digests would yield the COMPOSITE
value, a different number than the one the client sent. The serialization
now lives in multipart_object_checksum_record so both shapes are covered
by unit tests.

Records written by earlier builds carry the bare algorithm type with no
MULTIPART flags and no trailing part block; they keep reading back to the
same checksum value, and the FULL_OBJECT reader arm predates this change
so older peers parse the new record shape correctly too.

Found while root-causing rustfs#6825.

* fix(s3): reject contradicting multipart checksum type as client error

A CompleteMultipartUpload declaring an x-amz-checksum-type that
contradicts the type recorded at CreateMultipartUpload answered 500
InternalError, telling the caller to retry a request that can only ever
fail. The storage layer does refuse the combination, but through a
generic error that maps to InternalError.

Validate the header against the recorded type in the usecase, where the
upload metadata returned by get_multipart_info is already in hand, and
answer InvalidRequest naming both types, matching AWS. The storage-layer
check stays as a backstop for non-HTTP callers.

Uploads created without a checksum algorithm record no type, so there is
nothing to contradict and the header is left alone rather than newly
rejected. Replication is unaffected: replication_put_object_options
already excludes x-amz-checksum-type from the metadata it forwards.

* test(e2e): cover full-object multipart checksum type round-trip

Adds an end-to-end test that a CRC32 FULL_OBJECT multipart upload reports
x-amz-checksum-type: FULL_OBJECT and the unsuffixed full-object value on
both GetObject and HeadObject, and one that a CompleteMultipartUpload
contradicting the recorded type is rejected as InvalidRequest while
leaving the upload intact. Extends the existing CRC64NVME multipart test
with the same checksum-type assertion.

* fix(s3): keep checksum-type validation off the s3s error macro

The s3s footprint ratchet (scripts/check_s3s_footprint.sh) counts
s3_error! invocation lines and is lower-only: new code must route
through the gateway abstractions rather than widen the direct s3s
surface the s3gate migration is shrinking.

Raise the contradiction through ApiError::invalid_request instead. The
response is byte-for-byte identical -- From<ApiError> for S3Error carries
the InvalidRequest code and the message through unchanged -- and the
usecase already returns ApiError elsewhere, so this is the idiomatic
path rather than a way around the counter.

The explanatory comment deliberately says "the s3s error macro" instead
of naming the macro: the ratchet counts raw matches, so spelling it out
in a comment tripped the same check.
2026-09-03 07:03:19 +08:00
Henry Guo 98f7e63396 fix(heal): recover replacement after transient disk errors (#7059)
* fix(heal): recover replacement after transient disk errors

* fix(heal): satisfy replacement status clippy lint
2026-09-03 07:03:01 +08:00
Zhengchao An 07dce1cab0 feat(obs): export on-demand migration metrics and status snapshot (#7081)
* feat(obs): export on-demand migration bucket metrics

Add the on_demand_migration metric subsystem: per-bucket request,
pull, failure, inflight, queue depth, source latency distribution and
breaker state series fed from the ODM runtime snapshot through the
storage boundary, collected alongside bucket replication metrics, and
retired once a bucket's config disappears.

* feat(admin): report the full on-demand migration status snapshot

Extend GET /v3/on-demand-migration/{bucket}/status with provider,
endpoint host, breaker state, runtime counters, last source error,
inflight and queue gauges and the config timestamp. served_by_source_ratio
stays null: no per-bucket GET total exists to divide by. Update the
madmin status type and golden fixture together.
2026-09-03 03:46:28 +08:00
Zhengchao An b727e669db fix(object): route write-back test wire types through the s3 facade (#7080) 2026-09-03 03:38:40 +08:00
Zhengchao An 0fe6cc3641 feat(ecstore): add on-demand migration write-back pipeline (#7079)
* feat(ecstore): add on-demand migration pull queue and write-back pipeline

Background pull queue per bucket (bounded by pull_queue_capacity, concurrency via the state's pull slot), OdmWriteBack/PullSource traits, single-part and multipart write-back with a pumped body that enforces idle timeout, cancel and content length, retry policy for retryable source errors, inline commit helper, and stats accounting (rustfs/backlog#2153).

* feat(object): implement on-demand migration write-back over internal put

OdmWriteBack impl mapping source heads onto InternalPutContext (content-header allowlist, x-amz-meta copy, tags, dual-prefix odm-* provenance, ETag policy), injected into OnDemandMigrationSys at startup; removes the dead-code gates left by ODM-06a (rustfs/backlog#2153).
2026-09-03 02:45:24 +08:00
houseme 5be9d255e6 Fix rustfs-cli e2e target warning (#7073)
* fix: split rustfs-cli e2e entrypoint

* fix: allow e2e enrollment feature without fixtures
2026-09-02 18:21:27 +00:00
Zhengchao An 817adb127c fix(admin): gate the s3 facade auth re-export to tests (#7078) 2026-09-03 02:08:37 +08:00
Zhengchao An e3349f5f30 feat(object): serve HEAD misses from the on-demand migration source (#7077)
feat(object): proxy HEAD misses to the on-demand migration source

Add the ODM HEAD passthrough (rustfs/backlog#2155): after the local lookup
and the replication proxy both miss, resolve the bucket through
OnDemandMigrationSys and answer from the source's HEAD without writing back
or queueing a pull. Versioned reads, requests carrying the source-proxy
anti-loop marker, and a respected latest delete marker never consult the
source; policy.head=local_only, the negative cache and an open breaker
answer 404 locally. Source 404 feeds the negative cache; other source
failures map to 424 SourceUnavailable (class only) or 404 per
policy.source_error, and unsupported source objects always map to 424.
Source answers carry x-rustfs-on-demand-migration: source and omit version,
SSE and replication headers.

The delete-marker probe, request gate, 424 constructor and response marker
live in shared.rs for the GET passthrough to reuse.
2026-09-03 02:08:34 +08:00
Zhengchao An a5bde8b0af feat(admin): add on-demand migration bucket admin API (#7076)
* feat(ecstore): add on-demand migration bucket config model

Introduce OnDemandMigrationConfig (deny_unknown_fields, version 1) with typed validation, credential redaction, a secret-free Debug impl, and the OnceLock publish hook the runtime registers into. Exported through the api facade.

* feat(ecstore): persist on-demand migration config in bucket metadata

Store the config as a RustFS extension entry (on-demand-migration.json) with its update time in .metadata.bin, add the typed BucketMetadataSys accessor, and publish the config through the hook on every cache-install path alongside the durability sync.

* refactor(ecstore): extract shared remote S3 client builder

Move the aws_sdk_s3 client construction out of bucket_target_sys into
bucket/remote_s3_client.rs: endpoint assembly, credential provider,
path-style selection, custom CA / skip-TLS transports and the outbound
SSRF gate now build from a neutral RemoteS3EndpointSpec so replication
targets and the upcoming on-demand migration source client share one
policy. Replication builds its client through From<&BucketTarget>; the
gate keeps its relaxed semantics (private allowed, loopback only behind
RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) verbatim. The builder also
gains optional connect/read timeouts and a User-Agent suffix
interceptor, both unset for replication.

Refs rustfs/backlog#2149

* feat(ecstore): add on-demand migration SourceClient

Add bucket/on_demand_migration/source_client.rs on top of the shared
remote S3 builder: HEAD, ranged streaming GET, ListObjectsV2 with
source-prefix mapping, GetObjectTagging and an admin probe. Every request
carries the x-rustfs-/x-minio-source-proxy-request anti-loop markers and
a RustFS-OnDemandMigration/<version> User-Agent suffix; SSE-C source
objects are rejected as unsupported. SourceError classifies SDK failures
(not found, access denied, throttled, timeout, connect, server error)
with retryability and a stable metrics label. Debug output redacts
credentials.

Refs rustfs/backlog#2149

* docs(operations): point outbound policy at shared remote S3 client builder

* chore: integrate ODM-01 and ODM-02 as B1 base (fix facade merge)

* feat(admin): add on-demand migration bucket admin API

Add the management plane for On-Demand Migration (ODM-07,
rustfs/backlog#2154): PUT/GET/DELETE /v3/on-demand-migration/{bucket},
PUT ?dry-run=true, and a GET .../status skeleton.

- PUT authorizes SetBucketOnDemandMigration, checks the bucket, the
  RUSTFS_ON_DEMAND_MIGRATION_ENABLED switch and the license, validates the
  ODM-01 config against local endpoints and replication targets, probes the
  source with SourceClient::probe(), then persists through the incarnation
  gate and asks peers to reload. Responses carry the redacted config and a
  probe summary; probe failures name only the error class.
- GET answers 404 NoSuchConfiguration when unset; DELETE is idempotent (204).
- New AdminAction variants admin:SetBucketOnDemandMigration and
  admin:GetBucketOnDemandMigration, route policy matrix rows, registration
  and MinIO alias coverage, and a doc row for the extra handler gates.
- rustfs-madmin gains on_demand_migration wire types and client methods;
  golden fixtures under crates/madmin/fixtures/on_demand_migration/ are
  asserted byte-for-byte by both the handler and the client tests.

Anonymous sources still map to a 400 naming source.credentials until the
runtime slice adds the credential-less path.

* refactor(admin): route on-demand migration handler errors through the s3 facade
2026-09-03 01:58:49 +08:00
Zhengchao An a23d4b05a3 feat(ecstore): add on-demand migration runtime OnDemandMigrationSys (#7074)
* feat(ecstore): add on-demand migration bucket config model

Introduce OnDemandMigrationConfig (deny_unknown_fields, version 1) with typed validation, credential redaction, a secret-free Debug impl, and the OnceLock publish hook the runtime registers into. Exported through the api facade.

* feat(ecstore): persist on-demand migration config in bucket metadata

Store the config as a RustFS extension entry (on-demand-migration.json) with its update time in .metadata.bin, add the typed BucketMetadataSys accessor, and publish the config through the hook on every cache-install path alongside the durability sync.

* refactor(ecstore): extract shared remote S3 client builder

Move the aws_sdk_s3 client construction out of bucket_target_sys into
bucket/remote_s3_client.rs: endpoint assembly, credential provider,
path-style selection, custom CA / skip-TLS transports and the outbound
SSRF gate now build from a neutral RemoteS3EndpointSpec so replication
targets and the upcoming on-demand migration source client share one
policy. Replication builds its client through From<&BucketTarget>; the
gate keeps its relaxed semantics (private allowed, loopback only behind
RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) verbatim. The builder also
gains optional connect/read timeouts and a User-Agent suffix
interceptor, both unset for replication.

Refs rustfs/backlog#2149

* feat(ecstore): add on-demand migration SourceClient

Add bucket/on_demand_migration/source_client.rs on top of the shared
remote S3 builder: HEAD, ranged streaming GET, ListObjectsV2 with
source-prefix mapping, GetObjectTagging and an admin probe. Every request
carries the x-rustfs-/x-minio-source-proxy-request anti-loop markers and
a RustFS-OnDemandMigration/<version> User-Agent suffix; SSE-C source
objects are rejected as unsupported. SourceError classifies SDK failures
(not found, access denied, throttled, timeout, connect, server error)
with retryability and a stable metrics label. Debug output redacts
credentials.

Refs rustfs/backlog#2149

* docs(operations): point outbound policy at shared remote S3 client builder

* chore: integrate ODM-01 and ODM-02 as B1 base (fix facade merge)

* feat(ecstore): add on-demand migration runtime OnDemandMigrationSys

Per-node runtime for On-Demand Migration (rustfs/backlog#2152): turns each
bucket's persisted config into a live SourceClient guarded by a three-state
circuit breaker, a TTL negative cache, per-key singleflight, a pull
concurrency semaphore and lock-free counters with a serializable snapshot.

- sys.rs: OnceLock singleton; `apply` installs/rebuilds/removes bucket state
  (config compared by value, counters preserved across rebuilds, old
  cancellation token fired); `publish` is the metadata publish-hook entry
  (sync removal, spawned install, generation-ordered so a slow older install
  cannot overwrite a newer one); `resolve(bucket, key)` judges module switch,
  bucket state, prefix filter, client availability, negative cache, breaker.
- breaker.rs: Closed/Open/HalfOpen with fixed constants (5 failures / 30 s
  window / 30 s open / 1 probe); NotFound resets, AccessDenied is neutral.
- negative_cache.rs: moka sync cache keyed by local key, ttl=0 disables.
- stats.rs: requests_total{op,outcome}, pulled_bytes/objects, pull_failures,
  inflight/queue gauges, log-bucket latency histogram, last_source_error;
  snake_case snapshot pinned by a golden JSON test.
- Anonymous sources surface as a typed `OdmStateError::AnonymousUnsupported`
  until the shared client builder gains an anonymous mode.
- rustfs: `RUSTFS_ON_DEMAND_MIGRATION_ENABLED` module switch (default false)
  published to module_switches and injected into ecstore before bucket
  metadata loads; hook registered at the same point.
2026-09-03 00:51:03 +08:00
Zhengchao An 183b5c9ede refactor(object): extract internal put entry and ODM provenance keys (#7071)
* feat(utils): add on-demand migration provenance metadata suffixes

* refactor(object): extract internal put entry from the S3 PutObject path
2026-09-02 22:56:23 +08:00
Zhengchao An 1ab6405ac9 feat(ecstore): add on-demand migration bucket config model (#7061)
* feat(ecstore): add on-demand migration bucket config model

Introduce OnDemandMigrationConfig (deny_unknown_fields, version 1) with typed validation, credential redaction, a secret-free Debug impl, and the OnceLock publish hook the runtime registers into. Exported through the api facade.

* feat(ecstore): persist on-demand migration config in bucket metadata

Store the config as a RustFS extension entry (on-demand-migration.json) with its update time in .metadata.bin, add the typed BucketMetadataSys accessor, and publish the config through the hook on every cache-install path alongside the durability sync.

* test(e2e): rename stall timing variable flagged by typos

* test(storage): heap-pin the RestoreObject usecase future in the generation guard test
2026-09-02 22:54:51 +08:00
Zhengchao An 9e0663cbba test(e2e): rename stall timing variable flagged by typos (#7070) 2026-09-02 22:46:28 +08:00
houseme ba20af77bb fix(ecstore): wait for multipart copy readiness (#7065) 2026-09-02 14:31:29 +00:00
houseme 2231633ae1 fix: preserve protocol service account claims (#7062) 2026-09-02 14:16:15 +00:00
houseme 2e2bc814b1 fix(scanner): align usage floor compat marker (#7064)
fix(scanner): accept bootstrap marker on timeout fence
2026-09-02 14:03:04 +00:00
Zhengchao An 1dd81cf276 feat(rio): add bounded tee reader for one-read two-consumer paths (#7066)
Add `tee_reader` / `tee_reader_with_options` in `rustfs-rio`: a
`TeePrimary` that drives the source and a `TeeSecondary` that observes
an identical copy of every chunk through a byte-bounded queue. The
primary returns `Pending` when the queue is full, so both sides advance
at the pace of the slowest consumer; it is meant for small objects only.

Termination: source EOF and errors propagate to the secondary with the
same `io::ErrorKind`; dropping the secondary turns the primary into a
pass-through; dropping the primary early fails the secondary with
`BrokenPipe` by default, or hands the remaining source to a background
drain task bounded by `max_drain_bytes` when
`TeeOptions::drain_on_primary_drop` is set. `TeeSecondary::into_stream`
exposes the queued `Bytes` chunks without an extra copy.

Includes a proptest equivalence test, backpressure, error, drop,
drain-limit and cancel-safety tests, and a criterion bench comparing
tee throughput against a direct read (64 MiB in 1 MiB chunks).
2026-09-02 21:59:46 +08:00
Zhengchao An 01db1f6644 test(e2e): extend fake S3 target as an on-demand migration source (#7068)
* test(e2e): extend fake S3 target as an on-demand migration source

Add ListObjectsV2 paging, Range GET/HEAD, unversioned buckets, standard
and user metadata replay, ResponseStatus/TruncateBodyAt/Stall fault
actions, Range/User-Agent/prefix/continuation-token journal fields,
count_requests, direct seeding, and a configurable object cap to the
programmable fake S3 target, and add the on_demand_migration e2e
harness (OdmTestEnv, admin wrappers, source seeding, local-state
assertions, second RustFS source) with its self-test.

* test(ci): refresh darwin e2e-full selection for ODM harness
2026-09-02 21:59:28 +08:00
Zhengchao An 7e1f261e38 refactor(ecstore): shared remote S3 client builder and ODM source client (#7067)
* refactor(ecstore): extract shared remote S3 client builder

Move the aws_sdk_s3 client construction out of bucket_target_sys into
bucket/remote_s3_client.rs: endpoint assembly, credential provider,
path-style selection, custom CA / skip-TLS transports and the outbound
SSRF gate now build from a neutral RemoteS3EndpointSpec so replication
targets and the upcoming on-demand migration source client share one
policy. Replication builds its client through From<&BucketTarget>; the
gate keeps its relaxed semantics (private allowed, loopback only behind
RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) verbatim. The builder also
gains optional connect/read timeouts and a User-Agent suffix
interceptor, both unset for replication.

Refs rustfs/backlog#2149

* feat(ecstore): add on-demand migration SourceClient

Add bucket/on_demand_migration/source_client.rs on top of the shared
remote S3 builder: HEAD, ranged streaming GET, ListObjectsV2 with
source-prefix mapping, GetObjectTagging and an admin probe. Every request
carries the x-rustfs-/x-minio-source-proxy-request anti-loop markers and
a RustFS-OnDemandMigration/<version> User-Agent suffix; SSE-C source
objects are rejected as unsupported. SourceError classifies SDK failures
(not found, access denied, throttled, timeout, connect, server error)
with retryability and a stable metrics label. Debug output redacts
credentials.

Refs rustfs/backlog#2149

* docs(operations): point outbound policy at shared remote S3 client builder
2026-09-02 21:59:11 +08:00
houseme 99f85ca2b1 fix(scanner): recover fenced incomplete usage floors (#7055)
* fix(scanner): recover fenced incomplete usage floors

* fix(scanner): validate legacy usage floor shape

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-09-02 12:32:31 +00:00
cxymds afc66b7182 fix(ilm): enqueue committed tier free versions (#7041)
* fix(ilm): enqueue committed tier free versions

* fix(ilm): stabilize causal cleanup CI coverage

* test(ilm): make expire GET race deterministic

* test(ilm): synchronize expiry with active GET
2026-09-02 11:08:28 +00:00
唐小鸭 922552083f fix(replication): keep multipart objects on the multipart transport (#7047)
A 6 GiB object uploaded to the source as a 768-part multipart upload was
replicated to a generic S3 target with a single PutObject, and the target
rejected the body with EntityTooLarge. No CreateMultipartUpload was ever
issued, so the multipart replication transport never ran for the object
it exists for.

`replication_put_object_options` seeded the transport from
`object_info.is_multipart()` and then overwrote it with the second
return value of `decrypt_checksums`. Those two booleans do not mean the
same thing: the first is the object's storage shape, read from the ETag,
while the second reports whether the stored *checksum record* carries
per-part data. A full-object checksum -- what `aws s3 cp` writes by
default for a CRC algorithm -- is serialized with no MULTIPART flag even
on a multipart upload, so the record reports false and the object was
routed as a single PUT. `decrypt_checksums` documents this in
object_api/types.rs: callers that need routing must consult
`is_multipart()`. Replication did the opposite.

Route on the object's own shape, and let the checksum record only add
multipart-ness, never take it away. Objects already stored with such a
record are fixed too: the ETag was always right.

This also repairs the diagnosis of rustfs#6825, where the single-PUT
5 GiB guard fired against an object that was multipart all along and
told the operator to re-upload it as multipart.

Tests cover the three shapes the router has to separate: a multipart
object with a full-object checksum record (the regression, which fails
without this change), a multipart object with a composite record, and a
single-part object that must not be promoted onto multipart.
2026-09-02 18:22:32 +08:00
唐小鸭 32eb116cbc fix(ecstore): report unreachable bucket-delete residue at error level (#7048)
DeleteBucket answers from a raw per-disk residue scan rather than from a
listing, so it can refuse for a reason no S3 request can observe: the
client drains every version the API will show, DeleteBucket still returns
BucketNotEmpty, and the client-visible message is the generic "The bucket
you tried to delete is not empty" for every blocker kind.

The server does know which residue blocked it, and where — that is what
`bucket_delete_blocked` carries. But it was emitted at `debug`, below
both the `error` DEFAULT_LOG_LEVEL and the `info` the CI s3-tests lane
runs at, so it was never actually written down. An intermittent
BucketNotEmpty in that lane leaves a server log with no trace of the
refusal at all, which is not a diagnosable state: confirmed against the
artifact log of a failing run, where the rejected bucket appears only in
span-close lines and the blocker event is absent entirely.

Split the blocker kinds by whether the client can still reach the
residue. A visible version or a tier free-version is an ordinary 409 —
the bucket really is not empty and the caller can list and delete what is
left — so that stays at `warn`. UnknownXlMeta, OrphanDirectory, and
DiagnosticBudgetExceeded are on-disk state no S3 request can remove; that
is a server-side integrity problem and is now reported at `error`, with
the blocker kind, the residue counts, and the sample path.

This does not change what DeleteBucket accepts or rejects, and does not
retry or suppress anything — it makes the existing diagnosis reachable.

Refs #7005, #7010
2026-09-02 18:22:20 +08:00
houseme 68f47b9219 fix(s3): preserve s3s v0.16 compatibility (#7052) 2026-09-02 16:45:47 +08:00
houseme de7422b508 fix(server): align vhost domains with s3s port matching (#7051) 2026-09-02 07:55:30 +00:00
cxymds 1bbfa71b11 fix(ecstore): preserve buckets after pool expansion (#7040)
* fix(ecstore): preserve buckets after pool expansion

* fix(ecstore): scope bucket operations by erasure set

* fix(ecstore): preserve bucket metadata load errors
2026-09-02 15:33:55 +08:00
Zhengchao An c066faf07a fix: update stale docs/README.md references to docs/architecture/README.md (#7053)
fix(docs): update stale docs/README.md references to docs/architecture/README.md

The file docs/README.md was removed in a previous commit but references
in AGENTS.md, ARCHITECTURE.md, and CLAUDE.md were not updated. The
expanded check_doc_paths.sh now catches these stale references.
2026-09-02 15:30:42 +08:00
Henry Guo ca8bbbf8f3 test(heal): cover target endpoint recovery (#7046) 2026-09-02 05:51:23 +00:00
houseme bc789332b6 chore(deps): refresh s3s and smallvec (#7043)
Update the workspace s3s pin and refresh the lockfile with cargo update/upgrade.

Remove the unused lifecycle url dependency reported by cargo shear.

Tighten the s3s footprint ratchet to the current observed baseline.
2026-09-02 05:04:35 +00:00
cxymds b422d1fea9 fix(ecstore): make publication part matching bijective (#7037)
* fix(ecstore): make publication part matching bijective

* test(ecstore): persist opaque retry etag
2026-09-02 03:00:11 +00:00
cxymds 87bc9d14ea fix(ecstore): defer zero-evidence delete diagnostics (#7036) 2026-09-02 01:50:39 +00:00
Zhengchao An fddaeba247 fix(ci): pass repository to preview release cleanup (#7038) 2026-09-02 09:10:09 +08:00
cxymds b46a8164f8 fix(http): drain request bodies after early responses (#7019)
* fix(http): drain request bodies after early responses

* fix(http): log early response body drain failures

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-02 01:09:55 +00:00
hector b1faaafb1f fix(ci): repair chain handoff scripts broken by ${{VAR}} expressions (#7034)
PR #7023 rewrote the handoff retry scripts with shell parameter
expansions collapsed into Actions expression syntax: ${GH_TOKEN:-},
${{attempt}}, ${{DISPATCHED:-0}}, ${{TITLE}} etc. GitHub parses
${{...}} as workflow expressions, and bare identifiers are invalid
there, so all seven shared-VM suite workflows (upgrade, s3-compat, kms,
tier, storage, heal, pool-expand) were rejected as invalid workflow
files on main.

Symptoms since 2026-09-01 23:11 +0800 (bba9347):
- every push to any branch produced 'failure' runs with no jobs
  ('This run likely failed because of a workflow file issue')
- the nightly functional chain dispatched rustfs-chain-upgrade at
  17:08Z but the event was silently dropped: zero repository_dispatch
  runs for all eight shared-VM suites overnight (only performance,
  whose file was untouched, ran)
- the workflows API listed them by path instead of name

Fix: restore the shell expansions (${VAR}, ${VAR:-default}); quote the
expected-event name without legacy backticks; render the markdown fence
via printf so shellcheck can parse the block. actionlint and YAML
validation now pass clean on all eleven rustfs-*.yml workflows.
2026-09-02 08:36:13 +08:00
Zhengchao An 0a975f2fe2 docs(knowledge-base): prune stale content and add agent-facing index (#7035) 2026-09-02 08:26:59 +08:00
Zhengchao An ceeff52229 docs(swift): align README feature lists with router and handler wiring (#7033) 2026-09-02 08:08:37 +08:00
Zhengchao An e04e15aed1 ci: bump repo-visuals-action to v1.3.1 (#7032) 2026-09-02 08:08:07 +08:00
Zhengchao An afa84fa988 docs(readme): refresh feature and status matrix (#7031) 2026-09-02 08:07:51 +08:00
Zhengchao An 7aaed4d67b chore(agents): move mimocode skills into .agents/skills (#7030)
Relocate issue-triage and pr-review from the tool-specific .mimocode
directory into the shared .agents/skills tree that every agent already
reads (AGENTS.md, .claude/skills symlink), and ignore .mimocode/ so a
local copy never gets recommitted.
2026-09-01 23:17:06 +00:00
hector 36e07e104a ci(functional): add replication suite (bucket + site) as chain finale (#7026)
- New RustFS Replication Test workflow (rustfs-replication-test.yml):
  standalone workflow_dispatch (suite selector bucket/site/all) and
  repository_dispatch rustfs-chain-replication; runs on the shared
  smoke-testing runner under the shared functional concurrency group.
- Suite never fails the workflow (continue-on-error): failures are filed
  as redacted issues in rustfs/backlog (deduped per run) and the report is
  uploaded to rustfs/dashboard functional-reports/replication/<date>.md.
- Security now hands off to Replication, making it the tenth and final
  link: upgrade -> s3 -> kms -> tier -> storage -> heal -> pool ->
  security -> replication (performance stays parallel on pf-testing).
- Depends on rustfs/auto-testing#27 (rustfs-replication-test.sh).

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-02 07:08:33 +08:00
Henry Guo b03804566c test(heal): cover coordinator restart during rebuild (#7027)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-09-02 07:06:16 +08:00
Zhengchao An 40a2470feb fix(s3): align encrypted checksums and multipart completion (#7025)
* fix(s3): align encrypted checksum handling

* test(s3): align multipart SSE-C completion

* fix(ecstore): scope startup helper to tests
2026-09-01 17:35:49 +00:00
Henry Guo 7dcfdb3320 fix(heal): preserve automatic replacement recovery status (#7018)
* fix(heal): preserve automatic replacement recovery status

* fix(heal): admit unformatted replacement targets

* fix(heal): preserve replacement heal set scope

* fix(heal): attach scoped replacement targets

* fix(heal): preserve replacement heal set scope

* fix(ecstore): keep startup helper test-only

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-09-01 17:21:43 +00:00
cxymds 397dbcf102 fix(ecstore): reconcile pending capacity before exact delete (#7016)
fix(ecstore): reconcile capacity before exact delete
2026-09-02 00:25:55 +08:00
cxymds 99073938ae perf(s3): bound Snowball archive decoders (#7022) 2026-09-02 00:25:29 +08:00
唐小鸭 d22991f33b fix(replication): surface failed objects at the default log level (#7021)
Replication could fail an object with nothing in the server log an
operator could act on. Every failure branch in the resyncer is quieter
than `error` on purpose — most sit on the hot path and fire once per
object per ARN — but `DEFAULT_LOG_LEVEL` is `error`, so on a stock
deployment a failed object produced no line at all. Raising those
branches to `warn` (#6840) did not close this: the default filter still
dropped them.

Report the terminal outcome instead of the branches. `replicate_object_
with_outcome` and `replicate_delete_with_outcome` now emit one `error`
per failed (object, target) once the per-target results are merged,
carrying the object key, version id, target ARN and endpoint, and the
target's own error, redacted through `sanitize_resync_error_detail` so
an echoed credential cannot reach the log. Volume is bounded by objects
that actually fail rather than by attempts inside a transfer.

Also state the single-PutObject size limit instead of discovering it at
the target. Replication picks its transport from the source object's
storage shape, not its size, so an object written with one PutObject
replicates with one PutObject however large it is — and S3 caps that at
5 GiB. Such an object could never reach a generic S3 target, and only
found out after streaming the whole body. `replication_single_put_size_
error` fails it up front with a message naming the size, the limit, and
the remedy.

Version-identity drift moves to `error` on a 10-minute per-ARN throttle.
It was `warn` deduped once per ARN per process, so the one line
explaining why a purged version is still on the target was both filtered
out by default and gone for good after it first fired.

Fixes #6825
Refs #6822
2026-09-02 00:25:09 +08:00
唐小鸭 194c8643c0 fix(admin): report real peer health in site replication status (#7024)
`build_metrics_summary` emitted a single metric entry for the local
deployment with `online` hardcoded to `true` and `last_online` stamped
with the current time, so `mc admin replicate status` reported "I am
online" rather than whether the remote site was reachable. A peer could
be down for minutes with replication failing while the status page
stayed green, leaving operators with no signal that the link had
dropped.

Emit an entry for every peer instead, deriving `online` from the
`reachable_peers` set the handler already computes by probing each peer,
and take `total_downtime`/`last_online` from the replication heartbeat's
existing `EpHealth` tracking. Node-local replication counters stay on
the local entry so a two-site cluster does not double-count its own
traffic.

The new `BucketTargetSys::endpoint_health` accessor deliberately does not
call `init_hc`: unlike `is_offline` it must not create health entries as
a side effect, or merely rendering the status page would mark an unknown
peer online.

Failure counters (`Errors`) are unchanged and still read zero; that is a
separate defect in the bucket-level statistics path and is not addressed
here.
2026-09-01 16:23:01 +00:00
houseme 5720c5c748 fix(ecstore): bootstrap verified MinIO adoption metadata (#7020) 2026-09-01 23:15:41 +08:00
cxymds 1941189499 ci(tier): isolate per-run evidence (#7017) 2026-09-01 23:11:28 +08:00
hector bba934723a ci(functional): retry chain handoffs and alert on stall (#7023)
The repository_dispatch handoff step was continue-on-error with a single
attempt: if the call failed (token lacking contents:write, transient API
error), the chain stalled silently while every job stayed green.

Each handoff now retries 3x and, if all attempts fail, files an alert
issue in rustfs/backlog with the exact recovery command before exiting 1
(still continue-on-error, so suite workflows themselves never fail).
2026-09-01 23:11:25 +08:00
唐小鸭 cebe57a2f0 fix(admin): keep site region out of empty IDP comparison (#7015)
`local_idp_settings` stamped the site region into the reported OpenID
settings whenever the federated identity service was published, which it
is even with OpenID disabled and no provider configured. The add
preflight compares those settings verbatim, so two sites in different
regions could never be paired: `replicate add` failed with `IDP settings
mismatch` while both sites reported an identical, empty `identity_openid`
config.

Report the empty OpenID settings when no provider is configured, so the
region only qualifies real provider identities, and name the diverging
field in the rejection instead of emitting a bare mismatch. Scalar values
are echoed; nested objects and credential-derived leaves are reported by
presence only.

Fixes #7003
2026-09-01 22:41:24 +08:00
hector 6a8a8a1eaf ci(functional): reliable chain driver, heal-once, backlog issues, clone retry (#7013)
Problem: the nightly functional chain has not completed end-to-end.
Evidence from recent runs:
- workflow_run events are fire-and-forget: after KMS finished at 17:09Z
  on 8/31 no tier run was created; rustfs-storage-test.yml has never run.
- 'if: conclusion == success' gates skip downstream suites on any
  failure (security was skipped after pool failed on 9/1 01:48Z).
- rustfs-pool-expand-test.yml embedded a heal pass without
  continue-on-error, so a heal failure failed the whole workflow.

Fixes:
- Add rustfs-functional-chain.yml: entry point that dispatches the first
  suite via repository_dispatch; each suite hands off to the next with an
  explicit, re-drivable API call instead of workflow_run triggers.
- Split heal out of the pool workflow (renamed to RustFS Pool Expansion
  Test): heal now runs exactly once per chain, in rustfs-heal-test.yml
  (storage -> heal -> pool).
- Every suite job gets continue-on-error so a failing test never fails
  the workflow; failures are filed as issues in rustfs/backlog (report
  + redacted log tail) and the chain moves on.
- Clone rustfs/auto-testing with the PF token via 'gh repo clone' plus a
  5-attempt retry loop (transient clone failures aborted whole suites).
- Stop rewriting functional/index.html from every suite (divergent
  copies raced each other with stale SHAs); the canonical index now
  lives in the dashboard repo.
- Standalone workflow_dispatch runs are unchanged and never forward the
  chain; performance runs on its own runner, dispatched in parallel.
2026-09-01 21:08:58 +08:00
GatewayJ 833cc51534 feat(s3-select): stream JSON document input (#6980) 2026-09-01 20:17:14 +08:00
唐小鸭 43450df589 fix(ecstore): keep degraded objects listable when drives are offline (#7010) 2026-09-01 20:16:57 +08:00
唐小鸭 394394cdfc test(ecstore): deflake early-ack PUT fixtures in set_disk ops (#7009)
Six set_disk::ops tests failed non-deterministically only under
concurrent full-suite load, rotating between runs while each passed in
isolation. All six share one root cause: a lock-owning put_object
quorum-acks once the rename fanout reaches write quorum and lets a
detached tail task finish the lagging disks, so a fixture that inspects
per-disk state immediately after PUT can observe a disk the tail has not
reached yet.

The two heal report fixtures, the inline-commit fixture, and the
transaction-fencing fixture read or delete physical shards right after
PUT, and hit FileNotFound on a lagging disk. The two metadata-cache
fixtures prime the cache after PUT, and the read fanout refuses to publish
a cache entry while any disk still reports an error, so the priming read
observably published nothing.

Keep every affected setup PUT on the full-fanout commit path with
no_lock: true, following the existing precedent in this module, so PUT
returns only after every disk has committed. The option only governs lock
acquisition, so it does not weaken what any of these fixtures assert; the
transaction-fencing gate in particular is driven by the fleet proof and
env vars, never by the lock option. Where a fixture also depends on cache
publication, re-prime until the current generation is observably cached
instead of asserting on a single read that a loaded host can stall past
the cache TTL. The heal race fixture's shard damage injection is
best-effort by construction, so it now skips injection when the previous
round's tail still lags rather than unwrapping a read that may
legitimately race.

No production code changes, and no retries or sleeps added.
2026-09-01 20:12:43 +08:00
唐小鸭 af896dc427 test(ecstore): remove host and load dependencies from flaky suites (#7008)
* test(ecstore): retain final decommission capacity snapshot override

take_decommission_capacity_info_override_for_test used to pop the queue
to exhaustion, after which get_decommission_all_pool_capacity_infos
silently fell back to the host's real statfs numbers. Any new sampling
point added to the decommission start paths re-introduced that host
dependency and broke tests on some dev machines (#6989 patched one
instance by topping up snapshot counts, but the coupling remained).

Keep the final queued snapshot and replay it for every subsequent
sample so tests always observe injected capacity once an override is
installed. All existing injection patterns (single snapshot, repeated
identical snapshots, decreasing sequences ending at the post-operation
state) keep their semantics.

* test(ci): serialize load-sensitive heal and cache-generation tests

Under a heavily parallel nextest run (~792 ecstore tests), two tests of
set_disk::ops::heal::heal_result_report_tests failed nondeterministically
per round (different members each time; all 29 pass standalone). Every
test in the module builds a TempDir-backed 4-disk hermetic erasure set
and drives MiB-scale writes plus deep-scan heal: under load a single
disk's IO can fail while write quorum still holds, flipping per-disk
readback and aggregate-outcome assertions. The module's #[serial]
markers do not serialize across nextest's process-per-test boundary.

Verification also caught complete_multipart_generation_retires_cached_snapshot
failing once under the same load; it and its object.rs sibling carry
#[serial(metadata_cache_invalidation_probe)] and assert
get_object_metadata_cache generation semantics - the same shape that
forced the transition matrix tests into the serial group.

Add both families to the ecstore-serial-flaky test-group in the default
and ci profiles. Preventive serialization only, no retries. Three full
parallel rounds after the change: 792/792 passed each round.
2026-09-01 12:00:38 +00:00
Zhengchao An 297ff4688c feat(license): add entitlement provider abstraction (#7006) 2026-09-01 19:31:35 +08:00
唐小鸭 b9b2aa0b76 fix(ecstore): retry rename preparation on a pruned parent (#7005)
A completed multipart upload's staging cleanup prunes empty parent
directories up to the volume root, which removes shared prefixes such as
`data-movement/` and the per-object `<sha>/` while a concurrent
new_multipart_upload builds its destination chain below them. The writer
holds a descriptor to the pruned component, so its next handle-relative
mkdirat fails NotFound. Because rename never retried NotFound, the cleanup
fan-out failed several disks in the same window and broke write quorum.

Give rename preparation its own retry rule: a NotFound is retried once per
component below the base directory, so a rebuilt walk outlasts a pruning
walk, which removes ancestors monotonically upward and stops at the base.
A destination whose parent is the base keeps NotFound terminal, so
speculative cleanup renames still fail fast, and the base is only ever
opened, never created, so a genuinely missing base still fails. The rename
itself keeps its own budget and its unchanged NotFound-is-terminal rule.
2026-09-01 19:31:19 +08:00
唐小鸭 1dcdfe4817 build(make): resolve a Python 3.11+ interpreter for guard scripts (#7004)
scripts/check_test_wiring.py and scripts/check_security_coverage.py import
tomllib, which landed in Python 3.11. macOS ships /usr/bin/python3 at 3.9, so
`make pre-commit` failed on a clean machine with `ModuleNotFoundError: No
module named 'tomllib'` in test-wiring-check, even though the checkers
themselves are fine.

Add scripts/python_bin.sh, which resolves an interpreter (explicit
RUSTFS_PYTHON, then python3.14..3.11/python3/python on PATH, then a
`uv run --python 3.12 --no-project` fallback) and execs it, failing with the
concrete remediation when nothing usable exists. Route the Make call sites
through RUSTFS_PYTHON_BIN. CI workflows keep calling python3 directly because
their runners already provide 3.11+.
2026-09-01 19:09:36 +08:00
cxymds 6e26769265 fix(ecstore): make transitioned cleanup crash-safe (#6978)
* fix(ecstore): fence transitioned object cleanup

* fix(ecstore): address ILM recovery review findings

* fix(ecstore): complete crash-safe tier cleanup recovery

* test(ecstore): avoid typo false positive

* fix(ecstore): stabilize decommission error buckets

* fix(ecstore): stabilize transition delete validation

* fix(ecstore): resume authorized tier delete dispatch

* fix(ecstore): satisfy feature clippy
2026-09-01 19:09:22 +08:00
唐小鸭 bd66fa9dca test(e2e): poll for listing convergence after rolling upgrade restarts (#6997)
The mixed-version rolling upgrade suite asserted a single list_objects_v2
snapshot seconds after restarting a node. Peers keep a restarted node's
drive in Suspect/Returning for ~probe_interval(2s) x success_threshold(3),
and while one drive is excluded the strict listing quorum (write quorum,
3 of 4) drops objects that were themselves legally written at 3/4 during
an earlier node's identical post-restart window, under-counting the
listing (observed as 254 vs 258 in CI) even though every object still
GETs correctly. Replace the snapshot asserts with a bounded convergence
poll; a real upgrade data-loss regression still fails after the deadline.
2026-09-01 18:43:09 +08:00
cxymds 1aea7541c8 fix(release): normalize development package versions (#6994)
* fix(release): normalize development package versions

* ci: build only the rustfs release binary

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-01 18:32:16 +08:00
唐小鸭 9e6d34785b test(ecstore): deflake inline fanout gate assertion under load (#6992)
test(ecstore): assert inline fanout gate on deterministic scheduled metric

non_inline_data_read_early_stop_does_not_add_inline_fanout_on_unequal_layout
compared disk_call_counters::KIND_READ_VERSION totals between the two-phase
read-plan gate being off and on. That counter records inside each spawned
fanout task, so the single-pending inline hedge read races the early-stop
abort_all(): whether the hedge task gets its first poll before cancellation
decides a 4-vs-5 count per read. Under concurrent nextest load the two reads
can disagree (reproduced locally at ~5% when run beside one other test,
matching the CI failure on PR #6961).

Assert on the rustfs_io_get_object_metadata_fanout_scheduled histogram
instead, which records the scheduling decision synchronously in the fanout
loop and is deterministic, using the CapturingRecorder + current-thread
runtime pattern already used by the neighboring tests in this module.
2026-09-01 18:31:35 +08:00
cxymds 03aecc5c3e fix(heal): avoid pool metadata lock recursion (#6991) 2026-09-01 18:31:21 +08:00
cxymds 45fe54e389 fix(admin): align tier backend error contract (#6988) 2026-09-01 18:31:07 +08:00
cxymds 2ed5c297ac fix(deps): update mysql_async to 0.37.1 (#7007) 2026-09-01 18:30:10 +08:00
Zhengchao An 23ab078c56 fix(ecstore): reclaim stale object prefixes (#6974) 2026-09-01 10:09:00 +00:00
cxymds 80c629bfe0 test(ci): refresh e2e full selection manifests (#6983) 2026-09-01 09:26:13 +00:00
houseme 47304cc68d feat(build): allow overriding release version (#6998)
Allow release builds to set RUSTFS_BUILD_VERSION at compile time while keeping the existing tag, short commit, and package-version fallback when the variable is unset or empty.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-09-01 17:08:46 +08:00
cxymds cee84561e7 fix(heal): preserve resumable retry signals (#6995) 2026-09-01 16:55:57 +08:00
Zhengchao An b09ce8e6b5 docs: make pre-pr validation conditional (#7001) 2026-09-01 16:48:21 +08:00
cxymds a45951260a test(ecstore): stabilize activation race capacity (#6989) 2026-09-01 16:05:04 +08:00
houseme a41134eb8a chore(deps): update s3s and Rust MSRV (#6990)
* chore(deps): update s3s and Rust MSRV

* fix: silence startup IAM test hook warning
2026-09-01 07:35:08 +00:00
cxymds 35ce8cdb80 test(tier): enforce structured two-topology gate (#6985) 2026-09-01 14:12:11 +08:00
houseme 0b1a588da5 fix: adapt object metadata to s3s DTO changes (#6981) 2026-09-01 13:04:23 +08:00
cxymds f6c6736a01 docs(deps): expand tokio-tar cleanup contract (#6984) 2026-09-01 12:07:07 +08:00
houseme ab44ae7e83 fix(scanner): add supported usage state reset (#6972)
Add an authenticated scanner usage-state reset endpoint that publishes a fenced bootstrap marker for full rebuilds instead of requiring operators to delete usage metadata by hand.

Guard the reset with the scanner leader lock, storage publication epoch, and per-slot revision preconditions, and make startup resumable across stale cleanup leftovers while still rejecting newer conflicting usage state.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-09-01 01:51:46 +00:00
Zhengchao An b0256e3453 test(e2e): cover mixed-version rolling upgrades (#6975) 2026-09-01 07:09:00 +08:00
Zhengchao An 14a77f9d79 fix(ecstore): supplement split latest listings (#6977) 2026-09-01 07:08:25 +08:00
Zhengchao An 436a1be899 ci: allow macOS release builds to finish (#6976) 2026-09-01 05:01:23 +08:00
houseme 1ea1dfa0a1 fix(put): honor bucket default SSE in path selection (#6970)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-09-01 03:43:39 +08:00
Zhengchao An c45a8c35c4 test(ecstore): cover disk metric sequence snapshot (#6872) 2026-09-01 03:43:26 +08:00
唐小鸭 4932d1dedf fix(admin): allow re-pairing non-empty sites in site replication add (#6961)
The add preflight unconditionally rejected any topology with data on
more than one site, which made `replicate remove` a one-way door: a DR
cluster whose sites both hold data could never be re-paired, and the
only way out was wiping one side by hand.

Admit a multi-non-empty add when every bucket held by more than one
requested site is provably safe to merge through the existing
backfill/resync convergence: versioning must be Enabled on every holder
(so a same-key object from the peer lands as another version instead of
replacing the only copy) and object-lock enablement must match (lock
cannot be toggled after bucket creation). Incompatible adds are still
rejected, now with the operator recovery steps (empty one side, re-run
replicate add, resync) instead of a bare refusal. Bucket configs that
fail to decode fail the preflight closed.

A committed add now also clears this site's own half-finished
pending_remove, mirroring the join receiver (rustfs/rustfs#5963);
otherwise the reconcile tick would replay the stale removal against the
freshly re-paired peer and dismantle the new pairing.

Refs rustfs/backlog#2070
2026-09-01 01:21:06 +08:00
cxymds 041af14143 perf(s3): bound snowball member imports (#6945)
* fix(s3): harden Snowball extract error boundaries

* fix(s3): close Snowball extract compatibility gaps

* fix(s3): verify Snowball request body completion

* test(s3): reject forged Snowball streaming signatures

* build(deps): pin Snowball archive parser limits

* fix(s3): preserve Snowball trailer and member errors

* docs(architecture): register Snowball tar fork cleanup

* refactor(s3): route Snowball errors through object boundary

* ci(deps): allow pinned tokio-tar source

* fix: align Snowball archive codec detection

* fix(s3): harden Snowball codec compatibility

* fix(s3): preserve Snowball codec compatibility

* test(zip): align yield wake assertion with Tokio

* fix(rio): preserve legacy large-block reads

* fix(zip): accept blank tar numeric fields

* fix(s3): align Snowball member import semantics

* fix(s3): authorize PAX legal-hold conditions

* refactor(s3): preserve Snowball error boundary

* fix(iam): support legal-hold policy conditions

* perf(s3): bound Snowball member imports

* fix(s3): preserve bounded Snowball import contracts

* fix(s3): preserve bounded import invariants

* fix(s3): route Snowball limits through app boundary

* refactor(s3): keep bounded import errors behind facade

* fix(s3): satisfy Snowball feature lint gates

* fix(s3): preserve Snowball safety guardrails
2026-08-31 16:45:32 +00:00
houseme e44007012b fix(scanner): recover legacy usage floor from backup (#6964)
* fix(scanner): recover legacy usage floor from backup

Allow scanner usage-floor startup and leadership fencing to use a valid legacy backup when the legacy primary read fails with a corruption-shaped error.

Keep v2 primary read failures, stale metadata, transient I/O, and missing or invalid backups fail-closed.

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

* fix(scanner): cover legacy backup fencing gaps (#6966)

* fix(scanner): recover legacy usage from valid backup

* fix(scanner): recover legacy usage floor from backup

Allow scanner usage-floor startup and leadership fencing to use a valid legacy backup when the legacy primary read fails with a corruption-shaped error.

Keep v2 primary read failures, stale metadata, transient I/O, and missing or invalid backups fail-closed.

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

* fix(scanner): cover legacy backup fencing gaps

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Henry Guo <marshawcoco@gmail.com>
2026-09-01 00:01:38 +08:00
Zhengchao An e3ca1ca54c chore(release): prepare 1.0.0-rc.5 (#6968) 2026-08-31 23:54:35 +08:00
cxymds ec0a65703a fix(s3): align snowball member semantics (#6944)
* fix(s3): harden Snowball extract error boundaries

* fix(s3): close Snowball extract compatibility gaps

* fix(s3): verify Snowball request body completion

* test(s3): reject forged Snowball streaming signatures

* build(deps): pin Snowball archive parser limits

* fix(s3): preserve Snowball trailer and member errors

* docs(architecture): register Snowball tar fork cleanup

* refactor(s3): route Snowball errors through object boundary

* ci(deps): allow pinned tokio-tar source

* fix: align Snowball archive codec detection

* fix(s3): harden Snowball codec compatibility

* fix(s3): preserve Snowball codec compatibility

* test(zip): align yield wake assertion with Tokio

* fix(rio): preserve legacy large-block reads

* fix(zip): accept blank tar numeric fields

* fix(s3): align Snowball member import semantics

* fix(s3): authorize PAX legal-hold conditions

* refactor(s3): preserve Snowball error boundary

* fix(iam): support legal-hold policy conditions
2026-08-31 15:18:28 +00:00
hector 0d1e40ee73 ci: add storage engine workflow to functional test chain (#6971) 2026-08-31 23:05:15 +08:00
houseme 281e40f1cc fix(scanner): fit usage persistence within publication lease (#6967)
Lower the default scanner cache save timeout so the derived usage persistence budget stays inside the effective distributed publication lease window.

Add focused regressions for the default publication budget and bootstrap-pending observational baselines, and update operator docs with the new default.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 22:58:29 +08:00
houseme 7541bb2c5d fix(ecstore): stabilize decommission capacity retries (#6959)
* fix(heal): retry unavailable recreate targets

* fix(heal): refresh put-file epochs after target restart

* test(e2e): harden heal restart evidence

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

* test(e2e): cancel competing heal before restart

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

* fix(ecstore): complete decommission capacity recovery

* fix(ecstore): stabilize decommission capacity tests

Keep decommission test capacity snapshots deterministic across startup and mutation probes, serialize capacity-ledger entries during retries, and avoid reacquiring a multipart fence already covered by the outer migration fence.

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

* fix(ecstore): satisfy decommission test lint

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

* fix(ecstore): restore free-version decommission owner

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

---------

Co-authored-by: marshawcoco <marshawcoco@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-08-31 22:52:47 +08:00
houseme 25dd879cf4 test(perf): add PUT after-probes and node telemetry (#6965)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 14:28:13 +00:00
唐小鸭 af1ebbfb8e fix(admin): converge site replication IAM deletions and retry backoff (#6962)
* fix(admin): replay recorded IAM deletions in site replication retry drain

An IAM deletion whose peer delivery failed during an outage window was
previously unrecoverable without a manual repair: the collapsed retry
entry carries no body, the snapshot resend cannot express "this entity
no longer exists", and the drain escalated the entry into a permanent
marker. The deleted user kept working credentials on the peer until an
operator intervened — a security exposure (backlog#2071).

Record the verbatim deletion body (user/policy/group-removal/
policy-mapping-clear/service-account) in the persisted state, in the
same transaction as the retry-event upsert. The drain now replays the
recorded deletions before the snapshot resend — snapshot-after ordering
restores any entity recreated locally in the meantime — and settles the
collapsed entry when its whole liability is provably replayed. Entries
that predate recording, merge with legacy rows, or overflow the
per-peer record cap keep the escalation semantics: only explicitly
recorded deletion events are ever replayed, never a cross-site diff.

The peer apply handlers become idempotent for deletion shapes (missing
policy/group/member tolerated, matching the existing user-delete
tolerance), so a replayed deletion that already converged settles
instead of wedging the drain. The IAM change hook now attempts every
peer instead of failing fast, so a multi-site broadcast books a retry
entry (and deletion record) for each unreachable peer rather than only
the first. Repair success and peer removal clear the affected peer's
records alongside the entries they accompany.

* fix(admin): probe recovered peers to lift retry drain backoff

A bucket created while a peer was unreachable accumulated three failed
deliveries and entered exponential backoff (2400s and up, capped at a
day). After the peer recovered, the reconcile tick's drain kept
skipping the entry until the backoff elapsed, so the site stayed
diverged — NoSuchBucket resync noise on the source, missing bucket on
the peer — for up to 24 hours with nothing else driving convergence
(backlog#2071, round-four R1.6).

Split reachability from replay: the drain now probes each peer whose
replayable backlog is held back only by backoff (one cheap devnull POST
per peer per tick) and promotes the backlog when the peer answers, so a
recovered peer converges at the next 600s tick. A failed probe advances
nothing — retry counts only move on real delivery attempts, keeping the
exponential schedule intact for a peer that is genuinely down. The base
backoff still floors re-attempts against a reachable peer that keeps
rejecting a delivery. SITE_REPLICATION_RETRY_FAILED_AFTER stays at 3:
the flag is retryStats visibility only, and with the probe in place an
early failed mark is a timely operator signal rather than a dead end.

The drain tick also logs an operator-visible warning whenever the queue
holds failed or escalated entries, instead of backing off in silence.
2026-08-31 22:16:51 +08:00
唐小鸭 3e3eb4d8d5 fix(replication): let replicated version purges pass the peer WORM gate (#6960)
A replicated version purge reaches the peer without the governance
bypass header, so a GOVERNANCE-retained version deleted on the source
with x-amz-bypass-governance-retention was rejected by the peer's WORM
deletion gate forever: retryStats ended at a permanent failed count and
the sites stayed diverged (issue #6850).

The source is authoritative for such a purge: the same WORM gate
already ran there, and GOVERNANCE retention with an authorized bypass
is the only lock state it can purge through. The peer's commit-time
deletion gate now treats an authorized replication delete addressed to
an explicit version as carrying that judged bypass, reusing the same
trust judgment as the replication write exemption
(ObjectOptions::replication_request, set only after the handler
authorized ReplicateDeleteAction). COMPLIANCE retention and legal hold
keep blocking replicated purges, and a plain client delete without the
bypass header stays rejected.
2026-08-31 22:16:38 +08:00
cxymds 48b6548988 ci(pool): add stage-aware expansion diagnostics (#6963) 2026-08-31 13:55:31 +00:00
cxymds 655f6ae452 fix(s3): align Snowball codec compatibility (#6943)
* fix(s3): harden Snowball extract error boundaries

* fix(s3): close Snowball extract compatibility gaps

* fix(s3): verify Snowball request body completion

* test(s3): reject forged Snowball streaming signatures

* build(deps): pin Snowball archive parser limits

* fix(s3): preserve Snowball trailer and member errors

* docs(architecture): register Snowball tar fork cleanup

* refactor(s3): route Snowball errors through object boundary

* ci(deps): allow pinned tokio-tar source

* fix: align Snowball archive codec detection

* fix(s3): harden Snowball codec compatibility

* fix(s3): preserve Snowball codec compatibility

* test(zip): align yield wake assertion with Tokio

* fix(rio): preserve legacy large-block reads

* fix(zip): accept blank tar numeric fields
2026-08-31 13:09:51 +00:00
Henry Guo 61821a6f3e fix(heal): resume remote rebuilds after target restart (#6941)
* fix(heal): retry unavailable recreate targets

* fix(heal): refresh put-file epochs after target restart

* test(e2e): harden heal restart evidence

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

* test(e2e): cancel competing heal before restart

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

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 19:39:47 +08:00
hector 896781a52b feat(ci): add upgrade compatibility suite and reorder functional chain (#6950)
New RustFS Upgrade Test workflow (SUITE: upgrade) runs first in the
nightly functional chain:

- Nightly GNU Build -> Upgrade -> S3 -> KMS -> Tier -> Pool/Heal -> Security
- S3 compatibility now triggers on "RustFS Upgrade Test" completion, so an
  upgrade regression gates the rest of the chain.
- Security suite moves to the end, after pool/heal, on the shared VMs.
- The upgrade suite drives auto-testing's rustfs-upgrade-test.sh
  (UPG-101..402): seed golden data/identity/config on the OLD deb, upgrade
  in place to the NEW deb, verify byte-identical preservation, and publish
  functional-reports/upgrade/<date>.md.
- Add the Upgrade tab to every dashboard index writer so the shared
  functional/index.html stays consistent.
2026-08-31 19:32:07 +08:00
hector 612dd38fea ci(kms): add enforcement/frame/config-secret lane inputs (#6946)
New backlog#2024 KMS supplements (KMS-106..502) are gated behind node env
flags. Add workflow_dispatch inputs that append the corresponding
KEY=VALUE lines to /etc/default/rustfs via the suite's --extra-env option:

- enforce_sse_key_policy -> RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY (KMS-401/402)
- frame_v2               -> RUSTFS_ENCRYPTION_FRAME_V2 (KMS-318)
- config_secret          -> RUSTFS_KMS_CONFIG_SECRET (KMS-107)

Nightly runs keep the default local+vault-kv2 lane unchanged.
2026-08-31 19:31:51 +08:00
cxymds ff28b79088 fix(s3): harden Snowball archive extraction (#6942)
* fix(s3): harden Snowball extract error boundaries

* fix(s3): close Snowball extract compatibility gaps

* fix(s3): verify Snowball request body completion

* test(s3): reject forged Snowball streaming signatures

* build(deps): pin Snowball archive parser limits

* fix(s3): preserve Snowball trailer and member errors

* docs(architecture): register Snowball tar fork cleanup

* refactor(s3): route Snowball errors through object boundary

* ci(deps): allow pinned tokio-tar source

* ci(e2e): refresh Snowball smoke selection
2026-08-31 11:18:09 +00:00
唐小鸭 35456bcede test(scanner): serialize tests sharing process-global scanner state (#6940)
Under the cargo test fallback (threads in one process), tests that touch
the process-global scanner cycle recovery status or the global usage-save
metrics raced each other and failed randomly in full-suite runs.

Mark all touchers with #[serial] per docs/testing/README.md:
- 22 tests reading or writing scanner_cycle_recovery_status() via
  load_scanner_cycle_state_for_startup / reset_scanner_cycle_recovery
- 24 tests mutating global_metrics() usage-save counters via
  store_data_usage_in_backend*, which raced the existing serial
  test_deferred_usage_save_keeps_last_real_save_metric

No-op under nextest, which isolates each test in its own process.
2026-08-31 18:20:25 +08:00
Zhengchao An 9d4ccb7884 fix(ecstore): finalize decommission capacity recovery (#6955) 2026-08-31 18:09:09 +08:00
Zhengchao An 9a22cb85f3 fix(ecstore): complete decommission capacity recovery (#6949) 2026-08-31 16:53:14 +08:00
Zhengchao An 6c67086d0b fix(ecstore): reserve decommission capacity safely (#6917) 2026-08-31 15:20:09 +08:00
hector ea01cd339c fix(ci): continue functional chain and publish heal/pool reports (#6932) 2026-08-31 15:19:46 +08:00
houseme bb37841362 chore(capacity): update default refresh tuning (#6938)
Align object-capacity refresh defaults with the production-oriented environment values and keep docs, script examples, and tests in sync.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 14:43:59 +08:00
GatewayJ 59a7194d7f feat(s3select): schedule streaming progress events (#6913)
* feat(s3select): schedule streaming progress events

* test(s3select): poll permit release until timeout
2026-08-31 13:36:47 +08:00
GatewayJ f647ada320 feat(table-catalog): update object namespace properties (#6815)
* feat(table-catalog): update object namespace properties

* test(table-catalog): cover object namespace properties

* fix(ci): use admin storage contract in catalog test

* fix(table-catalog): reject corrupt namespace properties
2026-08-31 13:36:23 +08:00
GatewayJ 589a954478 feat(s3select): support compressed CSV and JSON input (#6915) 2026-08-31 13:35:59 +08:00
houseme 1d606e1cf6 perf(ecstore): retry degraded GET with late parity (#6933)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 13:32:46 +08:00
Zhengchao An dc2e25b48c fix(admin): preserve raw XML in metadata backups (#6936) 2026-08-31 13:15:35 +08:00
houseme d690f5d60d test(ecstore): stabilize tier recovery cursor fixture (#6935) 2026-08-31 12:09:19 +08:00
houseme 3eca80e37d test(ecstore): make heal rename fixture deterministic (#6934) 2026-08-31 12:09:01 +08:00
houseme 45a2ccb734 fix(ecstore): recover late parity after exact quorum (#6927)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 03:26:20 +00:00
houseme c876df53f5 fix(ecstore): fence snapshot stream polls on lock loss (#6930)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 02:26:24 +00:00
Zhengchao An 769da6d81f test(admin): prove backup import rollback compatibility (#6929) 2026-08-31 02:19:37 +00:00
Zhengchao An ca46ae9e56 test(ecstore): pin bucket metadata rollback reads (#6928) 2026-08-31 01:48:03 +00:00
Zhengchao An 7df0920c80 test(replication): bind writable paths to DTO fields (#6923) 2026-08-31 00:48:10 +00:00
Zhengchao An c4ac11d22e fix(scanner): persist decommission catch-up debt (#6922) 2026-08-31 08:45:36 +08:00
houseme 602ed2cbcd test(ecstore): add targeted refresh-loss harness (#6924)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 08:45:04 +08:00
hector b6c3108e53 fix(ci): keep functional workflow chain running after failures (#6926)
* fix(ci): isolate s3 compat temp file paths

* fix(ci): use rooted auto-testing s3 temp fix

* fix(ci): follow auto-testing main after temp-path merge

* fix(ci): stabilize tier mqtt bootstrap on shared runner

* feat(ci): publish functional reports and keep workflows non-blocking

* fix(ci): keep functional chain running after failures

* fix(ci): standardize functional workflow cleanup steps
2026-08-31 08:43:28 +08:00
houseme 8ecd8f2520 fix(scanner): preserve cache cycle during usage recovery (#6921)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 00:19:54 +00:00
Zhengchao An e6234d3714 test(ecstore): pin default bucket config bytes (#6920) 2026-08-31 00:03:07 +00:00
Zhengchao An 042a0c3014 docs: register persisted XML compatibility cleanup (#6918)
docs: register persisted XML compatibility
2026-08-30 23:53:02 +00:00
houseme 87333f7b24 test(e2e): exercise cluster volume fault proxy (#6919)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 23:37:34 +00:00
Zhengchao An 9945c67f7e fix(ecstore): supervise decommission worker recovery (#6908) 2026-08-31 06:18:00 +08:00
houseme fca1514aac fix(scanner): recover legacy empty usage floor (#6914)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 06:17:26 +08:00
houseme 47ad69b691 fix(ecstore): fail closed on unverifiable data quorum (#6903)
fix(ecstore): require verification source for degraded GET

Fail closed when reconstruction has only an exact decode quorum, because no surplus source remains to validate the rebuilt data. Cover both erasure engines and the data-shards-only rollout gate.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 21:07:47 +00:00
houseme 489408c0b0 perf(ecstore): reuse prepared Select metadata (#6911)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 20:41:24 +00:00
houseme 1b3744a1da test(perf): align GET attribution harness with backlog 2093 (#6912)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 20:28:28 +00:00
houseme 9244eb36ed test(e2e): route cluster volume endpoints through fault proxy (#6909)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 20:17:16 +00:00
houseme 442298d5f7 test(ecstore): prove in-flight prefetch cancellation (#6904)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 19:17:44 +00:00
houseme be7d35d441 perf(get): release disk permits for buffered bodies (#6906)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 19:10:26 +00:00
唐小鸭 ec1cd606d3 fix(replication): surface object-lock denied purges and back off heal retries (#6900) 2026-08-30 18:59:55 +00:00
houseme 16af688a7a fix(rpc): reject unsigned v2 control mutations (#6905)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 18:17:43 +00:00
唐小鸭 37b23a16da fix(replication): verify replica integrity and default to plain signed payloads (#6895) 2026-08-31 01:43:45 +08:00
houseme 006e9b7d28 test(e2e): cover four-node four-drive cluster topology (#6902)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 17:32:36 +00:00
houseme d214c27583 perf(ecstore): consolidate non-inline read planning (#6892)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 17:15:48 +00:00
GatewayJ 8fd364a99c feat(s3-tables): support object-backed table rename (#6899) 2026-08-31 00:30:49 +08:00
houseme c2d8488728 docs(architecture): reconcile generation contract (#6901)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-31 00:20:39 +08:00
唐小鸭 1370434f3a fix(scanner): unblock quota usage baseline on never-converged sites (#6896) 2026-08-31 00:20:04 +08:00
唐小鸭 5dde2c188c fix(replication): retry failed multipart aborts on bounded backoff (#6897) 2026-08-31 00:19:49 +08:00
houseme 2f9c75d04f perf(ecstore): reuse prepared metadata across pools (#6889)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 16:15:10 +00:00
唐小鸭 9ee7b1221d fix(admin): replicate user secret-key rotation to peer sites (#6893) 2026-08-30 23:32:07 +08:00
Zhengchao An fcc3c7fb6b test(s3): promote passing compatibility cases (#6891) 2026-08-30 21:56:17 +08:00
Zhengchao An 01dc55ee5b docs(security): add unsigned presign header lesson (#6894) 2026-08-30 21:55:47 +08:00
houseme 3d24526704 fix(ecstore): preserve parity reserves for data-only GET (#6888)
fix(ecstore): hedge data-only GET with parity

Route the opt-in data-shards-only lockstep path through the bounded parity race and preserve deferred parity reserves across canceled hedges.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 20:16:32 +08:00
houseme 51532e19fb test(ecstore): cover multipart snapshot overwrite race (#6887)
test(ecstore): cover multipart GET overwrite snapshot

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 12:15:02 +00:00
houseme 931ff60182 test(ci): refresh cluster nightly selection (#6886)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 12:10:07 +00:00
houseme 07212c4e26 perf(ecstore): gate quorum-aware GET early stop (#6885)
* perf(ecstore): add gated two-phase GET metadata reads

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

* fix(ecstore): require data-shard coverage for read plans

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

* perf(ecstore): avoid inline overhead in read plan rollout

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

* perf(ecstore): accept quorum-complete read candidates

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 09:33:35 +00:00
GatewayJ 4932af080b feat(s3select): expand typed JSON source paths (#6864) 2026-08-30 06:46:36 +00:00
GatewayJ d6f9a7c462 feat(table-catalog): vend credentials from LoadTable (#6878)
* feat(table-catalog): vend credentials from LoadTable

* fix(table-catalog): preserve entry-relative metadata paths
2026-08-30 06:33:28 +00:00
houseme 7345b49cf6 perf(ecstore): gate GET metadata timing when metrics off (#6879)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 05:43:08 +00:00
houseme 4753e35035 chore(deps): update flake.lock (#6880) 2026-08-30 13:14:13 +08:00
GatewayJ 96239fc034 feat(s3select): report uncompressed input byte metrics (#6865) 2026-08-30 04:07:20 +00:00
hector b428875bed fix(ci): stabilize tier MQTT bootstrap on shared runner (#6877)
* fix(ci): isolate s3 compat temp file paths

* fix(ci): use rooted auto-testing s3 temp fix

* fix(ci): follow auto-testing main after temp-path merge

* fix(ci): stabilize tier mqtt bootstrap on shared runner
2026-08-30 11:14:28 +08:00
Zhengchao An cf362282f0 fix(test): serialize transition matrix tests under nextest (#6874)
The transition_matrix_tests use #[serial_test::serial] which has no
effect under nextest (each test runs in a separate process). When running
alongside thousands of other ecstore tests, the shared metadata cache
generation counter can race, causing intermittent 'metadata read should
publish the generation under test' panics.

Add both tests to the ecstore-serial-flaky test group in both default
and ci nextest profiles so they run single-threaded.
2026-08-30 10:42:23 +08:00
Zhengchao An b2a2e637a5 fix(ci): refresh Linux full E2E selection (#6875) 2026-08-30 10:42:14 +08:00
cxymds 0c18012442 fix(admin): version remote target credential capabilities (#6876) 2026-08-30 10:42:10 +08:00
houseme ee39e4fccb fix(scanner): own publication mutations through storage drain (#6867)
* fix(scanner): own publication mutations through storage drain

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

* fix(storage): remove unused rename data shim

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 02:39:07 +00:00
houseme 90ab2e24c3 perf(ecstore): reuse local fd metadata snapshots (#6868)
* perf(ecstore): reuse local fd metadata snapshots

Cache the validated shard length beside each reusable descriptor so read hits avoid a repeated fstat while retaining generation and mutation invalidation semantics.

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

* fix(ecstore): pass cached entry to fd cache

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 09:08:44 +08:00
cxymds 21e5b3dc64 fix(ecstore): require durable decommission ledger format (#6871) 2026-08-30 08:47:09 +08:00
cxymds 1e8c8d4cd5 feat(replication): support temporary target credentials (#6860) 2026-08-30 08:44:34 +08:00
houseme ff3ad30f0c fix(scanner): bound publication proof retries on main (#6870)
* fix(scanner): retain completed publication candidates

* fix(scanner): export publication activity helper

* test(ecstore): retain activity snapshot across retries

* fix(scanner): rebase publication proof retry onto main

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

* fix(scanner): resolve publication proof retry conflicts

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

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 22:41:36 +00:00
houseme 47a3f5ef01 perf(ecstore): converge disk metric atomic loads (#6866)
Use the seqlock version as the publication fence and keep payload reads relaxed while validating the final version. This reduces ordering overhead in disk metric recording and snapshot collection without changing the rolling-window contract.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 20:38:59 +00:00
houseme a22fa7461d perf(put): adapt eager threshold to concurrency (#6863)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 20:21:05 +00:00
houseme 814ab5bbf3 fix(ecstore): classify system metadata failures (#6862)
fix(ecstore): classify system metadata volume failures

Preserve retryable quorum errors when system metadata reads or writes encounter missing volumes, and cover the create-bucket data-usage path with regressions.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 19:48:50 +00:00
houseme 498205b7ec fix(ecstore): keep 1MiB GET off mid-size reader (#6861)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 19:39:51 +00:00
houseme c235f7c05d fix(scanner): retain usage across transient peer failures (#6859)
* test(scanner): cover bucket drive guard lifecycle

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

* fix(scanner): recover usage floor from fenced backups

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

* fix(scanner): retry transient activity probes

Retry one failed scanner activity probe after a bounded reconnect when the failure is transport-like or timed out. Keep protocol and response validation failures fail-closed.

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

* fix(scanner): retain post-scan observations

Preserve a complete scanner walk as a non-converged observation when the final activity probe is unavailable. Advance the cycle as partial without acknowledging dirty usage.\n\nCo-Authored-By: heihutu <heihutu@gmail.com>

* fix(scanner): classify publication lease deferrals

Distinguish persistence budget and lease deadline deferrals from unavailable activity baselines, and ensure lease-gate deferrals update usage metrics. Keep the fixed lease gate fail-closed while storage-owned commit scope work remains pending.

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

* fix(scanner): recover usage floor from fenced backups

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

* fix(scanner): preserve publication lease defer reasons

Keep lease expiry and release failures distinct from activity baseline failures so scanner freshness metrics and cycle outcomes identify the publication barrier that blocked progress. Preserve fail-closed behavior.

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

* fix(scanner): reuse recovered usage baseline for publication

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

* fix(scanner): fence legacy usage floor fallback

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

* fix(scanner): use typed activity timeout error

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-30 02:54:20 +08:00
cxymds 64cca79fbb feat(admin): expose remote target credential capability state (#6857) 2026-08-30 00:42:14 +08:00
houseme 759e1041bd feat(nix): add NixOS service module and client package (#6856)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 16:14:44 +00:00
GatewayJ 8055aeb1d4 test(s3select): cover SelectRequest XML root alias (#6820) 2026-08-29 14:14:12 +00:00
GatewayJ 79bd6fa862 fix(s3select): return encryption response headers (#6819) 2026-08-29 19:46:34 +08:00
hector fa0be5d271 fix(ci): split workflows and add perf version reporting (#6848)
* ci: pass package selector to test run steps (fix rc.3 fallback)

* fix(ci): split workflows and add perf version reporting

* fix(ci): enforce strict shared workflow order

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-08-29 19:32:19 +08:00
houseme 78cb142c91 fix(s3): accept empty put without content length (#6849)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 19:31:33 +08:00
hector 5fa3d2a682 ci: pass package selector to test run steps (fix rc.3 fallback) (#6846) 2026-08-29 17:34:04 +08:00
hector fd8ddf0a02 fix: remove redundant --repo flag in preview release cleanup (#6847)
The check_preview_release_workflow.sh script uses exact line matching
(grep -Fxq) to verify the cleanup-preview-releases job contains:

  gh release delete "$preview_tag" --yes

The extra --repo flag is unnecessary in GitHub Actions context since
gh auto-detects the repository from GITHUB_REPOSITORY, and it causes
the Workflow Pin Report check to fail on all PRs.
2026-08-29 17:33:34 +08:00
Zhengchao An e1ea99ff06 fix(s3): return BadDigest for Content-MD5 mismatch (#6842) 2026-08-29 09:00:18 +00:00
唐小鸭 11c6ee42ea fix(kms): restore persisted configuration after restart (#6821)
* fix(kms): restore persisted configuration after restart

* docs(kms): cover the reload route and startup load states

The admin contract matrix pins every dynamic KMS route for the rc and
console handoff, so the new POST /kms/reload needs a row there, and the
reload response reuses the configure snapshot shape rather than adding a
wire type. The observability runbook gains the operator procedure the
reload exists for: telling a load_failed startup apart from a server
that was never configured, and recovering without resubmitting secrets.
2026-08-29 16:21:22 +08:00
hector 9307d2c8a8 ci: make MQTT broker setup deterministic in functional test suite (#6837)
* ci: make MQTT broker setup deterministic in functional test suite

* ci: default functional test suite to latest nightly deb
2026-08-29 15:59:56 +08:00
hector 84c5f2170f ci: upload performance report to rustfs/dashboard reports/YYYY-MM-DD.md (#6843)
* ci: upload performance report to rustfs/dashboard reports/YYYY-MM-DD.md

* ci: update token comment to dashboard

* ci: english-only report metadata in performance workflow
2026-08-29 15:50:46 +08:00
唐小鸭 e009eab4f1 fix(replication): surface failed objects and abort orphaned uploads (#6840)
fix(replication): surface per-object failures and abort orphaned multipart uploads

Replication could mark an object FAILED with no server-log line naming
the object: the target-offline skip paths logged at debug without the
object key, and several failure branches omitted the key entirely. A
failed multipart transfer also leaked its incomplete upload on the
target, since nothing ever aborted it after CreateMultipartUpload
succeeded.

Log the offline skips at warn with the object key (they report the
object FAILED, matching the per-object put_object failure level), add
the object field to the remaining failure branches, and abort the
target-side multipart upload best-effort on any post-create failure
without masking the original transfer error.

Fixes #6825
2026-08-29 15:49:59 +08:00
唐小鸭 ab84c3f5cf fix(replication): keep versionId on version-purge delete replication (#6841)
fix(replication): never mint delete markers when replicating a version purge

Heal/resync/MRF rebuilds of a delete-marker version purge carry
delete_marker: true together with a purge-shaped entry. Passing that flag
straight into replication_delete_remove_options made the target DELETE
omit the versionId (marker-creation semantics), so a generic S3 target
that ignores the internal source-version headers minted a fresh delete
marker on every retry instead of purging one — the marker count on the
target grew monotonically (rustfs#6823).

- Gate marker-creation semantics on the new pure helper
  delete_replication_creates_marker (delete_marker && !version purge) so
  a purge always addresses the exact version.
- Stop falling through to the marker-creation send when the pre-send
  source delete-marker verification fails with a transient error; fail
  the entry instead so the MRF replay / heal scanner retries without
  minting a marker on the target.
- Pin the purge-shape contract with unit tests in
  crates/replication/src/delete.rs.
2026-08-29 15:49:50 +08:00
houseme b5f9cbcee4 fix(heal): bound read-repair object commit locks (#6839)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 15:48:18 +08:00
Zhengchao An af6c229914 fix(ecstore): tier force removal bypasses lifecycle reference check (#6835) 2026-08-29 05:16:58 +00:00
houseme c0155f0dfa fix(logging): bound ECStore debug output (#6809)
Also replace deprecated Atomic::fetch_update calls with try_update so the
current Rust toolchain keeps lint and CI jobs warning-clean.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 04:51:37 +00:00
hector 346388b63c fix: provide cross-repo token for auto-testing checkout (#6831)
The test workflows checkout the private rustfs/auto-testing repository, but
the default GITHUB_TOKEN only has access to rustfs/rustfs, so every checkout
failed with 'repository ... not found' (nightly runs on 2026-08-28).

Pass secrets.PF_TESTING_GH_TOKEN (the existing cross-repo PAT already used
by the performance workflow) to the auto-testing checkout steps in all three
workflows.
2026-08-29 12:00:19 +08:00
cui fliter a56439219f fix(version): do not bump version when HEAD equals latest tag (#6828) 2026-08-29 03:25:05 +00:00
Zhengchao An 0fe41da688 fix(ecstore): document audit/notify KVS divergence and fix auth_token redaction (#6816)
Triages the three divergences backlog#2054 found between the audit and
notify default KVS tables, cross-checked against MinIO upstream
(internal/logger/config.go, internal/config/notify/parse.go):

- webhook: audit's extra batch_size/max_retry/retry_interval/http_timeout
  keys match MinIO's DefaultAuditWebhookKVS byte-for-byte, while notify's
  table matches MinIO's notify DefaultWebhookKVS (which lacks them).
  Intentional, not a copy/paste gap — documented with a doc comment on
  each table instead of changed.
- mqtt: audit's stronger QoS/keep-alive/reconnect defaults have no MinIO
  precedent (MinIO's audit logging has no MQTT target at all), while
  notify's 0/0s/0s defaults match MinIO's DefaultMQTTKVS exactly.
  Documented as an intentional RustFS-original choice, not changed.
- auth_token hidden_if_empty: audit had false, notify had true, with no
  MinIO precedent either way (this KVS version has no per-key hidden
  flag upstream). Fixed audit to true, matching notify and every other
  sensitive key in both files (MQTT_PASSWORD, *_TLS_*). Non-empty tokens
  were already redacted identically on both sides via ends_with("_token")
  pattern matching in config_admin.rs — this only changes how an *unset*
  audit webhook auth_token renders in admin config output (omitted
  instead of shown as an empty value).

Refs rustfs/backlog#2054
2026-08-28 17:34:04 +00:00
houseme 0953f7e912 perf(ecstore): optimize bounded small-object GET paths (#6808)
* perf(ecstore): bound mid-size GET decode buffering

Use a single in-flight decoded stripe for the gated mid-size GET path and avoid its outer synchronization mutex while preserving the general codec reader behavior. Add full, partial, degraded, error, and cancellation coverage for the bounded reader.

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

* perf(ecstore): unify small GET path validation

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

* fix(ecstore): bound mid-size prefetch and preserve gate metrics

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

* perf(ecstore): cache small-object read path plan

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

* perf(ecstore): cache GET path plan and verify wiring

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

* test(ecstore): remove redundant metadata clone

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

* test(ecstore): preserve dual inflight prefetch contract

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

* test(ecstore): make prefetch assertion deterministic

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 17:31:10 +00:00
Zhengchao An 75cd3885f3 fix(ecstore): reject Azure tier storageClass/spAuth instead of silently ignoring them (#6817)
TierAzure.storage_class and .sp_auth round-trip faithfully through the
admin API and on-disk config (ExternalTierAzure encode/decode in
tier.rs), so an operator can configure them, read them back via
ListTier, and never learn they do nothing. They are dropped only at the
WarmBackendAzure construction boundary: the Azure warm backend goes
through the same S3-compatible TransitionClient as every other
provider and has no Azure Blob-native client or Azure AD dependency
(confirmed: no azure_* crate anywhere in the workspace), so neither
field can actually be honored today. MinIO's reference implementation
(cmd/warm-backend-azure.go) treats both as first-class: storage_class
sets the blob access tier on every PUT, and sp_auth is a full
alternative to access/secret-key auth via azidentity, mutually
exclusive with it.

Rather than the larger, riskier options (add a native Azure SDK
dependency and a parallel non-S3 client path, or break the persisted
config format by removing the fields), this closes the silent-failure
gap with the minimal safe fix: TierConfigMgr::add now rejects an Azure
tier config with either field set, before backend construction,
returning ERR_TIER_INVALID_CONFIG with an explicit message instead of
accepting and ignoring. The fields stay in the config type (no format
break); already-persisted tiers with these fields set are grandfathered
in un-rejected (edit does not touch sp_auth or storage_class either).
Full support remains a larger follow-up if ever prioritized.

Also removes TierAzure::is_sp_enabled(), which had zero callers
repo-wide (backlog#2055 flagged this) and would have been misleading
dead weight once this decision was made — reusing it for the new gate
would also have been wrong, since it requires *all three* sp_auth
fields non-empty (&&), while the gate must reject on *any* one being set.

Refs rustfs/backlog#2055

(cherry picked from commit 8d148c4e9b2507a1c5075e3d9513adb8b5851ef5)
2026-08-28 17:12:30 +00:00
houseme 73c9dd4c9d fix(data-usage): preserve cold buckets in partial admin usage (#6811)
Merge newer partial observed usage into the complete authoritative admin baseline instead of replacing the full bucket set.

Keep the merged view partial and non-converged so shared consumers do not treat it as quota-authoritative.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-29 00:27:54 +08:00
Zhengchao An 2040f5aff9 fix(ci): pass --repo to gh release delete in preview cleanup (#6810) 2026-08-29 00:20:46 +08:00
唐小鸭 5104be1d23 fix(ecstore): move conditional PUT lock to commit-time recheck (#6801)
A PUT with HTTP preconditions took the per-object namespace write lock
before ingesting the request body and held it until commit, so any
concurrent read of the same object queued behind client-paced body
ingestion until the 5s acquire timeout and surfaced as 503. Exposed as
a deterministic S3 Implemented Tests gate failure when #6770 routed
1 MB conditional writes onto the streaming path (rustfs/backlog#2074).

Keep a lock-free advisory precondition check before the body for fast
412/404, and evaluate the authoritative check under the put_object
commit lock, reusing the deferred shape data movement already uses.
Reads during ingestion now return the last committed version, and a
precondition invalidated mid-stream fails closed with 412 at commit.
2026-08-28 14:53:05 +00:00
houseme 5ef8b1ce5c fix(tier): harden reference proof and audit output (#6807)
Validate lifecycle tier references through the tier reference proof path, preserve S3 list CommonPrefix XML compatibility, and make GetObject audit completion use real S3 error status codes.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 22:46:00 +08:00
唐小鸭 ce4eca40a6 fix(site-replication): rotate-svc-acct no longer wedges replication (#6793) 2026-08-28 22:13:16 +08:00
Zhengchao An 86b6fecbb4 refactor(ecstore): migrate minio/r2/rustfs warm backends to shared S3 constructor (#6776) 2026-08-28 22:13:03 +08:00
hector 88b43f546f Extend functional test workflow with S3/KMS/tier suites (#6806) 2026-08-28 22:12:21 +08:00
Zhengchao An 2092fbf465 fix(s3-client): rename-align CommonPrefix for tier in-use XML parsing (#6805) 2026-08-28 14:11:04 +00:00
Zhengchao An ed66b0a04d refactor(ecstore): migrate huaweicloud/tencent warm backends to shared S3 constructor (#6775)
refactor(ecstore): huaweicloud/tencent reuse the shared S3 constructor

Migrates the Huaweicloud and Tencent tier warm backends onto the shared
S3-compatible constructor (backlog#2040). Also makes the shared
constructor's outbound-URL validation injectable per provider
(S3CompatibleWarmBackendParams::validate_endpoint) so it can centralize
rustfs/rustfs#6764's SSRF check for the providers that don't need an
exception, while accommodating rustfs/rustfs#6773's RustFS-specific
debug-only loopback opt-in without weakening the other six providers.

Updates scripts/error-other-format-baseline.txt: the one ::other(format!)
call site moves from the two per-provider files into the new shared
call site in warm_backend.rs (net call-site count unchanged).

Refs rustfs/backlog#2042
2026-08-28 13:12:55 +00:00
Zhengchao An 847fbd2a8b test(e2e): restore tier and inline full-suite checks (#6794) 2026-08-28 21:08:30 +08:00
houseme 7eddd1cf83 fix(heal): skip dangling delete grace failures (#6799)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 20:09:45 +08:00
唐小鸭 2437069114 fix(iam): stop stamping the wall clock on policy-less group reads (#6791) 2026-08-28 19:50:24 +08:00
唐小鸭 3b87d61cbf fix(site-replication): send the reverse-reachability probe as POST (#6790) 2026-08-28 19:50:09 +08:00
唐小鸭 eb6b617ca2 fix(sse): diagnose unresolvable encrypted metadata on reads (#6784) 2026-08-28 19:49:58 +08:00
Zhengchao An 64705d7589 refactor(ecstore): migrate aliyun/azure warm backends to shared S3 constructor (#6774) 2026-08-28 19:49:27 +08:00
Zhengchao An 206ef7d086 fix(s3): preserve atomic 1 MiB conditional writes (#6798) 2026-08-28 19:48:55 +08:00
houseme b301834c6d chore(deps): update s3s revision (#6795)
Update the s3s git dependency to 6e7b41252c7ba218a90886f58d297716ddf68acf.

This pulls the upstream SelectRequest XML alias compatibility fix while keeping the RustFS s3s compatibility boundary intact.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 18:28:43 +08:00
Zhengchao An 028be4f604 refactor(heal): migrate mainline throttle to shared ForegroundPressure (#6780)
The heal manager carried its own byte-identical copy of the foreground pressure type and threshold computation that ecstore's data-movement backpressure also carries, so every change to the admission-utilization rules had to be mirrored by hand across two crates. The shared `ForegroundPressure` and `foreground_pressure` added to `rustfs-concurrency` now own that logic, and heal already depends on that crate, so this removes the duplicate without adding a crate edge.

`mainline_throttle_active` keeps the parts that are specific to this call site: the `mainline_throttle_enable` and both-thresholds-zero short circuit that avoids touching the provider at all, the optional-provider unwrap, and the heal-side threshold fields. Everything downstream is untouched — the `reason()` labels `foreground_read_pressure`, `foreground_write_pressure`, and `foreground_pressure` are byte-identical to the removed implementation, so the `rustfs_heal_mainline_throttle_total` reason label and the `heal_mainline_throttle` log fields keep their observability contract.

Refs rustfs/backlog#2049

(cherry picked from commit ec491bcbd8939e5978cd94f9a44cffb70d09fade)
(cherry picked from commit e800f29d6806591689204b3712300800b480beae)

Co-authored-by: houseme <housemecn@gmail.com>
2026-08-28 08:21:27 +00:00
唐小鸭 6f9adb3ad0 docs(kms): reconcile bulk-rekey contract with the shipped sweep (#6783) 2026-08-28 15:21:10 +08:00
Zhengchao An 19c7529d88 refactor(ecstore): migrate data movement backpressure to shared ForegroundPressure (#6779)
refactor(ecstore): use shared ForegroundPressure for data movement

The data movement backpressure module carried its own byte-identical copy of ForegroundPressure, its reason() label mapping, and the foreground utilization computation. rustfs-concurrency now owns that logic as workload::ForegroundPressure and workload::foreground_pressure, so the local copy was a cross-crate synchronization point that could silently drift from the heal-side and admission-side behavior.

Delete the local type and computation and call the shared function instead. The call site keeps what is specific to data movement: the config.enabled short circuit, the optional provider unwrap, and the read/write threshold percentages read from DataMovementBackpressureConfig. The reason() labels emitted into the rustfs_data_movement_backpressure_total metric and the data_movement_backpressure log event are unchanged, as are the existing tests and their assertions.

Refs rustfs/backlog#2048

(cherry picked from commit 6a26e144e06ced53a8dfd1712ab7aa24589646ff)
(cherry picked from commit ab5ab417e80179265c32b22a5e671eac0b9e43ae)
2026-08-28 15:11:08 +08:00
houseme 7951601ae8 perf(storage): optimize small-object GET/PUT paths (#6770)
* perf(ecstore): optimize small-object GET paths

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

* perf(rustfs): optimize small-object request paths

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

* chore(deps): upgrade argon2 and convert_case

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

* fix(ecstore): restore reader hotpath attribution

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

* test(ecstore): cover external mid-size fixtures

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 15:10:33 +08:00
Zhengchao An b87ce6b183 fix(ci): sync Linux full E2E selection (#6787) 2026-08-28 15:01:30 +08:00
Zhengchao An f135583fee refactor(ecstore): migrate notify.rs default KVS tables to shared constructors (#6778)
refactor(ecstore): migrate notify default KVS to shared constructors

The amqp, nats, pulsar, redis, postgres, kafka and mysql default KVS tables in config/notify.rs duplicated the corresponding tables in config/audit.rs literally, leaving seven cross-file sync points that a future default or key-order edit had to keep aligned by hand. Replace those seven table bodies with calls to the shared constructors added in config/target_defaults.rs, passing the notify-side literals where the two subsystems genuinely differ: NOTIFY_REDIS_DEFAULT_CHANNEL for the redis channel and "rustfs_events" for the mysql table.

Key order is part of the admin config contract, so this is a pure restructuring: for all seven tables the ordered key sequence and every key's value and hidden_if_empty flag are unchanged.

DEFAULT_NOTIFY_WEBHOOK_KVS and DEFAULT_NOTIFY_MQTT_KVS are deliberately left untouched. Those two tables really do diverge between audit and notify, so folding them into shared constructors would change runtime behavior; the divergence is tracked separately in rustfs/backlog#2054.

Refs rustfs/backlog#2046

(cherry picked from commit 6df9b53027ef2f0cf9aa7b82ecb2af8c5108f11f)
(cherry picked from commit 14bbf756bea2ee6daacbfdc7d7a452effa649cff)
2026-08-28 15:01:19 +08:00
Zhengchao An 75e605fe87 refactor(ecstore): migrate audit.rs default KVS tables to shared constructors (#6777)
refactor(ecstore): migrate audit KVS defaults to shared constructors

The amqp, nats, pulsar, redis, postgres, kafka and mysql default KVS tables in config/audit.rs duplicated the corresponding tables in config/notify.rs, leaving seven cross-file sync points where a default could silently drift between the two subsystems. Build them from the shared constructors added in config/target_defaults.rs instead, passing in the two literals that are genuinely audit-specific: the redis pub/sub channel (AUDIT_REDIS_DEFAULT_CHANNEL) and the mysql destination table ("rustfs_audit_logs").

Key order, every default value and every hidden_if_empty flag are preserved exactly, since the key order drives the order admin config output lists keys in. DEFAULT_AUDIT_WEBHOOK_KVS and DEFAULT_AUDIT_MQTT_KVS are left untouched: those two tables really do differ from their notify counterparts, so unifying them would change runtime behavior.

Refs rustfs/backlog#2045

(cherry picked from commit 4de580e6d8901485eef268191924e035322d4d4e)
(cherry picked from commit e5e301fa78e7f27ffa85b05cf5e7962301a5ff00)
2026-08-28 15:01:09 +08:00
hector d115f1cbd7 test(pool): abort stale multipart uploads before decommission (#6771)
warp is killed at the write threshold and can leave in-flight multipart
uploads behind. rc.4-preview.1's decommission post-check refuses to
finalize a pool that still contains one (data is already moved, then the
pool is marked failed with 'resolve it before retrying'). Abort any
multipart uploads in the test bucket before starting decommission
(ListMultipartUploads + AbortMultipartUpload via the admin API).
2026-08-28 14:59:21 +08:00
houseme cfaf87360f fix(storage): gate multipart upload part pressure (#6781)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-28 14:57:32 +08:00
Zhengchao An 876f60c1f4 fix(ci): restore tier e2e and locked builds (#6773)
* fix(tier): restore loopback e2e coverage safely

* fix(build): sync scanner dev dependency lock
2026-08-28 12:53:52 +08:00
Halil Teyfik 488af5984c docs: fix Getting Started links (#6782) 2026-08-28 12:42:50 +08:00
GatewayJ 7136062c75 docs(agents): make branch naming identity-neutral (#6772) 2026-08-28 11:44:30 +08:00
Zhengchao An 1585308f0f fix(test): restore #[serial] markers the fallback runner still needs (#6767) 2026-08-28 08:41:36 +08:00
Zhengchao An e388a3ff53 fix(startup): never panic when the system CA bundle is absent (#6769) 2026-08-28 00:34:51 +00:00
Zhengchao An 22741603f5 test(e2e): finish the helper consolidation onto common.rs (#6766)
- common.rs gains an AdminTransport knob (Signed | Awscurl) with admin_execute_at plus three family wrappers: admin_create_user_via, admin_add_canned_policy_via, admin_attach_user_policy_via; the existing admin_create_user now delegates over the Signed transport.
- Deleted the four signed admin request clones in admin_mfa_test, admin_auth_test, reliant/tiering, and inline_fast_path_cluster_test; each keeps a thin local wrapper over common::admin_request so call sites keep their Option<&str> body shape.
- Deduped the notification_webhook signer onto common::signed_request and the webdav_core signer plus its three admin helpers onto the shared _via helpers.
- Consolidated the S3-client-with-credentials builders: admin_auth s3_client_with, existing_object_tag user_client/sts_session_client, bucket_policy_check create_user_client, and the create_user_s3_client copies in group_delete_test and replication_extension_test now delegate to create_s3_client_with_credentials / build_test_s3_config; replication_extension admin_add_canned_policy and admin_attach_policy_to_user route through the _via helpers on the Signed transport.
- The awscurl-gated suites (existing_object_tag_policy, bucket_policy_check, policy/policy_variables) keep going through the external awscurl binary via AdminTransport::Awscurl, preserving their wire behavior.

Part of rustfs/backlog#1846 (cluster 2).
2026-08-28 00:12:45 +00:00
Zhengchao An 3c89c71f66 fix(s3): round-trip null-version delete-marker identity (#6765)
* fix(s3): round-trip null-version delete-marker identity through listing and delete responses

On a versioning-suspended bucket, a null delete marker's identity was lost on the way back to the client at three points (issue #6745): ListObjectVersions advertised the marker's VersionId as the literal nil UUID instead of null; deleting by that id succeeded but the DeleteObjects/DeleteObject response reported the identity as null with no way to correlate it to the request; and the response lacked DeleteMarker/DeleteMarkerVersionId because the marker-ness comparison mixed the client-facing identity (Some(nil)) with the storage identity (None), so the removal also mis-recorded accounting and fired DeleteMarkerCreated semantics on later paths.

- Listing (bucket_usecase, s3_api/bucket, build_list_versions_next_marker) now maps the synthesized nil UUID to the literal null everywhere it reaches the wire, and VersionMarker::parse folds a nil-UUID marker from older listings into VersionMarker::Null so pagination resumes correctly.
- delete_objects normalizes both sides of the marker-ness comparison via delete_file_info_version_id (matching the adjacent explicit_delete_marker admission check) and reports DeleteMarkerVersionId as null for an explicit null-marker removal.
- resolve_delete_version_state reports delete_marker for an explicit-version delete whose target is a delete marker even when the bucket is versioning-suspended, fixing x-amz-delete-marker on the single-object path.
- The DeleteObjects response entry echoes the version identity the request addressed for marker removals, marker-removal accounting no longer records a marker creation, and notification events fire DeleteMarkerCreated only for actual marker creation.

Fixes #6745

* fix(s3): keep null-marker removal write shape undeleted and report marker semantics response-side

The first cut marked the storage delete request deleted for a null-marker removal, which FileMeta::delete_version interprets as the suspended-bucket delete-mints-a-marker write and re-creates the marker just removed. Carry marker-ness to responses via explicit_delete_removed_marker (single path) and a response-only branch flag (batch path) instead, keeping every storage write shape byte-identical to the pre-fix behavior. Adds an embedded end-to-end regression test covering the full issue #6745 round trip.
2026-08-27 23:55:05 +00:00
Zhengchao An db57fabcbd fix(tier): validate outbound URLs for all warm backend providers (#6764)
WarmBackendS3::new already rejects loopback, private, link-local, and
cloud metadata-service endpoints via validate_outbound_url, but the
Aliyun, Azure, Huaweicloud, Tencent, MinIO, R2, RustFS, and GCS warm
backend constructors built their transition clients directly from
conf.endpoint without the same check.

The endpoint comes from the AddTier admin API, gated only by
SetTierAction, which can be a narrower IAM grant than root. Any
principal holding it could point one of these eight tier types at an
internal address (loopback, RFC1918, link-local, or a cloud metadata
IP) and have the server issue authenticated outbound requests to it, a
server-side SSRF vector that the S3 and Wasabi tier types were already
closed against.

Apply the same validate_outbound_url check at construction time for
all eight providers, before any credentials or network client are
built, mirroring the existing WarmBackendS3 pattern. GCS keeps its
default-endpoint behavior when conf.endpoint is empty and only
validates an explicitly configured endpoint.

Add a regression test per provider asserting that a loopback endpoint
is rejected before any backend/network setup, matching the existing
WarmBackendS3 coverage.

Update the error(format!) ratchet baseline: these are one-shot admin
tier-configuration validation errors returned once per AddTier call,
not per-disk I/O errors that flow through reduce_errs quorum
aggregation (backlog#1845), so the new ::other(format!) call sites do
not introduce a quorum-bucketing hazard. They mirror the pre-existing,
already-baselined warm_backend_s3.rs call site.
2026-08-27 23:37:50 +00:00
Zhengchao An 03888bd266 chore(tier): remove dead trailing_headers config from warm backends (#6762)
TransitionClient::new() in crates/s3-client/src/transition_api.rs computes
trailing_header_support = opts.trailing_headers && override_signer_type == SignatureV4,
but override_signer_type is hardcoded to SignatureDefault at construction
and never mutated afterwards, so the expression is always false regardless
of opts.trailing_headers. The resulting field also has no live reader: its
only reference is inside PutObjectOptions::validate() in
crates/s3-client/src/api_put_object.rs, which is itself
#[allow(dead_code, reason = "MinIO-parity ... no caller in this port")],
and even there the reference to trailing_header_support is commented out.

So trailing_headers: true in the seven warm_backend_*.rs constructors has
never had any effect on request signing or chunked/trailing-header
behavior (stream_sha256 signing is gated separately by
metadata.stream_sha256 && !self.secure). Remove the misleading dead
configuration from the seven provider constructors so it doesn't look
like intentional, load-bearing behavior to future readers.

Found during adversarial self-check while implementing rustfs/backlog#2040 (out of that issue's scope).
2026-08-28 07:35:48 +08:00
Zhengchao An a6a04b5faa refactor(ecstore,rustfs): reuse canonical starts_with_ignore_ascii_case (#6759)
* refactor(ecstore,rustfs): reuse canonical starts_with_ignore_ascii_case

`crates/utils/src/http/metadata_compat.rs` owns the internal metadata key helpers, including `starts_with_ignore_ascii_case`. Two files carried their own byte-identical copies of that predicate: `SetDisks::starts_with_ignore_ascii_case` in ecstore and a free function in the S3 options layer. Both drive internal metadata key classification (`internal_metadata_suffix` and quorum hashing on one side, `should_skip_object_metadata_key` and `is_reserved_user_metadata_key` on the other), so keeping three implementations of one predicate is an avoidable drift risk on a path that decides whether an internal key is treated as user metadata.

Delete both local copies and call the canonical implementation. Every prefix used at these call sites is an ASCII constant or literal, where the canonical byte-slice comparison and the removed `str::get(..n)` form are equivalent; that equivalence was checked differentially over 4.6M (key, prefix) pairs, including keys with multi-byte characters straddling the prefix boundary. No other logic in `internal_metadata_suffix` or `should_skip_object_metadata_key` changed.

Add regression tests on both sides pinning the two properties the switch depends on: internal prefixes match case-insensitively (a mixed-case `X-RustFS-Internal-*` key stays internal), and keys shorter than a prefix never match (they stay ordinary user metadata).

Refs rustfs/backlog#2051

* fix(rustfs): avoid typos-checker false positive in prefix-length test

The test literal "x-rustfs-encryptio" (a deliberate truncation of the
x-rustfs-encryption- prefix, used to assert that a key shorter than every
internal prefix falls through to user metadata) reads as a likely typo of
"encryption" to the repo's typos CI check. Derive it from
RUSTFS_ENCRYPTION_PREFIX via slicing instead of a hand-typed literal, which
both satisfies the linter and ties the truncation to the real constant
instead of a copy-typed guess.

Refs rustfs/backlog#2051
2026-08-28 07:35:09 +08:00
Zhengchao An 18068eb7e5 fix(deps): move off the yanked chacha20 0.10.1 (#6768)
chacha20 0.10.1 was yanked on crates.io today, which fails the Cargo Deny gate (error[yanked]) on every branch. cargo update -p chacha20 to 0.10.2; no API change, all dependents are semver-compatible.

Verification: cargo check -p rustfs-crypto; the Cargo Deny job on this PR is the authoritative gate.
2026-08-28 07:03:24 +08:00
Zhengchao An 921a48bd14 refactor(lifecycle): reuse the replication tag parser (#6761)
`crates/lifecycle/src/tagging.rs` carried a byte-identical copy of the `form_urlencoded` tag decoder already owned by `rustfs-replication`, plus a duplicate of its test. Since `crates/lifecycle` already depends on `rustfs-replication`, replace the copy with a `pub(crate) use` re-export: no new crate edge, one parser, and no second implementation to drift from the replication contract. The `rule.rs` call site is unchanged.

Also drop `crates/ecstore/src/bucket/lifecycle/tagging_boundary.rs`, a migration-era boundary shim with zero call sites in the tree.
2026-08-27 22:56:13 +00:00
Zhengchao An 2e6511566e refactor(rustfs): consolidate bucket metadata import match arms (#6760)
The import_bucket_metadata handler carried eight match arms whose bodies were
byte-identical apart from the type a payload is validated against and the pair
of BucketMetadata fields it lands in, so every arm repeated the same warn! call
and the same metadata lookup. Fold them into one or-pattern arm backed by
apply_imported_bucket_config, where a single conf_name match owns both the
validated type and the destination field pair and can no longer drift apart.

Validation still runs before the metadata lookup, the warn! event, fields, and
label are unchanged, and the BUCKET_POLICY_CONFIG and BUCKET_QUOTA_CONFIG_FILE
arms keep their own handling. Regression tests drive the full mapping table:
each config file's payload lands only in the field it owns, an unparsable
payload leaves the field untouched, and a rejected entry does not stop the
remaining ones from being imported.

Refs rustfs/backlog#2052
2026-08-27 22:55:09 +00:00
Zhengchao An ec8abb19ab refactor(s3-client): reuse rustfs-utils header classification instead of duplicating it (#6758)
crates/s3-client/src/utils.rs carried a verbatim copy of the header
classification tables and predicates owned by
crates/utils/src/http/headers.rs: SUPPORTED_HEADERS (same 11 keys),
SUPPORTED_QUERY_VALUES (same 9 keys), and is_standard_header /
is_storageclass_header / is_amz_header / is_rustfs_header /
is_minio_header with byte-identical bodies. The duplication was already
half-resolved and inconsistent — the local is_amz_header called
rustfs_utils::http::is_sse_header while consulting its own tables — and
s3-client already depends on rustfs-utils with the "full" feature, so
reusing the canonical owner adds no crate edge.

The sole caller, PutObjectOptions::header(), now imports the five
predicates from rustfs_utils::http. Semantics are unchanged: both sides
normalize with to_lowercase(), return false for unknown keys, and the
storage-class constants are the same string ("x-amz-storage-class" from
s3s::header::X_AMZ_STORAGE_CLASS vs rustfs_utils AMZ_STORAGE_CLASS), so
the set of user-metadata headers passed through verbatim rather than
prefixed with x-amz-meta- is identical.

SUPPORTED_QUERY_VALUES is deleted outright: s3-client had no reader for
it (utils consumes its own copy via is_standard_query_value). The
base64_encode/base64_decode helpers and their rustfs/rustfs#4811
regression test stay untouched, and lazy_static remains a dependency
because crates/s3-client/src/constants.rs still uses it.

Refs rustfs/backlog#2050
2026-08-28 06:53:15 +08:00
Zhengchao An 2a8be5566d refactor(concurrency): consolidate ForegroundPressure into workload owner (#6757)
ForegroundPressure had two definitions with byte-identical pressure computation: one in ecstore data-movement backpressure and one in the heal manager queue. That duplication is a violation of the ARCHITECTURE.md invariant that each type has exactly one definition, and it means any future change to the utilization math has to land twice.

Add the canonical `ForegroundPressure` and a `foreground_pressure(snapshot, read_threshold_pct, write_threshold_pct)` function to `crates/concurrency/src/workload.rs`, which already owns `WorkloadClass`, `AdmissionState`, and the admission snapshot contract. Both existing consumers already depend on `rustfs-concurrency`, so no crate edge is added.

The filter_map pipeline is transferred verbatim, preserving all five boundary behaviors (zero threshold, zero limit, missing entry, missing active count, and the `Saturated` full-utilization special case), the mul-before-div percentage normalization, the `>=` threshold comparison, and the read-then-write ordering that makes `max_by_key` break utilization ties toward the write class. The enable switch is deliberately left out: ecstore gates on `config.enabled` while heal gates on `mainline_throttle_enable` plus a both-thresholds-zero check, so each call site keeps its own condition.

This is the expand step only. The ecstore and heal copies are untouched and are removed by the follow-up migrate task.

Refs rustfs/backlog#2047
2026-08-28 06:52:50 +08:00
Zhengchao An bcbc58b6a0 refactor(ecstore): extract shared warm backend S3 constructor (#6755)
The seven S3-compatible warm backend providers (Aliyun, Azure, Huaweicloud, Tencent, MinIO, R2, RustFS) each carry a byte-identical copy of the same statically-credentialed TransitionClient construction and of the same optimal_part_size helper. Add both to the module that already owns the WarmBackend trait and WarmBackendS3, so the per-provider migrate step can drop its duplicate without redesigning anything.

bucket_lookup is a parameter rather than a constant because the providers split into two families: Aliyun, Azure, Huaweicloud, and Tencent pin BucketLookupDNS, while MinIO, R2, and RustFS leave it at the BucketLookupAuto default. Hardcoding either value would silently change bucket addressing for the other family during the migrate step.

Error texts, validation order, prefix and host/port normalization are reproduced exactly from the Aliyun/MinIO family. No provider file is touched and no production caller exists yet, so the new unit tests are the first callers.

Refs rustfs/backlog#2040
2026-08-27 22:16:47 +00:00
Zhengchao An 28fa412a06 refactor(ecstore): extract shared audit/notify KVS table constructors (#6756)
The audit and notify subsystems each declare their own default KVS table for the same nine delivery targets. For amqp, nats, pulsar, postgres and kafka the two declarations are byte-identical; for redis and mysql they differ only in a single default literal (the pub/sub channel and the destination table). Keeping two copies means every default or key-order change has to be made twice, and a missed edit silently changes what admin config reports for one subsystem only.

Add `config::target_defaults` with one constructor per shared table, taking the diverging literal as a parameter for redis and mysql, plus a small `kv` helper that replaces the repeated `KV { .. }` literals. Key order is reproduced exactly because it drives the order the admin API lists keys in. Unit tests pin the full ordered key/value/hidden_if_empty triple of every table against hard-coded literals, and cover both the audit and the notify literal for the two parameterized tables.

Webhook and mqtt are deliberately left out: audit's webhook table carries extra batching and retry keys, both webhook tables disagree on key order and on the auth-token hidden_if_empty flag, and mqtt disagrees on qos, keep-alive interval and reconnect interval. Those are real behavioral forks, not duplication, so they stay declared in place.

This is the expand step only. Nothing calls the new module yet, so audit.rs and notify.rs are untouched and no default changes; the constructors carry an item-level allow(dead_code) until the migrate step points both files at them.

Refs rustfs/backlog#2044
2026-08-27 22:16:03 +00:00
houseme 6f7a4ff060 fix(api): preserve server-side storage error surface (#6753)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-27 15:42:51 +00:00
hector 8a57632bfd ci: use dedicated RUSTFS_PERF_NODES for performance test (#6754) 2026-08-27 22:55:26 +08:00
GatewayJ 2eb4ddf4af test(table-catalog): automate DuckDB REST conformance (#6750)
* test(table-catalog): automate DuckDB REST conformance

* fix(table-catalog): protect DuckDB smoke tables
2026-08-27 22:34:10 +08:00
GatewayJ e281ed2f6d test(table-catalog): generate DuckDB REST attach SQL (#6749) 2026-08-27 22:25:33 +08:00
hector 2e6c820f53 test(heal): relative disk target and fail fast on terminal-but-short (#6748)
* test(heal): relative disk target and fail fast on terminal-but-short

The absolute 40 GiB heal target was calibrated to the background scanner
(auto-heal), which is now disabled for determinism; with only the explicit
heal the recovered node lands at ~36 GiB for 40 GiB survivors. Make the
success criterion relative: the outage node must reach at least 90% of the
least-used surviving node (absolute HEAL_TARGET_GB floor optional, default
0 = relative only).

Also fail fast when the heal task reaches a terminal success but the disk
target is not met (previously the monitor kept polling until timeout), and
drop the misleading 'progress absent' warning on the final (cleaned) task
response — mid-run progress is reported correctly.

Validated live: heal summary=finished, 0 failed, vm000/vm001=40GB,
vm002=40GB (target 36GB), test PASSED.

* test(heal): gate success on server verdict + data read-back, drop disk GB gate

The per-node disk-usage target (40 GiB / 90% of survivors) is not a
code-level invariant: EC distributes different shards per node, so the
final GB per node depends on the layout, not on heal correctness. Gate the
test on what the server actually verifies:

- Heal task terminal success (finished/completed) with objectsFailed == 0
  (the server's per-object scan/repair verdict).
- S3 read-back verification: list the test bucket and GET a sample of
  objects, requiring HTTP 200 for every read (end-to-end proof the data is
  still reconstructable after repair). The GET uses a discard mode so
  binary bodies are not captured (no null-byte warnings / SIGPIPE).

Per-node disk usage stays in the output as observability (with a warning if
the outage node gained no usage), not as the pass/fail gate. Removes the
heal_target_gb input and the relative-target logic.

Validated live: heal summary=finished, 0 failed, 20/20 objects read back,
vm002_used=40GB, PASS.
2026-08-27 22:25:17 +08:00
hector d48dda5bdc ci: add RustFS 4x4 performance test workflow and scripts (#6752)
* ci: add RustFS 4x4 performance test workflow and scripts

* ci: run performance test on dedicated pf-testing runner
2026-08-27 22:24:57 +08:00
1255 changed files with 407875 additions and 41493 deletions
@@ -0,0 +1,33 @@
# Adversarial Review Shape
Use when root `AGENTS.md` triggers adversarial validation or for a substantial PR
review. Paths below are repository-relative. The root finding standard and
completion rule apply; selecting a lens does not require finding a defect.
Risk and review shape:
- **Exempt:** documentation, comments, formatting, or typos with no runtime,
build, test, or agent-execution effect.
- **Mechanical:** renames, moves, test/tooling-only changes, and agent-rule
changes. Run correctness and simplicity lenses.
- **Standard:** localized behavior changes. Run one integrated final-diff pass
covering correctness, simplicity, and test coverage; add only domain lenses
matched by the diff.
- **High risk / substantial PR review:** high risk includes locking,
erasure/quorum/heal, replication, multipart, RPC, lifecycle/tiering,
persistence/fsync, IAM/KMS/auth, cryptography, on-disk/on-wire formats, and
S3-visible semantics. Cover all applicable lenses using exactly two
independent reviewers when delegation is explicitly authorized. Split the
lenses between them. Otherwise perform two fresh sequential passes.
- **Outbound client defaults:** what `TargetClient`, `PutObjectOptions`, or
the remote SDK configuration sends to every replication or migration target
is high risk for every target class even when the change fixes one. Follow
the SOP in `docs/postmortems/2026-09-03-replication-checksum-default-regression.md`:
run the outbound target matrix, document each new env knob in the same PR,
and list verified and unverified target classes in the PR Impact section.
Available domain lenses are security, concurrency/durability, compatibility,
and performance. Select `.agents/skills/adversarial-validation/SKILL.md` for an
explicit adversarial request, a high-risk change, or a substantial PR review;
then read only its matching role references. A routine standard pass does not
load the playbook unless the reviewer needs a RustFS-specific probe.
+65
View File
@@ -0,0 +1,65 @@
# Implementation Rules
Applies when changing code or running artifact-heavy work. Paths below
are repository-relative. Read only the relevant sections during read-only review.
## Worktree and Disk Hygiene
- Start implementation from the latest `origin/main` and confirm the requested
change is not already present.
- An existing clean, isolated task worktree is sufficient. Create another
worktree only when the current checkout is shared, dirty with unrelated work,
or belongs to another task.
- Never commit from a shared checkout.
- Use a task-specific branch named `<type>/<topic>`, such as `fix/...`,
`feat/...`, `test/...`, or `docs/...`, unless the user specifies a name.
- Do not include agent, tool, contributor, account, or organization names in
branch names.
- Push to the user-requested remote or the repository's configured push remote.
Do not hard-code or infer a remote from an account name.
- Check free space before artifact-heavy builds, tests, coverage, or downloads.
Re-check before a broad gate when space is tight.
- Remove only task-owned temporary/build artifacts. Never delete another task's
worktree or uncommitted data.
- At handoff, mention disk or cleanup details only when they affected execution
or artifacts/worktrees remain intentionally.
## Change Style
- Preserve existing control flow unless changing it is required for correctness.
- Prefer a direct local edit over new files, wrappers, managers, or speculative
abstractions.
- Add a helper only when it removes current duplication, names a real domain
boundary, or isolates a non-trivial invariant.
- Remove an in-scope path superseded by the change. If compatibility requires it,
adapt at the boundary to one canonical core and use the repository's
`RUSTFS_COMPAT_TODO` policy.
- Comments explain non-obvious invariants or reasons. Do not narrate code or
record change history.
- Mention unrelated problems when useful; do not fix them in a narrow task.
## Reuse and Boundary Rules
- Before adding helpers, constants, fixtures, or wrappers, search the touched
crate, the domain-owning crate, `crates/utils`, `crates/common`, and relevant
direct dependencies.
- Reuse requires matching semantics: normalization, error types, deadlines,
durability, and compatibility must fit the call site. A narrowly named local
helper is better than forced reuse with different semantics.
- Validate untrusted input at its trust boundary, then trust the validated type.
Values crossing disk, RPC, persistence, or version boundaries remain
untrusted at every consumer.
- Re-check boundary values immediately before destructive actions such as
delete, overwrite, or quorum decisions.
- Every new branch needs a concrete triggering input/state. For decoded or peer
data, corruption and mixed-version input are valid triggers.
- Required values must return a typed error when absent or corrupt; do not use a
default that converts corruption into a plausible result.
- Attach error context once where it is actionable. Do not erase typed errors
below aggregation or quorum layers.
## Naming
Use Rust API naming: `SCREAMING_SNAKE_CASE` constants/statics, `snake_case`
functions/variables, and `PascalCase` types. Do not rename unrelated existing
violations.
+51
View File
@@ -0,0 +1,51 @@
# Git and Pull Request Rules
Applies to commits, pushes, PRs, and issue/discussion actions. Paths below are
repository-relative. User authorization and root `AGENTS.md` still govern scope.
## Final PR Preflight
Before creating or updating a PR, reuse completed review and verification:
- Verify the actual base (normally `origin/main`) and the complete task diff,
including file names and whitespace. Exclude secrets, logs, generated
artifacts, and unrelated edits. Retain an existing PR's base unless requested.
- Confirm the final diff passed the root verification tier; fix task-owned
failures and run missing scoped checks. Report unresolved required checks or
authority without expanding the task. Do not start another general review.
- Keep the English Conventional Commit title at most 72 characters. Use the
template headings, actual checks, material risks, and rollback notes.
- Immediately before writing to GitHub, confirm the head and task diff are
unchanged. Rerun only checks invalidated by edits or relevant state changes.
## Pull Request Lifecycle
- Creating or updating a PR includes one immediate snapshot of checks,
mergeability, reviews, and unresolved threads.
- Unless the user explicitly requests monitoring, a release workflow requires
it, or an automation already owns it, hand off after the PR is open with the
current state and next event to watch. Do not delay ordinary handoff with
fixed quiet-period sleeps.
- For requested monitoring, use event-driven or bounded waits. Report only state
changes, actionable failures, or a meaningful prolonged delay.
- Investigate failures/comments before changing code. Fix task-attributable
issues, rerun affected verification, push, reply or resolve the thread, then
resume the requested monitor.
- Never merge without required reviewer approval or explicit authority.
- After an observed merge, verify the commit reached the base, then clean the
task worktree/branch when safe. Preserve unmerged work for closed PRs unless
deletion was explicitly authorized.
## Git and PR Baseline
- Follow Conventional Commits; keep the subject at most 72 characters.
- Source comments, commits, PR titles, and PR bodies are in English.
- Keep every heading from `.github/pull_request_template.md`; use `N/A` where
needed and include commands actually run.
- Use `--body-file` for multiline `gh pr create`/`gh pr edit` content.
- PR/issue/discussion content must not contain the literal sequence `\n` or
hard-wrapped prose paragraphs.
- Do not include local absolute paths or tool-specific labels/prefixes in GitHub
content.
- Resolve review threads after the underlying issue is fixed. If declining a
suggestion, reply with a short evidence-based reason.
+11 -8
View File
@@ -1,11 +1,11 @@
---
name: adversarial-validation
description: Review a final RustFS diff adversarially when the user requests adversarial review, the root AGENTS.md classifies the change as high risk, or a substantial PR is being reviewed. Do not use for ordinary questions, diagnosis, planning, status, documentation-only work, or routine low-risk implementation.
description: Review RustFS diffs or designs for explicit adversarial requests, high-risk changes under the repository review policy, or substantial PR reviews. Skip ordinary questions, diagnosis, planning, status, routine low-risk implementation, and prose with no execution effect.
---
# RustFS Adversarial Validation
Use the risk tier and review shape defined in the root `AGENTS.md`. This skill
Use the [repository risk tiers and review shape](../../references/adversarial-validation.md). This skill
routes a review to RustFS-specific probes without loading unrelated domains.
## Select Lenses
@@ -31,15 +31,18 @@ adversarial review.
## Review Protocol
1. Freeze the exact final diff/head and list the selected lenses.
2. Run the review shape required by root `AGENTS.md`.
1. Freeze the exact final diff/head (or the design under review) and list the
selected lenses.
2. Run the review shape required by the repository risk tier.
3. For each selected lens, either report a concrete finding or a null verdict
naming the attacks performed.
4. A finding needs `file:line`, a triggering input/state/interleaving, the wrong
outcome, and a focused fix or missing regression check.
5. Fix or rebut every finding with code-path, test, or invariant evidence.
4. Apply root `AGENTS.md`'s finding standard. Test each candidate against callers,
existing coverage, and invariants before accepting it; an adversarial role
does not have to produce a defect.
5. Fix or rebut supported findings with code-path, test, or invariant evidence.
6. After a non-trivial edit, rerun only lenses affected by that edit against the
new exact diff.
Do not turn a null verdict into a long checklist. Record concise evidence that
the relevant failure classes were attacked.
the relevant failure classes were attacked, then stop under the root completion
rule. Keep the required per-lens verdicts for high-risk PRs.
@@ -22,3 +22,17 @@
fixtures and encrypted migration data.
- Compatibility shims use `RUSTFS_COMPAT_TODO(<task-id>)`, have a removal
condition, and default toward reading old data safely.
## Outbound targets
- A change to what the replication or migration client sends by default
(checksum policy, payload framing, headers, version-id addressing) is judged
against every target class, not the one it fixes. Name each target-side rule
the current default satisfies — checksum required with Object Lock
parameters, `aws-chunked` decoding, version-id adoption, ETag equals content
MD5 — and show which cell of
`crates/e2e_test/src/replication_target_matrix_test.rs` covers each.
- A test that asserts the fix ("no trailer header") is not evidence; the
matrix cell that asserts the target accepted and stored the object is.
- Every new environment escape hatch appears in
`docs/operations/replication-outbound-transport.md` in the same diff.
@@ -3,9 +3,10 @@
- For every behavior claim, name the focused test/check that fails if the
changed hunk is reverted. If none is practical, require the reason and
residual risk.
- Confirm tests exercise the real production path and assert returned values,
exact bytes, stored state, or the specific error variant—not only success,
`is_err()`, or no panic.
- Confirm tests exercise the real production path and distinguish the intended
behavior from the named regression. A success, `is_err()`, or no-panic check
can be sufficient when that is the actual contract; require exact values,
bytes, state, or error variants when those distinctions matter to the change.
- For new flags/modes, verify each branch and ask which test fails if the branch
is inverted.
- For new error propagation, inject the failure and assert the caller observes
+10 -7
View File
@@ -1,12 +1,13 @@
---
name: arch-checks
description: Resolve failures from the repository's architecture guard scripts — check_layer_dependencies.sh, check_architecture_migration_rules.sh, check_unsafe_code_allowances.sh, check_logging_guardrails.sh, check_doc_paths.sh. Use when make pre-commit / pre-pr or CI fails on one of these checks.
description: Diagnose failures from check_layer_dependencies.sh, check_architecture_migration_rules.sh, check_unsafe_code_allowances.sh, check_logging_guardrails.sh, check_doc_paths.sh, or check_no_planning_docs.sh. Use when one of these guards fails, not for every architecture question or documentation edit.
---
# Architecture Guard Checks
All five run in `make pre-commit` / `make pre-pr` and in CI. Fix the cause;
never weaken a check to get green.
Read only the section for the failing guard. Use `.config/make/` and the current
workflow to verify its wiring; not every guard is part of every gate. Fix the
cause and rerun the failed guard; never weaken a check to get green.
## `check_layer_dependencies.sh` — layer DAG in `rustfs/src`
@@ -50,10 +51,12 @@ consider adding it to the script's `checked_files` list.
## `check_doc_paths.sh`
Instruction/architecture docs (`AGENTS.md`, `CLAUDE.md`, `ARCHITECTURE.md`,
`docs/architecture/*.md`) must not reference repo file paths that no longer
exist. If your refactor moved code, update the docs that point at it — the
error message lists `doc -> stale-path` pairs.
Instruction docs (`AGENTS.md`, `CLAUDE.md`, `ARCHITECTURE.md`) and every
Markdown file under `docs/` (architecture, operations, testing, index) must not
reference repo file paths that no longer exist. If your refactor moved code,
update the docs that point at it — the error message lists `doc -> stale-path`
pairs. In durable docs, cite paths plus symbol names rather than line numbers
(see `docs/architecture/README.md`). Review findings still need `file:line`.
## `check_no_planning_docs.sh`
@@ -8,19 +8,11 @@ description: Review a commit, PR, or merged patch when the user requests ordinar
Use this skill for an ordinary requested review. If the root policy or user calls
for adversarial validation, use `adversarial-validation` instead of running both.
## Quick Start
1. Read the scope: commit, PR, patch, or file list.
2. Map each changed area by risk and user impact.
3. Inspect each risky change in context.
4. Report findings first, ordered by severity.
5. Close with residual risks and verification recommendations.
## Core Workflow
### 1) Scope and assumptions
- Confirm change source (diff, commit, PR, files), target branch, language/runtime, and version.
- If context is missing, state assumptions before deeper analysis.
- Derive the change source, target branch, and relevant runtime/version from the
supplied diff and metadata. Ask only when missing context could change the verdict.
- Focus only on requested scope; avoid reviewing unrelated files.
### 2) Risk map
@@ -40,43 +32,20 @@ for adversarial validation, use `adversarial-validation` instead of running both
- unchecked assumptions and null/empty/error-path handling
- stale tests, fixtures, and configs
- hidden coupling to shared helpers/constants/features
- If a point is uncertain, mark it as an open question instead of guessing.
- Apply root `AGENTS.md`'s finding standard: try to disprove a candidate before
reporting it. Mention an unresolved question only when it could materially
change the verdict; do not fill the report with speculative possibilities.
#### Rust-specific checks (apply to all Rust changes)
#### Rust-specific checks
Run the full checklist in [rust-code-quality](../rust-code-quality/SKILL.md) — the canonical Rust review checklist for the unwrap/casting/cloning/locking/recursion/error-type/serde/test rules and the reuse-and-necessity checks (duplicated helpers, defensive branches without a nameable trigger, redundant error wrapping). Do not restate those rules here; carry its P0–P3 ratings over unchanged and use this skill's output format.
For changed Rust behavior, use the matching sections of [rust-code-quality](../rust-code-quality/SKILL.md). Reuse checks already performed by the selected review workflow. Comment-only or formatting-only Rust diffs do not require the full Rust checklist. Carry its P0–P3 ratings over unchanged and use this skill's output format.
### 4) Findings-first output
- Order findings by severity:
- P0: critical failure, security breach, or data loss risk
- P1: high-impact regression
- P2: medium risk correctness gap
- P3: low risk/quality debt
- For each finding include:
- Severity
- `path:line` reference
- concise issue statement
- impact and likely failure mode
- specific fix or mitigation
- validation step to confirm
- If no issues exist, explicitly state `No findings` and why.
- Order supported findings by P0–P3 severity; preserve the Rust ratings above.
Include `path:line`, the failure and impact, a focused fix, and its validation.
- If no supported issues remain, state `No findings` with the reviewed scope and
any material verification limitation. Do not append optional improvements to
make a clean review look productive.
### 5) Close
- Report assumptions and unknowns.
- Suggest targeted checks (tests, canary checks, logs/metrics, migration validation).
## Output Template
1. Findings
2. No findings (if applicable)
3. Assumptions / Unknowns
4. Recommended verification steps
## Finding Template
- `[P1] Missing timeout for downstream call`
- Location: `path/to/file.rs:123`
- Issue: ...
- Impact: ...
- Fix suggestion: ...
- Validation: ...
Close after the required review. Recommend additional verification only for an
identified unresolved risk or required gate; reuse evidence for unchanged code.
+112
View File
@@ -0,0 +1,112 @@
---
name: issue-triage
description: Assess whether a GitHub issue is fixed, needs implementation, or can be closed by checking related work and current code. Use for issue completion/triage requests. Status questions are read-only; comment, close, or change labels only when the conversation authorizes that action.
---
# Issue Triage
Use this skill when the user provides a GitHub issue URL and asks "can this be closed?", "is this already implemented?", "check completion status", or similar triage questions.
## Workflow
### 1. Fetch issue context
```bash
gh issue view <N> --repo <owner/repo> --json title,body,state,comments,labels,updatedAt
```
Read the issue body to understand what was requested. Extract:
- The specific feature/fix/behavior described.
- Any linked PRs or commits mentioned in the body or comments.
- Any checklist items or sub-issues.
Resolve the issue repository and implementation repository separately (for example, `rustfs/backlog` tracks work in `rustfs/rustfs`). Pass the implementation repository explicitly to PR queries; the current checkout may belong to another repository.
### 2. Search for related work
Search git history for commits referencing the issue:
```bash
git log --oneline --all --grep="<N>" | head -30
```
Search for related PRs:
```bash
gh pr list --repo <implementation-repo> --search "<issue-url>" --state all --json number,title,state,mergedAt
```
Also search qualified issue references and subject keywords; for same-repository
issues, include `#<N>`. Follow explicit links even without a text match. A search
page with no match does not prove the work is absent.
If the issue mentions specific PRs, check their status:
```bash
gh pr view <PR_N> --repo <implementation-repo> --json state,mergedAt,title,mergeCommit,baseRefName
```
### 3. Verify implementation
Fetch the implementation repository's current base branch. For each merged candidate, verify its merge commit is present and inspect the current code for the claimed behavior; a commit message match alone is not proof:
```bash
git fetch <implementation-remote> <base-branch>
git merge-base --is-ancestor <merge-commit> <implementation-remote>/<base-branch>
```
If the issue describes a specific defect, inspect the fetched base's code rather than assuming the current checkout contains it:
```bash
git show <implementation-remote>/<base-branch>:crates/<relevant>/src/<file>.rs
```
For issues with checklists, verify each item individually. If sub-items are tracked as separate issues, check those too:
```bash
gh issue view <SUB_N> --repo <owner/repo> --json state
```
### 4. Determine verdict
- **All items fixed and merged**: Recommend closing; name the verified PRs and behavior.
- **Some items fixed, some remaining**: Keep open; report each remaining item.
- **Not yet implemented**: Keep open; report what remains.
- **Superseded or no longer relevant**: Recommend closing with evidence.
### 5. Take action
For a status-only request, return the assessment without GitHub writes. If commenting, closing, or label edits are authorized, perform only those actions; do not ask again for authority already given. Prepare the final assessment before asking for any missing authority. Write `rustfs/backlog` issue content in Chinese.
Close with comment:
```bash
gh issue close <N> --repo <owner/repo> --comment "<body>"
```
Comment without closing:
```bash
gh issue comment <N> --repo <owner/repo> --body-file /tmp/triage.md
```
Update labels only when label changes are authorized, using existing repository labels; never add tool-specific labels:
```bash
gh issue edit <N> --repo <owner/repo> --add-label "<existing-label>"
```
Always use `--body-file` for multiline content, never inline `--body`.
### 6. Handle multi-issue batches
When the user asks to check multiple issues (e.g., "check all issues by user X" or "scan backlog for closable issues"):
1. List the full requested scope with pagination (for example `gh api --paginate 'repos/<repo>/issues?state=open&per_page=100'`, excluding entries with `pull_request`). Add an author filter only when the user requested one; the default page/limit is not evidence that all issues were checked.
2. For each issue, run steps 1-5 above.
3. Report a summary table of all triaged issues with verdicts.
## Report
Identify the issue and current state, verified implementation/PR evidence,
remaining items, verdict, and action actually taken. Use a table for batches;
a single issue does not require a heading for each field. Follow step 4's
verdicts without repeating the assessment in another template.
## Notes
- The user may ask in Chinese ("是否可以关闭", "检查完成情况"); respond in the same language.
- When closing, always include a summary of what was fixed and which PRs resolved it — this creates a useful audit trail.
- For issues in `rustfs/backlog`, use `--repo rustfs/backlog`.
- For issues in `rustfs/rustfs`, use `--repo rustfs/rustfs`.
- If the issue has sub-issues (GitHub sub-issues API), check each one's state before declaring the parent complete.
@@ -1,6 +1,6 @@
---
name: plugin-contract-guard
description: Invariants and change procedure for the target-plugin / extension system — plugin manifests, admin plugin/extension catalog and instance APIs, secret redaction, external-plugin install policy. Use when editing crates/targets (manifest, plugin, control_plane, catalog, runtime), crates/extension-schema, or rustfs/src/admin plugin_contract.rs / plugins_*.rs / extensions.rs / target_descriptor.rs.
description: Guard changes to target-plugin manifests, extension schemas, admin catalog/instance contracts, secret redaction, and external-plugin install policy. Use when a diff changes those contracts in crates/targets, crates/extension-schema, or admin plugin/extension handlers; path membership alone, comments, and unrelated runtime internals do not trigger it.
---
# Plugin & Extension Contract Guard
@@ -1,46 +0,0 @@
---
name: pr-creation-checker
description: Perform the final RustFS PR preflight and draft compliant English title/body metadata immediately before creating or updating a PR. Do not use during implementation or as a second general code review.
---
# PR Creation Checker
Use this skill only at the PR boundary. Reuse completed diff review and
verification evidence; do not reread the repository or rerun equivalent checks.
## Preflight
1. Confirm the branch is based on current `origin/main` and contains only the
intended task diff.
2. Inspect `git diff --stat`, `git diff --check`, and changed file names for
secrets, logs, generated artifacts, or unrelated edits.
3. Confirm the checks selected by root `AGENTS.md` passed on the final diff.
Do not replace focused behavioral tests with a generic gate or rerun checks
already covered by an unchanged umbrella run.
4. Read `.github/pull_request_template.md`. Consult `Makefile`, `.config/make/`,
or CI only when the required command/current gate is uncertain.
5. Return `BLOCKED` for an unclean scope, missing required evidence, failed
required checks, or non-compliant metadata.
## Metadata
- Title: English Conventional Commit, at most 72 characters, with no tool
prefix.
- Body: English, exact template headings, `N/A` where needed, concise rationale,
actual verification commands, and material risks/rollback notes.
- Use repository-relative paths; never include local absolute paths.
- Keep prose paragraphs on one logical line and never include the literal
sequence `\n`.
- Use a temporary body file with `gh pr create --body-file` or
`gh pr edit --body-file`; never pass multiline Markdown inline.
## Output
- Status: `READY` or `BLOCKED`.
- Title.
- Complete PR body.
- Verification commands and results.
- Risks or `N/A`.
Immediately before the GitHub write, repeat only the five preflight checks above
against the final head.
@@ -1,4 +0,0 @@
interface:
display_name: "PR Creation Checker"
short_description: "Draft RustFS-ready PRs with checks, template, and blockers."
default_prompt: "Use $pr-creation-checker for final PR preflight and compliant English title/body metadata."
+93
View File
@@ -0,0 +1,93 @@
---
name: pr-review
description: Review a GitHub PR from a URL or number using its actual base/head and risk-appropriate code review. Use when the user asks for a PR review, not a status lookup or PR wording edit. Publish a review only when authorized; delegation and monitoring follow the requested scope and root AGENTS.md.
---
# PR Review
Use this skill for PR context and review delivery. An ordinary review request is read-only unless the conversation also authorizes posting or fixes. Reuse that authorization without asking again; prepare the review before requesting any missing publication approval.
## Prerequisites
- Follow root `AGENTS.md`; classify risk with the [review policy](../../references/adversarial-validation.md) and consult relevant [change-style and boundary rules](../../references/implementation.md).
- Select `code-change-verification` for ordinary review or `adversarial-validation` for explicitly adversarial, substantial, or high-risk review; do not run both on the same diff.
## Workflow
### 1. Gather PR context
```bash
gh pr view <N> --repo <owner/repo> --json title,author,state,body,additions,deletions,changedFiles,commits,baseRefName,headRefName,baseRefOid,headRefOid
gh pr diff <N> --repo <owner/repo> --name-only
```
Read the PR body and linked issues to understand the change's purpose. If the PR references an issue, fetch that too:
```bash
gh issue view <ISSUE> --repo <issue-owner/repo> --json title,body,state
```
### 2. Fetch the diff and classify the change
```bash
git fetch <repo-remote> <baseRefName> refs/pull/<N>/head
git diff <baseRefOid>...<headRefOid> --stat
```
Resolve `<repo-remote>` to the PR repository; do not assume the current checkout's `origin` or `main` matches. Record the exact base/head used. If either moved during fetching, refresh the snapshot before reviewing. Classify using the repository review policy; instruction changes that affect agent execution are mechanical, not exempt.
### 3. Review the changed behavior
Group files by functional area to trace callers and invariants. Use the root risk tier's review shape and only matching lenses. File count does not authorize delegation. When delegation is explicitly authorized, high-risk/substantial reviews use exactly two independent reviewers with the applicable lenses split between them; otherwise use two fresh sequential passes. Reviewers do not spawn further agents.
Findings need a concrete failure scenario with `file:line`; a null verdict briefly names the relevant probes. Reuse existing evidence and choose local checks from the final diff under the root verification policy.
### 4. Check CI status
```bash
gh pr checks <N> --repo <owner/repo>
```
Investigate a failed check when it bears on a finding or the user requested CI diagnosis/merge readiness:
```bash
gh run view --repo <owner/repo> --log-failed --job=<JOB_ID>
```
Use current evidence to distinguish pre-existing, flaky, and PR-caused failures. Do not classify them by guesswork or turn a code-only review into unrelated CI repair.
### 5. Synthesize findings
Report the PR, reviewed base/head, and risk tier, then summarize the assessment.
Use the selected review's P0–P3 ratings and root finding standard: supported
findings with `file:line`, failure scenario, and fix, or `No findings`.
State the observed check status, including pending or unavailable checks, and
the verdict (`APPROVE`, `REQUEST_CHANGES`, or `COMMENT`). Do not infer a pass
from missing checks or add style nits to populate a clean review.
### 6. Post the review
Only when posting is authorized, write the review body to a temp file and post via CLI. Refresh the PR head first; if it changed, review the delta and update the verdict before posting:
```bash
# Request changes
gh pr review <N> --repo <owner/repo> --request-changes --body-file /tmp/pr_review.md
# Approve
gh pr review <N> --repo <owner/repo> --approve --body-file /tmp/pr_review.md
# Comment only (no verdict)
gh pr review <N> --repo <owner/repo> --comment --body-file /tmp/pr_review.md
```
For authorized inline comments, use [the submission example](references/posting.md).
Always use `--body-file` or `--input`, never inline multiline `--body`.
### 7. Handle follow-up
Follow the [PR lifecycle](../../references/pull-requests.md) and any explicit monitoring request. For follow-up, fetch the new head and compare the recorded reviewed SHA with the new SHA; revisit affected callers and findings. Never use an unfetched `origin/pull/<N>/head` ref as evidence. Update the posted review or resolve addressed threads only within existing authorization.
## Notes
- The user may ask for review in Chinese; respond in the same language but keep the review body in English per AGENTS.md rules.
- When the user asks for "多角色对抗 review", run the full adversarial validation protocol — this skill's step 3 covers that.
- If the PR is from a fork, check `maintainerCanModify` before attempting to push fixes.
- For very large PRs, batch the review by functional area while keeping the same bounded review shape.
@@ -0,0 +1,22 @@
# Inline PR Review Submission
Read only when an inline review is authorized. Recheck the PR head before posting and bind the review to the reviewed commit.
For inline comments on specific lines, use the GitHub API:
```bash
cat > /tmp/pr_review.json <<'EOF'
{
"commit_id": "<reviewed-head-sha>",
"body": "review body",
"event": "REQUEST_CHANGES",
"comments": [
{
"path": "crates/foo/src/bar.rs",
"line": 42,
"body": "finding description"
}
]
}
EOF
gh api --method POST /repos/{owner}/{repo}/pulls/<N>/reviews --input /tmp/pr_review.json
```
+16 -23
View File
@@ -1,6 +1,6 @@
---
name: rust-code-quality
description: Run a focused Rust quality review when the user requests one, when reviewing a Rust PR/commit, or when another selected review workflow delegates Rust-specific checks. Do not auto-load for every implementation edit.
description: Run a focused Rust quality review when the user requests one or a selected review workflow needs Rust-specific checks for changed behavior. Do not auto-load for every implementation edit, comment-only or formatting-only Rust diff, or repeat an already completed review.
---
# Rust Code Quality Gate
@@ -8,12 +8,18 @@ description: Run a focused Rust quality review when the user requests one, when
Use this skill for a dedicated Rust review to cover rules that `cargo clippy`
does not catch.
Search matches and checklist items are candidates, not findings. Apply the root
finding standard; distinguish a demonstrated bug, an explicit rule violation,
and an optional preference. P2/P3 suggestions do not need to be invented or
included in an otherwise clean correctness review.
## Quick Start
1. Identify changed `.rs` files.
2. Run automated checks on changed files.
3. Run manual review checklist on the diff.
4. Resolve or rebut every finding with evidence; P0/P1 findings cannot be deferred.
2. Run the matching candidate searches on changed files.
3. Apply the manual checklist sections whose behavior the diff touches.
4. Report or rebut every finding with evidence; P0/P1 findings block approval.
Fix them when implementation is authorized; a read-only review reports them.
## Automated Checks
@@ -35,7 +41,7 @@ rg -n 'Result<.*String>' <changed-files>
rg -n 'Box<dyn.*Error' <changed-files>
# 5. println/eprintln in production
rg -n 'println!\|eprintln!' <changed-files>
rg -n 'println!|eprintln!' <changed-files>
# 6. Ordering::Relaxed usage (verify each is intentional)
rg -n 'Ordering::Relaxed' <changed-files>
@@ -80,7 +86,7 @@ For the Rust diff under review, verify:
- [ ] Test volume and line count are never treated as production-code growth
### Serde
- [ ] Structs from untrusted input have `#[serde(deny_unknown_fields)]`
- [ ] Structs from untrusted input reject unknown fields where the compatibility contract permits; otherwise validate security-critical fields explicitly and test the supported input shape
- [ ] `#[serde(default)]` not used on security-critical fields without validation
### Code Hygiene
@@ -104,20 +110,7 @@ For the Rust diff under review, verify:
## Output Template
```
## Rust Code Quality Report
### Automated Scan
- unwrap/expect candidates inspected: N
- numeric-cast candidates inspected: N
- error-type candidates inspected: N
- output-macro candidates inspected: N
### Findings
- [P1] `path:line` — description
- Fix: ...
- Validation: ...
### Verdict
PASS / BLOCKED (list blocking findings)
```
Use the calling review's output format. For a standalone review, report supported
findings with severity, location, impact, fix, and validation, or `No findings`.
Include only material unverified checks. Candidate counts are not a quality
metric and do not need a separate scan report.
+13 -104
View File
@@ -4,9 +4,14 @@ description: "Run the end-to-end RustFS console gate, version bump, preview vali
---
# RustFS Release Publish (preview-validated pipeline)
This skill orchestrates a full release. It wraps `rustfs-release-version-bump` (which only edits version files and opens the PR) with a mandatory preview-tag validation loop before the final tag is published.
This skill orchestrates a full release. It wraps `rustfs-release-version-bump` (invoked here with the authorized commit/push/PR scope) with a mandatory preview-tag validation loop before the final tag is published.
Core design: **version files never carry a `-preview.N` suffix**. The preview suffix exists only in tag names. A preview tag creates a visible GitHub Release marked Prerelease and uploads versioned assets, but it never becomes GitHub Latest and never updates `*-latest`, `latest.json`, R2, Docker, or Helm channels. That Release is temporary: `build.yml` deletes it automatically once the final tag's Release is published, so the Releases page ends up carrying deliverables only while the `-preview.N` tags stay behind as the traceability record. This works because the binary self-reports the git tag it was built from (`build::TAG` via shadow_rs, see `rustfs/src/config/cli.rs` `SHORT_VERSION`), and `build.yml` derives artifact names and preview classification from the tag name — Cargo.toml's version is only a no-tag fallback. Therefore the preview tag and the final tag can (and MUST) point at the exact same commit: what you validated is byte-for-byte the source that ships.
The binary reports its build tag (`build::TAG` via shadow_rs; `SHORT_VERSION` in
`rustfs/src/config/cli.rs`), and `build.yml` derives asset names and preview
classification from that tag. Cargo.toml supplies only the no-tag fallback.
Preview and final tags must therefore share the validated source commit;
their tag-dependent version and asset names differ. The channel and cleanup
constraints are defined once under Preview tag naming and Hard rules below.
Pipeline shape:
@@ -29,9 +34,9 @@ On validation failure: fix lands on main via normal PR (version files are alread
- Final target version, for example `1.0.0-beta.10`.
- Preview iteration `N` (default: next unused preview tag for that target; check with `git tag -l '<target>-preview.*'` after `git fetch --tags`).
If the target version is missing or ambiguous, stop and ask before doing anything (see the semver gate below).
If the target version is missing or ambiguous, collect the current release/tag baseline and ask before version edits or publication. Continue independent read-only preflight while the answer is pending (see the semver gate below).
## Semver gate — confirm the target version before touching anything
## Semver gate — resolve the target before version edits or publication
Versions follow [SemVer 2.0.0](https://semver.org/). Precedence reminder:
@@ -43,7 +48,7 @@ Numeric prerelease identifiers compare numerically (`beta.9 < beta.10`), not lex
Rules:
- A request like "发个版" / "release the next version" without an exact version string is ALWAYS ambiguous. Derive the current latest tag (`git tag --sort=-v:refname | head`), then ask the user to choose via AskUserQuestion with concrete candidates, e.g. from `1.0.0-beta.10`: next prerelease `1.0.0-beta.11`, promote to `1.0.0-rc.1`, promote to stable `1.0.0`. Never guess between these — they have very different meanings (channel promotion vs. iteration) and different CI classification consequences.
- A request like "发个版" / "release the next version" without an exact version string is ALWAYS ambiguous. Derive the current latest tag (`git tag --sort=-v:refname | head`), then ask the user to choose with concrete candidates, e.g. from `1.0.0-beta.10`: next prerelease `1.0.0-beta.11`, promote to `1.0.0-rc.1`, promote to stable `1.0.0`. Never guess between these — they have very different meanings (channel promotion vs. iteration) and different CI classification consequences.
- After a stable `X.Y.Z` exists, the next version must state which component bumps: patch `X.Y.(Z+1)` for fixes only, minor `X.(Y+1).0` for backward-compatible features, major `(X+1).0.0` for breaking changes. If the user names a bump type but not a number, compute it from the latest stable tag and echo the exact resulting version back for confirmation.
- Echo the final confirmed version string verbatim in your first status report; every later phase must use exactly that string. If at any point the user's wording and the confirmed version diverge, stop and re-confirm.
@@ -77,58 +82,7 @@ Rules:
### Console release gate
Complete this gate before changing any RustFS version file or creating any RustFS tag. RustFS `build.yml` downloads the asset returned by `repos/rustfs/console/releases/latest`, so a successful Console build alone is insufficient.
1. Read the latest published Console tag and compare it with Console `main`:
```bash
CONSOLE_REPO="rustfs/console"
CONSOLE_LATEST=$(gh api "repos/${CONSOLE_REPO}/releases/latest" --jq .tag_name)
gh api "repos/${CONSOLE_REPO}/compare/${CONSOLE_LATEST}...main" \
--jq '{status, ahead_by, behind_by, commits: [.commits[] | {sha, message: .commit.message}]}'
```
- `ahead_by == 0`: no merged Console change is waiting for release. Still verify the current latest asset using step 4, then continue to Phase 1.
- `ahead_by > 0` and `behind_by == 0`: publish Console before continuing. Report the merged commits and select the next unused `vX.Y.Z` tag. Default to the next patch version when the changes are fixes or backward-compatible UI work; stop for confirmation if a minor/major bump is plausible.
- Any diverged history or `behind_by > 0`: stop and resolve the Console release baseline explicitly. Do not guess a range or publish RustFS.
2. Clone/fetch `rustfs/console` into a scratch directory and record its exact `main` commit. Before creating a tag, check for a `v*` tag or Release workflow already associated with that hash. If one is in progress, wait for it instead of creating another version:
```bash
CONSOLE_SCRATCH=$(mktemp -d)
gh repo clone "$CONSOLE_REPO" "$CONSOLE_SCRATCH/console"
git -C "$CONSOLE_SCRATCH/console" fetch origin main --tags
CONSOLE_HASH=$(git -C "$CONSOLE_SCRATCH/console" rev-parse origin/main)
git -C "$CONSOLE_SCRATCH/console" tag --points-at "$CONSOLE_HASH" 'v*'
gh run list -R "$CONSOLE_REPO" --workflow release.yml --commit "$CONSOLE_HASH" --limit 5
```
If no release exists or is running for `CONSOLE_HASH`, create the selected annotated tag at that exact hash and push it:
```bash
git -C "$CONSOLE_SCRATCH/console" tag -a "<console-tag>" -m "Release <console-tag>" "$CONSOLE_HASH"
git -C "$CONSOLE_SCRATCH/console" push origin "<console-tag>"
```
Console tags include the `v` prefix. Pushing the tag triggers `.github/workflows/release.yml` (`🚀 Release`). Remove `CONSOLE_SCRATCH` after the gate completes.
3. Find the exact tag run and wait for completion:
```bash
gh run list -R "$CONSOLE_REPO" --workflow release.yml --branch "<console-tag>" --limit 1
gh run watch -R "$CONSOLE_REPO" "<console-run-id>" --exit-status
```
4. Block until the published Release is non-draft, the latest endpoint returns the expected tag, and `rustfs-console-<console-tag>.zip` is uploaded, non-empty, and carries a `sha256:` digest:
```bash
gh release view -R "$CONSOLE_REPO" "<console-tag>" --json isDraft,isPrerelease,assets,url
test "$(gh api "repos/${CONSOLE_REPO}/releases/latest" --jq .tag_name)" = "<console-tag>"
test "$(gh api "repos/${CONSOLE_REPO}/releases/tags/<console-tag>" \
--jq '[.assets[] | select(.name == "rustfs-console-<console-tag>.zip" and .state == "uploaded" and .size > 0 and (.digest | startswith("sha256:")))] | length')" -eq 1
```
Treat a missing/mismatched asset, digest, latest tag, or failed/cancelled workflow as BLOCKED. Do not start Phase 1 until the Console gate passes. Record `CONSOLE_TAG`, `CONSOLE_HASH`, Console run URL, and Release URL for the final report.
Read and complete [the Console gate](references/console-gate.md) before Phase 1. Verify the latest published Console asset and exact commit; if Console main is ahead, complete its release and asset verification first. A successful build alone does not satisfy this gate.
## Phase 1 — Version bump to the final target (once)
@@ -164,54 +118,9 @@ On a restart (N+1), refresh `PREVIEW_HASH=$(git rev-parse origin/main)` first
- Record `PREVIOUS_DELIVERABLE`, selected from published Releases by `publishedAt` after excluding the current tag and every `-preview.N` tag. Verify `gh release view "<preview-tag>" --json body --jq .body` contains `## What's Changed` and, when `PREVIOUS_DELIVERABLE` exists, `**Full Changelog**: https://github.com/rustfs/rustfs/compare/<PREVIOUS_DELIVERABLE>...<preview-tag>`. For a repository with no previous deliverable, verify a Full Changelog link exists and record the GitHub baseline fallback.
- Confirm preview-triggered Docker and Helm jobs are skipped. Preview validation covers the built RustFS binaries, embedded console, and rc compatibility; Docker image construction and Helm publication are deferred to the final tag because the Dockerfiles consume GitHub Release assets.
## Phase 4 — Run the artifact locally, verify the console
## Phases 4–5 — Local artifact, Console, and rc acceptance
Work inside the session scratchpad directory; never leave stray data dirs.
```bash
gh release download "<preview-tag>" -p "rustfs-macos-aarch64-v<preview-tag>.zip" -D "$SCRATCH"
cd "$SCRATCH" && unzip -o rustfs-*.zip
./rustfs --version # must report the PREVIEW TAG (build::TAG), not the Cargo.toml version, plus expected short SHA
mkdir -p data
RUSTFS_ACCESS_KEY=rustfsadmin RUSTFS_SECRET_KEY=rustfsadmin ./rustfs ./data
```
Defaults: S3 endpoint `:9000`, embedded console `:9001`.
Checks (all must pass):
- `./rustfs --version` reports the preview tag name and the short SHA of `PREVIEW_HASH`. Reporting `<target>` without the `-preview.N` suffix means the build did not embed the tag — treat as FAIL and investigate before proceeding.
- `curl -fsS http://localhost:9000/health/ready` returns ready.
- Startup log shows the embedded console being served (this was the regression that `fix(release): require embedded console assets` guards).
- Open `http://localhost:9001` in the browser: login with `rustfsadmin`/`rustfsadmin`; dashboard renders without JS console errors; create a bucket, upload a file, download it back (byte-identical), delete the object and bucket. Keep the server running for Phase 5.
## Phase 5 — Validate with the latest rc client
`rc` is the RustFS CLI client from <https://github.com/rustfs/cli>.
- Ensure the latest release is installed: compare `rc --version` against `gh api repos/rustfs/cli/releases/latest --jq .tag_name`; update via `brew upgrade rustfs/tap/rc` (or download the release binary).
- Point it at the preview server and run the command matrix, recording PASS/FAIL per command:
```bash
rc alias set preview http://localhost:9000 rustfsadmin rustfsadmin
rc ls preview/
rc mb preview/rel-check
rc cp <local-file> preview/rel-check/
rc stat preview/rel-check/<file>
rc cat preview/rel-check/<file> # matches source
rc cp preview/rel-check/<file> ./out && cmp <local-file> ./out
rc cp -r <local-dir>/ preview/rel-check/dir/
rc find preview/rel-check --name "*"
rc share download preview/rel-check/<file> --expire 1h # presigned URL fetchable via curl
rc rm preview/rel-check/<file> && rc rm -r --force preview/rel-check/dir
rc rb preview/rel-check
rc admin user list preview/
rc admin user add preview/ relcheckuser relchecksecret12
rc admin user remove preview/ relcheckuser
rc alias remove preview
```
- Any FAIL blocks the release. Afterwards stop the server and delete the scratch data directory.
Read and complete [preview acceptance](references/preview-acceptance.md): verify the downloaded binary's tag/SHA and readiness, exercise Console CRUD with byte-identical download, and pass the full latest-rc command matrix. Any failure blocks final publication. Retain the results for the confirmation gate below.
### Manual confirmation gate
@@ -0,0 +1,58 @@
# Console Release Gate
Read during Phase 0, before changing RustFS version files or tags. Follow the parent skill's release scope and authorization rules.
### Console release gate
Complete this gate before changing any RustFS version file or creating any RustFS tag. RustFS `build.yml` downloads the asset returned by `repos/rustfs/console/releases/latest`, so a successful Console build alone is insufficient.
1. Read the latest published Console tag and compare it with Console `main`:
```bash
CONSOLE_REPO="rustfs/console"
CONSOLE_LATEST=$(gh api "repos/${CONSOLE_REPO}/releases/latest" --jq .tag_name)
gh api "repos/${CONSOLE_REPO}/compare/${CONSOLE_LATEST}...main" \
--jq '{status, ahead_by, behind_by, commits: [.commits[] | {sha, message: .commit.message}]}'
```
- `ahead_by == 0`: no merged Console change is waiting for release. Still verify the current latest asset using step 4, then continue to Phase 1.
- `ahead_by > 0` and `behind_by == 0`: publish Console before continuing. Report the merged commits and select the next unused `vX.Y.Z` tag. Default to the next patch version when the changes are fixes or backward-compatible UI work; stop for confirmation if a minor/major bump is plausible.
- Any diverged history or `behind_by > 0`: stop and resolve the Console release baseline explicitly. Do not guess a range or publish RustFS.
2. Clone/fetch `rustfs/console` into a scratch directory and record its exact `main` commit. Before creating a tag, check for a `v*` tag or Release workflow already associated with that hash. If one is in progress, wait for it instead of creating another version:
```bash
CONSOLE_SCRATCH=$(mktemp -d)
gh repo clone "$CONSOLE_REPO" "$CONSOLE_SCRATCH/console"
git -C "$CONSOLE_SCRATCH/console" fetch origin main --tags
CONSOLE_HASH=$(git -C "$CONSOLE_SCRATCH/console" rev-parse origin/main)
git -C "$CONSOLE_SCRATCH/console" tag --points-at "$CONSOLE_HASH" 'v*'
gh run list -R "$CONSOLE_REPO" --workflow release.yml --commit "$CONSOLE_HASH" --limit 5
```
If no release exists or is running for `CONSOLE_HASH`, create the selected annotated tag at that exact hash and push it:
```bash
git -C "$CONSOLE_SCRATCH/console" tag -a "<console-tag>" -m "Release <console-tag>" "$CONSOLE_HASH"
git -C "$CONSOLE_SCRATCH/console" push origin "<console-tag>"
```
Console tags include the `v` prefix. Pushing the tag triggers `.github/workflows/release.yml` (`🚀 Release`). Remove `CONSOLE_SCRATCH` after the gate completes.
3. Find the exact tag run and wait for completion:
```bash
gh run list -R "$CONSOLE_REPO" --workflow release.yml --branch "<console-tag>" --limit 1
gh run watch -R "$CONSOLE_REPO" "<console-run-id>" --exit-status
```
4. Block until the published Release is non-draft, the latest endpoint returns the expected tag, and `rustfs-console-<console-tag>.zip` is uploaded, non-empty, and carries a `sha256:` digest:
```bash
gh release view -R "$CONSOLE_REPO" "<console-tag>" --json isDraft,isPrerelease,assets,url
test "$(gh api "repos/${CONSOLE_REPO}/releases/latest" --jq .tag_name)" = "<console-tag>"
test "$(gh api "repos/${CONSOLE_REPO}/releases/tags/<console-tag>" \
--jq '[.assets[] | select(.name == "rustfs-console-<console-tag>.zip" and .state == "uploaded" and .size > 0 and (.digest | startswith("sha256:")))] | length')" -eq 1
```
Treat a missing/mismatched asset, digest, latest tag, or failed/cancelled workflow as BLOCKED. Do not start Phase 1 until the Console gate passes. Record `CONSOLE_TAG`, `CONSOLE_HASH`, Console run URL, and Release URL for the final report.
@@ -0,0 +1,52 @@
# Preview Artifact Acceptance
Read after Phase 3 succeeds. Complete every check below before the parent skill's manual confirmation gate. These checks cover the downloaded artifact, embedded Console, and latest rc client.
## Phase 4 — Run the artifact locally, verify the console
Work inside the session scratchpad directory; never leave stray data dirs.
```bash
gh release download "<preview-tag>" -p "rustfs-macos-aarch64-v<preview-tag>.zip" -D "$SCRATCH"
cd "$SCRATCH" && unzip -o rustfs-*.zip
./rustfs --version # must report the PREVIEW TAG (build::TAG), not the Cargo.toml version, plus expected short SHA
mkdir -p data
RUSTFS_ACCESS_KEY=rustfsadmin RUSTFS_SECRET_KEY=rustfsadmin ./rustfs ./data
```
Defaults: S3 endpoint `:9000`, embedded console `:9001`.
Checks (all must pass):
- `./rustfs --version` reports the preview tag name and the short SHA of `PREVIEW_HASH`. Reporting `<target>` without the `-preview.N` suffix means the build did not embed the tag — treat as FAIL and investigate before proceeding.
- `curl -fsS http://localhost:9000/health/ready` returns ready.
- Startup log shows the embedded console being served (this was the regression that `fix(release): require embedded console assets` guards).
- Open `http://localhost:9001` in the browser: login with `rustfsadmin`/`rustfsadmin`; dashboard renders without JS console errors; create a bucket, upload a file, download it back (byte-identical), delete the object and bucket. Keep the server running for Phase 5.
## Phase 5 — Validate with the latest rc client
`rc` is the RustFS CLI client from <https://github.com/rustfs/cli>.
- Ensure the latest release is installed: compare `rc --version` against `gh api repos/rustfs/cli/releases/latest --jq .tag_name`; update via `brew upgrade rustfs/tap/rc` (or download the release binary).
- Point it at the preview server and run the command matrix, recording PASS/FAIL per command:
```bash
rc alias set preview http://localhost:9000 rustfsadmin rustfsadmin
rc ls preview/
rc mb preview/rel-check
rc cp <local-file> preview/rel-check/
rc stat preview/rel-check/<file>
rc cat preview/rel-check/<file> # matches source
rc cp preview/rel-check/<file> ./out && cmp <local-file> ./out
rc cp -r <local-dir>/ preview/rel-check/dir/
rc find preview/rel-check --name "*"
rc share download preview/rel-check/<file> --expire 1h # presigned URL fetchable via curl
rc rm preview/rel-check/<file> && rc rm -r --force preview/rel-check/dir
rc rb preview/rel-check
rc admin user list preview/
rc admin user add preview/ relcheckuser relchecksecret12
rc admin user remove preview/ relcheckuser
rc alias remove preview
```
- Any FAIL blocks the release. Afterwards stop the server and delete the scratch data directory.
@@ -4,17 +4,16 @@ description: "Prepare the version-file and release-asset bump for an exact RustF
---
# RustFS Release Version Bump
Use this skill to publish a RustFS release (alpha, beta, or stable) with a minimal, auditable diff and a complete ship flow (`edit -> verify -> commit -> push -> PR`).
Use this skill to prepare and verify release version files. Commit, push, and PR steps apply only when included in the user's delivery scope; publishing release tags belongs to `rustfs-release-publish`.
Validated baseline: release pattern used in PR `#2957`.
## Required inputs
- Exact target version, for example `1.0.0-beta.4`.
- Delivery scope:
- Local only (`edit/verify`).
- Local + git (`commit/push`).
- Full GitHub flow (`commit/push/PR`).
- Delivery scope: local (`edit/verify`), git (`commit/push`), or GitHub
(`commit/push/PR`). Derive it from the conversation; when unspecified, prepare
and verify locally without blocking on a delivery question.
If target version is missing or ambiguous, stop and ask before editing.
@@ -23,7 +22,7 @@ Reject any target version containing `-preview`: preview identifiers are tag-onl
## Read before editing
- `AGENTS.md` (root and nearest path-specific files).
- `.github/pull_request_template.md`.
- `.github/pull_request_template.md` only when preparing a PR.
- Current branch status and diff against `origin/main`.
## Default release file scope
@@ -50,8 +49,7 @@ Only drop a file when the current repository release process clearly no longer r
## Step-by-step workflow
1. Confirm intent and isolate scope
- Confirm target version string exactly.
- Confirm whether user requested local-only or full GitHub flow.
- Use the exact target and delivery scope already supplied; ask only for a missing or ambiguous target or a material release-policy choice.
- Inspect current branch and ensure only release-related files are touched for this task.
2. Update workspace versions
@@ -82,18 +80,18 @@ Only drop a file when the current repository release process clearly no longer r
4. Verify before shipping
- Run:
- `make pre-commit`
- If `make pre-commit` fails, return `BLOCKED` with root cause and do not silently widen scope to fix unrelated issues unless user asks.
- If `make pre-commit` fails, fix task-attributable failures and rerun affected checks. Report unresolved required checks as `BLOCKED`; do not silently widen scope to fix unrelated issues.
5. Commit strategy
5. Commit strategy (only when committing is authorized)
- Preferred split when both parts changed:
- `chore(release): prepare <version>` for `Cargo.toml` and `Cargo.lock`.
- `chore(release): align release assets for <version>` for docs and packaging files.
- If user asks for one commit, use one commit.
- Stage only intended release files; do not include unrelated working tree changes.
6. Push and PR
6. Push and PR (only for the authorized delivery scope)
- Push branch:
- `git push -u origin <branch>` (first push), or `git push` (tracking already exists).
- Use the user-requested or configured push remote: `git push -u <push-remote> <branch>` (first push), or `git push` when tracking is already configured.
- Create PR with template headings unchanged:
- `gh pr create --base main --head <branch> --title ... --body-file ...`
- PR title/body must be English.
@@ -12,8 +12,9 @@ matched security surface, the concise security reference under
## Workflow
1. Freeze the exact diff/head and identify the changed trust boundaries.
2. Read [advisory-patterns.md](references/advisory-patterns.md), then apply only
the matching sections. Useful headings are
2. Inspect the headings in [advisory-patterns.md](references/advisory-patterns.md),
then read the matching sections. Read the full map only for a broad security
audit. Useful headings are
auth/admin, IAM/STS/OIDC, policy/plugins, S3/copy/multipart, protocols, paths,
secrets/logging/RPC, browser/CORS/proxy, SSE, Object Lock, and serde.
3. Trace unauthenticated, low-privilege, wrong-action/owner/bucket, malformed,
@@ -48,6 +48,7 @@ Update this file only when an advisory adds or changes a reusable lesson, affect
### S3 object actions, copy, multipart, and upload policy validation
- `GHSA-g8w9-qw9q-fghr` and `GHSA-xm99-m3gq-83g8`: a valid presigned `PutObject` accepted extra `x-amz-*` headers omitted from `SignedHeaders`; `x-amz-copy-source` could turn that upload capability into a cross-bucket read performed as the signer. Lesson: a presigned URL is a bounded capability; inspect all received security-sensitive headers and reject unsigned ones before selecting the S3 operation or reaching storage.
- `GHSA-3ppv-fx5m-m749`: explicit `versionId` reads and copy sources authorized `s3:GetObject` instead of `s3:GetObjectVersion`. Lesson: version-specific object access must select version-specific actions for direct reads, `CopyObject`, and `UploadPartCopy`, with tests proving the backend is not reached on denial.
- `GHSA-x298-9x87-fvjq`: anonymous `ListObjectVersions` fell back to `ListBucket` and returned before public-access-block gates. Lesson: compatibility fallbacks must converge on the same post-authorization checks as direct grants, especially `RestrictPublicBuckets` and anonymous data-plane denies.
- `GHSA-mx42-j6wv-px98`: `UploadPartCopy` missed source authorization and allowed cross-bucket object exfiltration. Lesson: multipart copy must enforce the same source and destination contract as `CopyObject`.
@@ -107,9 +108,9 @@ Update this file only when an advisory adds or changes a reusable lesson, affect
### Serde deserialization and input validation
- No `#[serde(deny_unknown_fields)]` found across the entire codebase. Lesson: all structs deserialized from untrusted input (S3 API XML/JSON, lifecycle rules, bucket policies, replication configs) should have `#[serde(deny_unknown_fields)]` to reject malformed or adversarial payloads.
- Reject unknown fields in untrusted S3 API XML/JSON, lifecycle, policy, and replication input where compatibility permits. Check the current type and supported payload fixtures; do not infer the repository's current coverage from an older audit. Where extra fields are part of the compatibility contract, validate security-critical values explicitly.
- `#[serde(default)]` on security-critical fields silently accepts missing values as zero/empty. Lesson: when a field has security implications (retention days, permissions, limits), validate the deserialized value explicitly rather than relying on defaults.
- Integer fields deserialized from user input and cast with `as` (e.g., `i32 as u32`) can wrap negative values to large positives. Lesson: validate ranges before casting; use `try_into()` or clamp.
- Integer fields deserialized from user input and cast with `as` (e.g., `i32 as u32`) can wrap negative values to large positives. Lesson: validate ranges before casting; use `try_into()` with a typed error, or clamp only when the domain explicitly requires saturation.
- XML config typos (e.g., `"NoncurentDays"` instead of `"NoncurrentDays"`) are silently accepted when `deny_unknown_fields` is absent. Lesson: strict deserialization prevents silent misconfiguration that could cause data loss or unexpected retention behavior.
## Useful Search Seeds
@@ -119,7 +120,7 @@ Use these targeted searches when a diff touches security-sensitive code:
```bash
rg -n "validate_admin_request|check_permissions|AdminAction::|deny_only|is_allowed" rustfs crates
rg -n "authorize_operation|FtpsDriver|SftpDriver|RETR|MKD|SIZE|MDTM|CreateBucket|GetObject|HeadObject" crates/protocols rustfs
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|content-length-range|starts-with" rustfs crates
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|presign|SignedHeaders|x-amz-copy-source|collect_signed_headers|content-length-range|starts-with" rustfs crates
rg -n "ListBucketVersions|GetObjectVersion|versionId|VersionId|ExistingObjectTag|ForAllValues|ForAnyValue|POLICY_PLUGIN|opa" rustfs crates
rg -n "normalize_extract_entry_key|Snowball|auto-extract|PathBuf::join|canonicalize|\\.\\.|x-forwarded-for|x-real-ip|SourceIp" rustfs crates
rg -n "DEFAULT_SECRET|DEFAULT_ACCESS|TEST_PRIVATE_KEY|rustfs rpc|RUSTFS_RPC_SECRET" rustfs crates
@@ -136,6 +137,7 @@ rg -n "deny_unknown_fields|serde.default|as u32|as usize|as i32" rustfs crates
- Protocol frontend authz fixes: include denied `RETR`, `SIZE`/`MDTM`, `MKD`, bucket probe, and sibling allowed-operation cases, and assert denied paths do not reach the storage backend.
- IAM fixes: include import/update/list service-account cases with attacker-controlled parent, claims, access key, secret key, and policy.
- Copy/upload fixes: include cross-bucket, cross-user, source-denied, destination-denied, copy-source-condition, and multipart completion cases.
- Presigned upload fixes: include a valid presign with extra unsigned tagging, redirect, storage-class, and cross-bucket copy-source headers; require rejection before source or destination storage access, and verify explicitly signed equivalents still work.
- Version-action fixes: include historical UUID, explicit current version, `null`, range, partNumber, presigned, STS/session, service-account, anonymous bucket-policy, copy source, and multipart-copy source cases.
- Policy-condition fixes: include reserved-key header collisions, missing keys, partially overlapping multi-value sets, plugin mode, and built-in policy mode.
- Path fixes: include encoded traversal, absolute path, nested traversal, archive entries with `..`, valid object keys that resemble traversal text but should be rejected, and canonical bucket/prefix boundary checks.
+28 -37
View File
@@ -1,6 +1,6 @@
---
name: test-coverage-improver
description: Run project coverage checks, rank high-risk gaps, and propose high-impact tests to improve regression confidence for changed and critical code paths before release.
description: Analyze a supplied coverage report or perform an explicitly requested RustFS coverage assessment, rank uncovered risks, and propose focused tests. Do not trigger for ordinary implementation verification, a single regression test, documentation wording, or release preparation without a coverage request.
---
# Test Coverage Improver
@@ -9,58 +9,49 @@ Use this skill when you need a prioritized, risk-aware plan to improve tests fro
## Usage assumptions
- Focus scope is either changed lines/files, a module, or the whole repository.
- Coverage artifact must be generated or provided in a supported format.
- Reuse a supplied coverage artifact when its revision, scope, and format match.
- If required context is missing, call out assumptions explicitly before proposing work.
## Workflow
1. Define scope and baseline
- Confirm target language, framework, and branch.
- Confirm whether the scope is changed files only or full-repo.
- Derive the revision and scope from the request, diff, or supplied report.
- Default to the affected files/module; whole-workspace coverage requires that
scope in the request. Ask only if a wrong scope would change the result.
2. Produce coverage snapshot
- Rust: `cargo llvm-cov` (or `cargo tarpaulin`) with existing repo config.
- JavaScript/TypeScript: `npm test -- --coverage` and read `coverage/coverage-final.json`.
- Python: `pytest --cov=<pkg> --cov-report=json` and read `coverage.json`.
- Collect total, per-file, and changed-line coverage.
2. Obtain coverage evidence
- First inspect a matching existing artifact; do not regenerate it merely
because this skill was selected.
- If measurement is needed, read the Coverage section of
[the testing guide](../../../docs/testing/README.md#coverage), check disk
space/tool availability, and select package/test-scoped `cargo llvm-cov`
using the repository's nextest configuration. `make coverage` measures the
whole workspace (excluding E2E) and is only for that requested scope.
- Collect only metrics the report supports. Missing branch/changed-line
coverage is unknown, not zero.
- If measurement cannot run, continue with code-based test proposals and
mark measured coverage unverified; do not invent a coverage percentage.
3. Rank highest-risk gaps
- Prioritize changed code, branch coverage gaps, and low-confidence boundaries.
- Apply the risk rubric in [coverage-prioritization.md](references/coverage-prioritization.md).
- Keep shortlist to 5–8 gaps.
- Report up to 5–8 evidenced gaps; do not pad a small scope.
- For each gap, capture: file, lines, uncovered branches, and estimated risk score.
4. Propose high-impact tests
- For each shortlisted gap, output:
- Intent and expected behavior.
- Normal, edge, and failure scenarios.
- Assertions and side effects to verify.
- Setup needs (fixtures, mocks, integration dependencies).
- Estimated effort (`S/M/L`).
- For each gap, name the behavior and regression, distinguishing assertions,
relevant normal/edge/failure cases, necessary setup, and estimated effort.
- Include only scenarios and setup that apply; reuse shared fixture details.
5. Close with validation plan
- State which gaps remain after proposals.
- Provide concrete verification command and acceptance threshold.
- Give a scoped verification command and behavior-based acceptance criterion;
use a coverage threshold only when the task or repository requires one.
- List assumptions or blockers (environment, fixtures, flaky dependencies).
## Output template
## Report
### Coverage Snapshot
- total / branch coverage
- changed-file coverage
- top missing regions by size
### Top Gaps (ranked)
- `path:line-range` | risk score | why critical
### Test Proposals
- `path:line-range`
- Test name
- scenarios
- assertions
- effort
### Validation Plan
- command
- pass criteria
- remaining risk
Summarize the supported metrics, then combine each ranked gap with its proposed
test and validation. Include source lines only when supplied or inspected;
mark missing metrics or locations as unknown. Do not duplicate gaps and tests
in separate templates or fill empty categories for an otherwise small report.
@@ -1,4 +1,4 @@
interface:
display_name: "Test Coverage Improver"
short_description: "Find top uncovered risk areas and propose high-impact tests."
default_prompt: "Run coverage checks, identify largest gaps, and recommend highest-impact test cases to improve risk coverage."
default_prompt: "Use $test-coverage-improver to analyze coverage for the requested scope, reuse matching reports, and propose tests for evidenced risks."
+5 -2
View File
@@ -5,8 +5,11 @@ description: Debug ILM tiering / lifecycle transition issues — NoSuchVersion o
# Tier / ILM Debugging
Full playbook: [docs/operations/tier-ilm-debugging.md](../../../docs/operations/tier-ilm-debugging.md)
— read it before changing tier code.
Playbook: [docs/operations/tier-ilm-debugging.md](../../../docs/operations/tier-ilm-debugging.md).
Read the section matching the symptom: metadata/`xl.meta`, runtime versionId,
manual jobs, or retained-record recovery. Read the local-first expiry invariant
before changing cleanup ordering. Before a reconcile/disposition action, read
its entire procedure and retain its exact-evidence and confirmation gates.
Quick moves:
+5
View File
@@ -0,0 +1,5 @@
# Bound individual tool outputs retained in context; retrieve relevant ranges
# from task-owned log files when more evidence is needed.
# https://learn.chatgpt.com/docs/config-file/config-reference
tool_output_token_limit = 4000
+2
View File
@@ -0,0 +1,2 @@
sha256-linux=21ab66fd172018682ce825a53f26aa8f286d57f3f687d8616d1850e56474dcd8
sha256-darwin=21ab66fd172018682ce825a53f26aa8f286d57f3f687d8616d1850e56474dcd8
+2 -2
View File
@@ -1,2 +1,2 @@
sha256-darwin=d6aa36cfaae2c4d8590482c7e47138c5965b335b34a75f50d11ffc3366e9021e
sha256-linux=c8315465f50c194faee36141cdbb1e15e59271e524d948564a69e2d5eb408f2a
sha256-darwin=9e9687ded961aa4e619a15def6e1ee0a5153be05423d009ee7a58126624c83e7
sha256-linux=364666693c2bf258997643c9d87e07e7b7501e58175f190d201d53c81946eb20
+2 -1
View File
@@ -1 +1,2 @@
sha256=9b9bc336b43b70d0e06e0adb5455bf035bb18945d85d60936eb6fe4d48e0e680
sha256-darwin=83a7dcaffd5a789517ae9f02a224f66a9713937885cff96fca2ad7e216f197ae
sha256-linux=626c10f8c964507ff987b6c86069e9019dc6d2ae7fb02db9be5df5aa8cc5145b
+1
View File
@@ -0,0 +1 @@
sha256=87c05c46d611ea7ed3feb5f7276bda8e5a0f70d72165d305d73a457907e7ba79
+1 -1
View File
@@ -1 +1 @@
sha256=655a3f3c1d042e694339d15caba7580518320322d1bac0f09450b37e6c09e2e7
sha256=e4ee3898b840a8374cb02c64342cc15f8d7ecd1becaf4b4a2a4657290f9e7ff1
+1 -1
View File
@@ -1 +1 @@
sha256=294350518743cac8d7c41880a2835216e4b697908d7b0b1bc92b62816d94c59d
sha256=548a58d74f3cb3b5a1ab9dcd4d3f8625b9ddebe847a2e14de1d08c3b2e6292eb
+87
View File
@@ -0,0 +1,87 @@
{
"lane": "ci/test-and-lint",
"tests": [
{
"invariant": "write-quorum",
"suite": "rustfs-ecstore",
"name": "set_disk::ops::object::inline_put_commit_path_tests::inline_put_direct_commit_accepts_exact_quorum_and_rejects_quorum_minus_one"
},
{
"invariant": "metadata-rollback",
"suite": "rustfs-ecstore",
"name": "set_disk::core::io_primitives::tests::write_unique_file_info_reverts_metadata_when_write_quorum_fails"
},
{
"invariant": "stale-writer",
"suite": "rustfs-ecstore",
"name": "set_disk::ops::object::put_object_tmp_cleanup_tests::put_object_no_lock_aborts_after_outer_namespace_lock_loss"
},
{
"invariant": "range-body",
"suite": "rustfs-ecstore",
"name": "set_disk::ops::object::transition_upload_integrity_tests::transitioned_compressed_object_range_get_returns_plaintext_slice"
},
{
"invariant": "multipart-cancellation",
"suite": "rustfs-ecstore",
"name": "set_disk::ops::multipart::tests::cancelled_complete_keeps_upload_lock_through_tail_cleanup"
},
{
"invariant": "list-uncommitted-version",
"suite": "rustfs-filemeta",
"name": "metacache::tests::resolve_with_write_quorum_slack_keeps_partial_latest_hidden_during_merge"
},
{
"invariant": "minio-object-fixture",
"suite": "rustfs-filemeta",
"name": "filemeta::test::parses_real_minio_object_xlmeta"
},
{
"invariant": "corrupt-part-arrays",
"suite": "rustfs-filemeta",
"name": "filemeta::test::crc_valid_but_part_arrays_corrupt_into_fileinfo_errors_not_panics"
},
{
"invariant": "odm-source-contract-s3",
"suite": "rustfs",
"name": "on_demand_migration::source_client::tests::s3_backend_satisfies_the_shared_backend_contract"
},
{
"invariant": "odm-source-contract-azure",
"suite": "rustfs",
"name": "on_demand_migration::azure::tests::azure_backend_satisfies_the_shared_backend_contract"
},
{
"invariant": "odm-source-contract-gcs",
"suite": "rustfs",
"name": "on_demand_migration::gcs::tests::gcs_native_backend_satisfies_the_shared_backend_contract"
}
],
"fixtures": [
{
"path": "crates/filemeta/tests/fixtures/minio/object_large_bin.xlmeta.hex",
"sha256": "e8093767806d701e639b48d023190e858fbc4cde69bcfd83c22af8cba8452ce5",
"source": "MinIO RELEASE.2025-07-23T15-54-02Z; crates/ecstore/tests/fixtures/minio/README.md"
},
{
"path": "crates/filemeta/tests/fixtures/minio/object_small_txt.xlmeta.hex",
"sha256": "2a415ad3a3be5a9440035d4026ff880e0e8c1ec1701be9f4e077734e8dce03da",
"source": "MinIO RELEASE.2025-07-23T15-54-02Z; crates/ecstore/tests/fixtures/minio/README.md"
},
{
"path": "crates/filemeta/tests/fixtures/minio/object_versioned_txt.xlmeta.hex",
"sha256": "7f21f50c326dd8b0228deb6dbdb7052b3d0a3f8ee6c85d43486f0e6bb7a97261",
"source": "MinIO RELEASE.2025-07-23T15-54-02Z; crates/ecstore/tests/fixtures/minio/README.md"
},
{
"path": "crates/ecstore/tests/fixtures/minio/bucket_metadata.blob.hex",
"sha256": "f2b6e260aff106adf6039feb1c645686e84e75404ff725491fb18668be5db203",
"source": "MinIO RELEASE.2025-07-23T15-54-02Z; crates/ecstore/tests/fixtures/minio/README.md"
},
{
"path": "crates/ecstore/tests/fixtures/minio/bucket_metadata_full.xlmeta.hex",
"sha256": "3b6de589519c08a1614c8bd409bb8199c17d42043861b07bce513075e6fbfc12",
"source": "MinIO RELEASE.2025-07-23T15-54-02Z; crates/ecstore/tests/fixtures/minio/README.md"
}
]
}
+1
View File
@@ -0,0 +1 @@
27e9584a2d776db9416d0edfd040d524d48b5d4d
+1 -1
View File
@@ -23,4 +23,4 @@ coverage: core-deps ## Workspace line coverage (cargo-llvm-cov + nextest; slow,
@mkdir -p target/llvm-cov
cargo llvm-cov report --lcov --output-path target/llvm-cov/lcov.info
cargo llvm-cov report --json --output-path target/llvm-cov/coverage.json
python3 scripts/coverage_per_crate.py target/llvm-cov/coverage.json
$(RUSTFS_PYTHON_BIN) scripts/coverage_per_crate.py target/llvm-cov/coverage.json
+2 -1
View File
@@ -88,7 +88,8 @@ offline-enrollment-e2e-check: core-deps ## Build and exercise the dedicated offl
.PHONY: test-wiring-check
test-wiring-check: ## Check tests stay registered and selected by their intended runners
@echo "🧪 Checking test wiring..."
python3 ./scripts/check_test_wiring.py
$(RUSTFS_PYTHON_BIN) ./scripts/check_test_wiring.py
$(RUSTFS_PYTHON_BIN) ./scripts/ci_gate.py --check-workflow
.PHONY: log-analyzer-rules-check
log-analyzer-rules-check: core-deps ## Check log-analyzer rule anchors still exist verbatim in source
+3 -2
View File
@@ -3,9 +3,10 @@
.NOTPARALLEL: pre-commit pre-pr dev-check
.PHONY: setup-hooks
setup-hooks: ## Set up git hooks
setup-hooks: ## Install the configured pre-commit hooks
@echo "🔧 Setting up git hooks..."
chmod +x .git/hooks/pre-commit
pre-commit validate-config
pre-commit install
@echo "✅ Git hooks setup complete!"
.PHONY: doc-paths-check
+25 -5
View File
@@ -31,18 +31,38 @@ script-tests: ## Run shell script tests
./scripts/test_object_batch_bench_enhanced.sh
./scripts/test_hotpath_warp_ab_gate.sh
./scripts/test_hotpath_warp_abba.sh
./scripts/test_scanner_validation_harness.sh
./scripts/test_scanner_heal_checkpoint_crash_evidence.sh
./scripts/test_scanner_heal_authority_evidence.sh
./scripts/test_scanner_heal_scoped_ack_evidence.sh
./scripts/test_scanner_heal_legacy_rollback_evidence.sh
./scripts/test_scanner_heal_g14_multiset_evidence.sh
./scripts/test_scanner_heal_scheduler_pressure_evidence.sh
./scripts/test_scanner_heal_status_outcome_evidence.sh
./scripts/test_scanner_heal_maintenance_evidence.sh
./scripts/test_scanner_heal_w13_mrf_evidence.sh
./scripts/test_scanner_heal_w16_recovery_evidence.sh
./scripts/test_exact_1mib_handoff_abba.sh
./scripts/test_pinned_paired_abba_bench.sh
./scripts/test_manual_transition_runbooks.sh
./scripts/test_fuzz_runner.sh
./scripts/test_python_bin.sh
./scripts/check_embedded_secrets.sh --self-test
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/check_security_coverage.py --self-test
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/s3-tests/test_report_compat.py
$(RUSTFS_PYTHON_BIN) ./scripts/check_test_wiring.py --self-test
$(RUSTFS_PYTHON_BIN) ./scripts/test_e2e_binary.py
$(RUSTFS_PYTHON_BIN) ./scripts/ci_gate.py --self-test
$(RUSTFS_PYTHON_BIN) ./scripts/check_security_coverage.py --self-test
$(RUSTFS_PYTHON_BIN) ./scripts/check_scheduled_validation_freshness.py --self-test
$(RUSTFS_PYTHON_BIN) ./scripts/test_security_workflow.py
$(RUSTFS_PYTHON_BIN) ./scripts/test_nightly_candidate.py
$(RUSTFS_PYTHON_BIN) ./scripts/test_functional_chain.py
$(RUSTFS_PYTHON_BIN) ./scripts/test_functional_chain_health.py
$(RUSTFS_PYTHON_BIN) ./scripts/test_ci_timing_report.py
$(RUSTFS_PYTHON_BIN) ./scripts/s3-tests/test_report_compat.py
bash -n ./scripts/validate_object_data_cache_cold_stampede.sh
python3 ./scripts/check_object_data_cache_follower_samples.py --self-test
$(RUSTFS_PYTHON_BIN) ./scripts/check_object_data_cache_follower_samples.py --self-test
./scripts/validate_object_data_cache_cold_stampede.sh --self-test
./scripts/run_scanner_heal_evidence_case.sh --self-test
.PHONY: test
test: core-deps script-tests ## Run all tests (needs cargo-nextest; RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to override)
+5 -4
View File
@@ -1,10 +1,11 @@
# 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.
# The floor equals the exact count of rustfs-ecstore --lib tests, with the
# test-util feature enabled, 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
946
+255 -26
View File
@@ -46,6 +46,11 @@ e2e-reliability = { max-threads = 1 }
e2e-inline-boundaries = { max-threads = 1 }
e2e-cluster-nightly = { max-threads = 1 }
# Deep async storage futures are composed into tests across several crates.
# Keep the test stack bounded but above libtest's 2 MiB default.
[scripts.setup.ecstore-base-stack]
command = ['sh', '-c', 'echo RUST_MIN_STACK=4194304 >> "$NEXTEST_ENV"']
# These exact regression scenarios build deep async storage futures that exceed
# libtest's 2 MiB spawned-thread stack on Linux. Give only their test processes
# the same 32 MiB stack already used by the crate's dedicated large-stack tests.
@@ -60,13 +65,23 @@ command = ['sh', '-c', 'echo RUST_MIN_STACK=33554432 >> "$NEXTEST_ENV"']
# --- default profile (local): serialize the flaky groups, never retry --------
[[profile.default.scripts]]
filter = 'package(rustfs-ecstore) & test(/^(bucket::lifecycle::bucket_lifecycle_ops::tests::manual_transition_worker_result_recovery_marks_unknown_for_corrupt_marker|services::rebalance::entry::tests::real_rebalance_run_fence_loss_blocks_multipart_publication|store::init::tests::(decommission_entry_(allows_free_version_consumed_before_source_lock|rejects_subquorum_free_version_conflict_and_retains_source|skips_cleanup_only_marker_when_free_version_is_present)|prepared_tier_delete_recovery_(checks_later_pool_then_commits_after_source_removal|finds_directory_source_on_encoded_set|retains_journal_on_source_metadata_error)|tier_mutation_peer_handler_applies_prepare_commit_and_abort_idempotently|transition_response_loss_persists_unknown_outcome_for_provider_recovery|transition_transaction_recovery_(drops_record_after_confirmed_local_commit|keeps_cleanup_pending_local_commit)))$/)'
filter = 'package(rustfs-ecstore) & test(/^(bucket::lifecycle::bucket_lifecycle_ops::tests::manual_transition_worker_result_recovery_marks_unknown_for_corrupt_marker|services::rebalance::entry::tests::real_rebalance_run_fence_loss_blocks_multipart_publication|store::init::tests::(batch_transitioned_delete_uses_free_version_per_item|decommission_entry_(allows_free_version_consumed_before_source_lock|rejects_subquorum_free_version_conflict_and_retains_source|skips_cleanup_only_marker_when_free_version_is_present)|dispatched_tier_delete_recovery_(checks_later_pool_then_commits_after_source_removal|finds_directory_source_on_encoded_set|retains_journal_on_source_metadata_error)|force_tier_remove_blocks_on_physical_free_version_hidden_by_other_pool|legacy_unknown_transition_delete_falls_back_for_single_batch_and_blocks_prefix|multi_pool_(recursive_prefix_rejects_legacy_or_hidden_merge_loser_before_delete|same_remote_tuple_(batch|single)_delete_waits_for_all_sources|same_tuple_recursive_prefix_uses_one_journal_owner|transitioned_delete_persists_one_free_version_per_remote_tuple)|recursive_prefix_partial_(pool|set)_failure_keeps_prepared_cleanup_owners|restored_transitioned_delete_uses_free_version_as_cleanup_owner|stable_transitioned_recursive_prefix_delete_uses_journal_owners|suspended_null_transition_delete_uses_free_version_as_sole_owner|tier_mutation_peer_handler_applies_prepare_commit_and_abort_idempotently|transition_response_loss_persists_unknown_outcome_for_provider_recovery|transition_transaction_recovery_(drops_record_after_confirmed_local_commit|keeps_cleanup_pending_local_commit)|transitioned_delete_(free_version_replays_after_store_restart|local_quorum_failure_rolls_back_without_cleanup_owner|uses_free_version_as_cleanup_owner)|versioned_delete_marker_keeps_transitioned_source_and_remote_object|versioned_explicit_transition_delete_preserves_other_version_then_allows_bucket_delete))$/)'
setup = 'ecstore-large-stack'
[[profile.default.scripts]]
filter = 'package(rustfs-ecstore) | package(rustfs-s3select-api) | package(rustfs-scanner) | (package(rustfs) & test(/^(app::multipart_usecase::tests::concurrent_completions_share_durable_bucket_quota_reservations|app::object::delete::tests::compressed_delete_requests_update_observed_usage_without_releasing_quota_floor|app::object::internal_put::tests::internal_multipart_roundtrip_completes_and_abort_leaves_nothing|app::object::restore::tests::execute_restore_object_maps_failures_to_typed_s3_errors|storage::access::tests::(delete_object_access_captures_authorized_bucket_incarnation|copy_operations_reject_recreated_source_bucket_after_authorization|request_slot_keeps_bucket_policy_bound_to_its_store))$/))'
setup = 'ecstore-base-stack'
[[profile.default.scripts]]
filter = 'binary(lifecycle_integration_test) | (package(rustfs) & test(/^app::lifecycle_transition_api_test::/))'
setup = 'lifecycle-large-stack'
# The pool metadata placement regression composes the full erasure-heal
# startup future and overflows the default Linux test-thread stack.
[[profile.default.scripts]]
filter = 'package(rustfs-heal) & test(=replacement_pool_metadata_follows_real_two_set_placement)'
setup = 'ecstore-large-stack'
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & (test(concurrent_resend_same_part_commits_one_generation) | test(concurrent_config_writes_from_separate_nodes_do_not_lose_writes) | test(/^store::bucket::tests::bucket_delete_(mark_delete|purge_removes|default_s3_delete)/))'
test-group = 'ecstore-serial-flaky'
@@ -80,6 +95,29 @@ test-group = 'ecstore-serial-flaky'
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
test-group = 'ecstore-serial-flaky'
# Serialize the heal result-report tests. Every test in the module builds a
# real-disk (TempDir-backed) hermetic erasure set and drives MiB-scale writes
# plus deep-scan heal — the same load-sensitive cross-disk IO shape as the
# crash_consistency scenarios above. Under a heavily parallel run a single
# disk's IO can fail while write quorum still holds, which flips per-disk
# readback and aggregate-outcome assertions nondeterministically (different
# tests each round; all pass standalone). Preventive serialization only, no
# retries. The matching ci-profile override is after [profile.ci].
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::heal::heal_result_report_tests::/)'
test-group = 'ecstore-serial-flaky'
# Serialize the metadata-cache generation-retirement pair. Both carry
# #[serial(metadata_cache_invalidation_probe)] — a no-op across nextest's
# process boundary — and assert get_object_metadata_cache generation
# semantics on a 4-disk hermetic set, the same load-sensitive shape that
# forced the transition matrix tests into this group. Preventive
# serialization only, no retries. The matching ci-profile override is after
# [profile.ci].
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(retires_cached_snapshot)'
test-group = 'ecstore-serial-flaky'
# The production-handler relocation regression builds an isolated 8-disk,
# 2-pool store and commits a 72 MiB multipart object. Keep that cross-disk IO
# from overlapping the ecstore commit fixtures above.
@@ -94,18 +132,50 @@ test-group = 'ecstore-serial-flaky'
filter = 'package(rustfs) & (binary(/^embedded.*_test$/) | binary(admin_diagnostic_capability_e2e))'
test-group = 'embedded-test-ports'
# The real object probe includes bucket creation and cleanup in its short
# measurement budget. Keep competing storage fixtures outside that budget.
[[profile.default.overrides]]
filter = 'package(rustfs) & binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)'
threads-required = "num-test-threads"
# Serialize the durable manual-transition checkpoint test across nextest's
# process boundary; it mutates bucket lifecycle metadata and is not quarantined.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
test-group = 'ecstore-serial-flaky'
# Serialize the transition matrix tests. They build a 4-disk hermetic erasure
# set, populate the get_object_metadata_cache, and assert generation lifecycle
# semantics. serial_test's #[serial] has no effect across nextest's process
# boundary, so concurrent execution races the shared metadata-cache generation
# counter and causes spurious "metadata read should publish the generation"
# panics. Preventive serialization, no retries.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(set_disk::transition_matrix_tests::)'
test-group = 'ecstore-serial-flaky'
# The durable ILM decommission regressions build isolated multi-pool stores and
# deliberately take source or target disks offline while checking fencing.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & (test(decommission_migrates_and_verifies_registered_durable_ilm_records) | test(decommission_durable_ilm_target_read_error_is_not_masked_by_peer_success) | test(decommission_durable_ilm_terminal_receipt_recovers_failed_source_cleanup) | test(decommission_durable_ilm_receipt_pagination_fails_closed_on_second_page) | test(decommission_durable_ilm_recovery_keeps_multiple_active_sources))'
test-group = 'ecstore-serial-flaky'
# Decommission entry and marker/barrier tests share process-wide fault hooks and
# deterministic commit barriers. Keep the whole init decommission family in one
# nextest group; serial_test alone cannot isolate separate test processes.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(/^store::init::tests::(decommission_.*|suspended_.*decommission.*)$/)'
test-group = 'ecstore-serial-flaky'
# The tier overwrite cleanup-owner matrix restarts a real 4-disk store and
# drives free-version recovery through lifecycle expiry workers. Reserve the
# full test budget so competing real-store fixtures cannot starve the walk's
# filesystem progress deadlines. Keep the production deadlines and no retries.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(tier_overwrite_put_and_self_copy_recover_persisted_cleanup_owners)'
test-group = 'ecstore-serial-flaky'
threads-required = "num-test-threads"
# Serialize the bucket-incarnation / lifecycle-fence tests. They drive
# init_bucket_metadata_sys and bucket_metadata_sys_of, i.e. process-global
# OnceLock state that serial_test's #[serial] cannot protect across nextest's
@@ -134,6 +204,13 @@ test-group = 'e2e-reliability'
filter = 'package(e2e_test) & test(/^inline_fast_path_cluster_test::/)'
test-group = 'e2e-inline-boundaries'
# 4-node 4-drive distributed Actions suite: each case starts four rustfs
# processes and up to sixteen data directories. Serialize across nextest's
# process boundary so several 4x4 clusters never overlap.
[[profile.default.overrides]]
filter = 'package(e2e_test) & test(/^distributed::/)'
test-group = 'e2e-cluster-nightly'
# Vault KMS tests share the fixed dev-server port 8200. serial_test's #[serial]
# does not cross nextest process boundaries, so keep every Vault-backed test in
# one group.
@@ -141,6 +218,12 @@ test-group = 'e2e-inline-boundaries'
filter = 'package(e2e_test) & (test(/^kms::kms_vault_test::/) | test(/^kms::kms_rekey_sweep_test::/) | test(/^kms::configured_roundtrip_test::test_configured_vault_kms_admin_and_versioned_cleanup$/))'
test-group = 'e2e-vault'
# This four-disk, 65-member rollback probe already drives up to 32 concurrent
# durable deletions. Reserve this nextest run's capacity for its progress oracle.
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(=store::init::tests::dispatch_manifest_rollback_bounded_concurrency_reaches_tail_behind_slow_member)'
threads-required = "num-test-threads"
# ---------------------------------------------------------------------------
# ci profile — the strict CI gate (ci.yml `cargo nextest run --profile ci`)
# ---------------------------------------------------------------------------
@@ -157,13 +240,23 @@ fail-fast = false
path = "junit.xml"
[[profile.ci.scripts]]
filter = 'package(rustfs-ecstore) & test(/^(bucket::lifecycle::bucket_lifecycle_ops::tests::manual_transition_worker_result_recovery_marks_unknown_for_corrupt_marker|services::rebalance::entry::tests::real_rebalance_run_fence_loss_blocks_multipart_publication|store::init::tests::(decommission_entry_(allows_free_version_consumed_before_source_lock|rejects_subquorum_free_version_conflict_and_retains_source|skips_cleanup_only_marker_when_free_version_is_present)|prepared_tier_delete_recovery_(checks_later_pool_then_commits_after_source_removal|finds_directory_source_on_encoded_set|retains_journal_on_source_metadata_error)|tier_mutation_peer_handler_applies_prepare_commit_and_abort_idempotently|transition_response_loss_persists_unknown_outcome_for_provider_recovery|transition_transaction_recovery_(drops_record_after_confirmed_local_commit|keeps_cleanup_pending_local_commit)))$/)'
filter = 'package(rustfs-ecstore) & test(/^(bucket::lifecycle::bucket_lifecycle_ops::tests::manual_transition_worker_result_recovery_marks_unknown_for_corrupt_marker|services::rebalance::entry::tests::real_rebalance_run_fence_loss_blocks_multipart_publication|store::init::tests::(batch_transitioned_delete_uses_free_version_per_item|decommission_entry_(allows_free_version_consumed_before_source_lock|rejects_subquorum_free_version_conflict_and_retains_source|skips_cleanup_only_marker_when_free_version_is_present)|dispatched_tier_delete_recovery_(checks_later_pool_then_commits_after_source_removal|finds_directory_source_on_encoded_set|retains_journal_on_source_metadata_error)|force_tier_remove_blocks_on_physical_free_version_hidden_by_other_pool|legacy_unknown_transition_delete_falls_back_for_single_batch_and_blocks_prefix|multi_pool_(recursive_prefix_rejects_legacy_or_hidden_merge_loser_before_delete|same_remote_tuple_(batch|single)_delete_waits_for_all_sources|same_tuple_recursive_prefix_uses_one_journal_owner|transitioned_delete_persists_one_free_version_per_remote_tuple)|recursive_prefix_partial_(pool|set)_failure_keeps_prepared_cleanup_owners|restored_transitioned_delete_uses_free_version_as_cleanup_owner|stable_transitioned_recursive_prefix_delete_uses_journal_owners|suspended_null_transition_delete_uses_free_version_as_sole_owner|tier_mutation_peer_handler_applies_prepare_commit_and_abort_idempotently|transition_response_loss_persists_unknown_outcome_for_provider_recovery|transition_transaction_recovery_(drops_record_after_confirmed_local_commit|keeps_cleanup_pending_local_commit)|transitioned_delete_(free_version_replays_after_store_restart|local_quorum_failure_rolls_back_without_cleanup_owner|uses_free_version_as_cleanup_owner)|versioned_delete_marker_keeps_transitioned_source_and_remote_object|versioned_explicit_transition_delete_preserves_other_version_then_allows_bucket_delete))$/)'
setup = 'ecstore-large-stack'
[[profile.ci.scripts]]
filter = 'package(rustfs-ecstore) | package(rustfs-s3select-api) | package(rustfs-scanner) | (package(rustfs) & test(/^(app::multipart_usecase::tests::concurrent_completions_share_durable_bucket_quota_reservations|app::object::delete::tests::compressed_delete_requests_update_observed_usage_without_releasing_quota_floor|app::object::internal_put::tests::internal_multipart_roundtrip_completes_and_abort_leaves_nothing|app::object::restore::tests::execute_restore_object_maps_failures_to_typed_s3_errors|storage::access::tests::(delete_object_access_captures_authorized_bucket_incarnation|copy_operations_reject_recreated_source_bucket_after_authorization|request_slot_keeps_bucket_policy_bound_to_its_store))$/))'
setup = 'ecstore-base-stack'
[[profile.ci.scripts]]
filter = 'binary(lifecycle_integration_test) | (package(rustfs) & test(/^app::lifecycle_transition_api_test::/))'
setup = 'lifecycle-large-stack'
# The pool metadata placement regression composes the full erasure-heal
# startup future and overflows the default Linux test-thread stack.
[[profile.ci.scripts]]
filter = 'package(rustfs-heal) & test(=replacement_pool_metadata_follows_real_two_set_placement)'
setup = 'ecstore-large-stack'
# ===========================================================================
# QUARANTINE — flaky tests granted retries = 2 under the ci profile ONLY.
#
@@ -220,22 +313,55 @@ test-group = 'e2e-reliability'
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
test-group = 'ecstore-serial-flaky'
# Serialize the heal result-report tests under the ci profile too (see the
# matching default-profile override near the top). Not a quarantine: no
# retries, just serialized real-disk heal IO.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::heal::heal_result_report_tests::/)'
test-group = 'ecstore-serial-flaky'
# Serialize the metadata-cache generation-retirement pair under the ci
# profile too (see the matching default-profile override near the top). Not a
# quarantine: no retries.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(retires_cached_snapshot)'
test-group = 'ecstore-serial-flaky'
# Match the default-profile embedded test isolation without quarantining or
# retrying failures in CI.
[[profile.ci.overrides]]
filter = 'package(rustfs) & (binary(/^embedded.*_test$/) | binary(admin_diagnostic_capability_e2e))'
test-group = 'embedded-test-ports'
[[profile.ci.overrides]]
filter = 'package(rustfs) & binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)'
threads-required = "num-test-threads"
# Serialize the durable manual-transition checkpoint test under the ci profile
# too. No retries: failures stay visible.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
test-group = 'ecstore-serial-flaky'
# Serialize the transition matrix tests under the ci profile too (see the
# matching default-profile override near the top). No retries.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(set_disk::transition_matrix_tests::)'
test-group = 'ecstore-serial-flaky'
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & (test(decommission_migrates_and_verifies_registered_durable_ilm_records) | test(decommission_durable_ilm_target_read_error_is_not_masked_by_peer_success) | test(decommission_durable_ilm_terminal_receipt_recovers_failed_source_cleanup) | test(decommission_durable_ilm_receipt_pagination_fails_closed_on_second_page) | test(decommission_durable_ilm_recovery_keeps_multiple_active_sources))'
test-group = 'ecstore-serial-flaky'
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(/^store::init::tests::(decommission_.*|suspended_.*decommission.*)$/)'
test-group = 'ecstore-serial-flaky'
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(tier_overwrite_put_and_self_copy_recover_persisted_cleanup_owners)'
test-group = 'ecstore-serial-flaky'
threads-required = "num-test-threads"
# Serialize the bucket-incarnation / lifecycle-fence tests under the ci profile
# too (see the matching default-profile override near the top). No retries.
[[profile.ci.overrides]]
@@ -278,7 +404,8 @@ test-group = 'ecstore-serial-flaky'
# allowlist", so any new replication test lands in nightly by default (never
# silently unrun) until it is explicitly blessed as fast here. Keep the two
# regexes byte-identical. The committed profile selection digests make changes
# visible in CI; current counts live in docs/testing/e2e-suite-inventory.md.
# visible in CI; list current membership with `cargo nextest list -p e2e_test
# --profile <profile>` (platform-dependent; see docs/testing/README.md).
# HISTORY (2026-07-11): the 20 fast tests were briefly pulled out of this lane
# (#4724) because they set a loopback (127.0.0.1) replication target that the
# SSRF egress guard rejected on every PR after repl-1 (#4712). That is fixed —
@@ -316,13 +443,32 @@ test-group = 'ecstore-serial-flaky'
# rustfs/rustfs#5169 disabled them) have PR-lane signal, not just merge-gate.
# Single-node servers on random ports with isolated temp dirs — meets the
# admission criteria unchanged.
#
# On-demand migration GA (backlog#2163 ODM-16): three named cases join the
# lane, one per user-visible contract of the feature — a GET miss that pulls
# the object and persists it locally, a HEAD miss that answers from the source
# and stores nothing, and the admin config/status pair that must redact the
# source secret. Each spawns one single-node rustfs server plus the in-process
# fake S3 source (`fake_s3_target`, already in the first clause), so they meet
# the admission criteria unchanged; measured at 15.8 s / 15.8 s / 15.9 s, which
# is entirely the shared server startup and overlaps the lane's other tests.
# The rest of `on_demand_migration::{get_basic,interaction,backfill,
# harness_self}_test` stays in e2e-full and the fault / concurrency /
# real-source modules stay in e2e-nightly; this is an allowlist, not a module
# clause, so a new ODM test never lands here silently.
#
# Scanner authoritative usage publication (backlog#2213): data_usage_test is
# the PR-lane e2e coverage for scanner usage snapshots consumed by quota and
# admin surfaces. It uses the same single-node, random-port, isolated-temp-dir
# fixture as the existing smoke modules.
[profile.e2e-smoke]
default-filter = """
package(e2e_test) & (
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|list_buckets_auth|list_buckets_iam_filter|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|compression|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat)_test::|^fake_s3_target::/)
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|list_buckets_auth|list_buckets_iam_filter|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|compression|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat|data_usage)_test::|^fake_s3_target::/)
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
| test(/^reliant::lifecycle::/)
| test(/^reliant::tiering::/)
| test(/^on_demand_migration::(get_basic_test::(get_miss_pulls_inline_and_serves_locally_afterwards|head_miss_answers_from_the_source_without_persisting)|interaction_test::test_odm_admin_config_is_redacted_and_status_counts_match_the_source)$/)
)
"""
fail-fast = false
@@ -362,6 +508,10 @@ slow-timeout = { period = "60s", terminate-after = 2, grace-period = "10s" }
# until it is explicitly promoted to the fast PR subset — no replication test
# is ever silently left out of CI.
#
# replication_target_matrix_test (the outbound target matrix: every object
# shape against every remote-target failure mode the fake target models) runs
# here in full; its expectation table pins known-red cells to open issues.
#
# #[serial] does NOT serialize under nextest (process-per-test; see the file
# header). These tests need no cross-test serialization: each spawns its own
# server(s) on random ports with isolated temp dirs, so they are parallel-safe
@@ -379,7 +529,7 @@ slow-timeout = { period = "60s", terminate-after = 2, grace-period = "10s" }
[profile.e2e-repl-nightly]
default-filter = """
package(e2e_test)
& test(/^replication_extension_test::/)
& (test(/^replication_extension_test::/) | test(/^replication_target_matrix_test::/))
& !test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
"""
fail-fast = false
@@ -392,15 +542,29 @@ path = "junit.xml"
# ---------------------------------------------------------------------------
# e2e-nightly profile — destructive multi-process cluster fault domains
# ---------------------------------------------------------------------------
# These seven modules are deliberately outside e2e-full's merge budget. Each
# These eight modules are deliberately outside e2e-full's merge budget. Each
# starts a real multi-process or multi-disk topology and exercises node/disk
# loss, quorum, cleanup, notification fan-in, or admin-timeout behavior. The
# consolidated nightly workflow runs them serially to avoid resource
# starvation; failures are never retried.
#
# heal_erasure_disk_rebuild_test also runs in e2e-full so core heal rebuild
# regressions are caught no later than the merge/main lane. It remains here for
# nightly serial coverage with the other cluster fault domains.
#
# On-demand migration (backlog#2158 ODM-11) joins by the second clause: the
# fault matrix waits out the 30 s circuit-breaker window, the concurrency
# matrix drives 100-deep bursts, and the real-source cases start a second
# (loop guard: a third) RustFS process. They are too slow or too heavy for
# the merge budget; `on_demand_migration::{get_basic,interaction}_test` stay
# in e2e-full, which excludes exactly these three modules.
[profile.e2e-nightly]
default-filter = """
package(e2e_test)
& test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|heal_erasure_disk_rebuild_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
& (
test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|degraded_listing_availability_test|heal_erasure_disk_rebuild_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
| test(/^on_demand_migration::(concurrency_test|fault_test|real_source_test)::/)
)
"""
fail-fast = false
@@ -411,6 +575,62 @@ path = "junit.xml"
filter = 'package(e2e_test)'
test-group = 'e2e-cluster-nightly'
# The EC8+4 multi-set heal proof deliberately uploads a larger workload so the
# background root-heal pass can be interrupted after targeting the replacement
# drive's erasure slots. Keep the extended budget scoped to this proof case.
[[profile.e2e-nightly.overrides]]
filter = 'package(e2e_test) & test(=heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_ec84_shards_across_multi_set_after_background_target_restart)'
slow-timeout = { period = "120s", terminate-after = 12, grace-period = "10s" }
# ---------------------------------------------------------------------------
# e2e-distributed profile — 4-node 4-disk Actions suite
# ---------------------------------------------------------------------------
# Storage-sensitive PR / nightly / dispatch lane owned by
# .github/workflows/e2e-distributed.yml.
# Each case starts four rustfs processes (and for site replication, two
# clusters). Upgrade cases also require RUSTFS_UPGRADE_SOURCE_BINARY.
# Serialized via e2e-cluster-nightly with no retries.
[profile.e2e-distributed]
default-filter = 'package(e2e_test) & test(/^distributed::/)'
fail-fast = false
# Decommission / rebalance cases poll for up to 180s with little stdout.
slow-timeout = { period = "120s", terminate-after = 6 }
[profile.e2e-distributed.junit]
path = "junit.xml"
[[profile.e2e-distributed.overrides]]
filter = 'package(e2e_test)'
test-group = 'e2e-cluster-nightly'
# ---------------------------------------------------------------------------
# e2e-odm-interop profile — on-demand migration provider interop lane (ODM-20)
# ---------------------------------------------------------------------------
# backlog#2167. Report-only, scheduled, never a required check; wired by
# .github/workflows/on-demand-migration-interop.yml.
#
# The four cases in `on_demand_migration::interop_test` take their source from
# the environment (`RUSTFS_ODM_INTEROP_*`, documented on the constants in
# `crates/e2e_test/src/on_demand_migration/common.rs`), so the same bodies run
# against the in-process fake source locally and against a MinIO container or a
# real cloud provider in the lane. The cloud jobs narrow this profile with their
# own `-E` filter to the three-case minimum (GET miss, HEAD miss, merged list
# pagination) and pass `--no-tests=fail` so a rename cannot silently select
# nothing; the MinIO job runs the whole profile, backfill included.
#
# These cases are deliberately absent from every other lane: without an
# interop source they only re-prove what `get_basic_test` and
# `list_through_test` already cover in e2e-smoke and e2e-full. The committed
# selection digest is the guard against a rename dropping one of them.
[profile.e2e-odm-interop]
default-filter = 'package(e2e_test) & test(/^on_demand_migration::interop_test::/)'
fail-fast = false
[profile.e2e-odm-interop.junit]
# Emitted to target/nextest/e2e-odm-interop/junit.xml; the lane uploads it and
# reconciles it against the per-case JSON report entries.
path = "junit.xml"
# ---------------------------------------------------------------------------
# e2e-protocols profile — serial protocol lane
# ---------------------------------------------------------------------------
@@ -431,16 +651,27 @@ path = "junit.xml"
# quota, checksum, encryption,
# security-boundary, ... — that the fast PR `e2e-smoke` subset deliberately
# skips. Budget <= 45 min; authority for the suite count is `cargo nextest list
# --profile e2e-full` (see docs/testing/e2e-suite-inventory.md).
# --profile e2e-full -p e2e_test` (platform-dependent; see docs/testing/README.md).
#
# The filter is "the whole e2e_test crate MINUS the sets owned by other lanes":
# * protocols:: — FTPS/SFTP/WebDAV, run from the dedicated protocol profile
# with one worker because the suite owns fixed ports.
# * the 7 cluster suites that spin up a RustFSTestClusterEnvironment
# * cluster suites that spin up a RustFSTestClusterEnvironment
# (cluster_concurrency, cluster_multidrive_pool, stale_multipart_cleanup_cluster,
# namespace_lock_quorum, heal_erasure_disk_rebuild, admin_timeout_regression,
# object_lambda) — too heavy for the merge budget; they run in the
# e2e-nightly serial cluster-fault lane.
# namespace_lock_quorum, admin_timeout_regression, object_lambda) — too
# heavy for the merge budget; they run in the e2e-nightly serial
# cluster-fault lane. heal_erasure_disk_rebuild is intentionally not
# excluded here because backlog#2213 promotes core heal rebuild coverage to
# this merge/main lane while retaining nightly coverage.
# * distributed:: — 4-node 4-disk Actions suite (S3, lock, versioning,
# replication, quota, observability, expand/decommission/rebalance, site
# replication, chaos, upgrade history/IAM). Owns [profile.e2e-distributed] and
# .github/workflows/e2e-distributed.yml.
# * on_demand_migration::interop_test — the ODM-20 provider interoperability
# cases, which are meaningless without a source: they run in the dedicated
# [profile.e2e-odm-interop] lane below, where the workflow points them at a
# MinIO container or a real cloud provider. Excluding them here also keeps
# this profile's committed selection digest stable.
# * replication_extension_test — repl-1 already splits it into the PR
# `e2e-smoke` (20 fast) and `e2e-repl-nightly` (56 slow) lanes and reserves
# it for those, so e2e-full does not double-run it.
@@ -450,25 +681,17 @@ path = "junit.xml"
# Each e2e test spawns its own single-node rustfs server on a random port with
# an isolated temp dir (crates/e2e_test/src/common.rs), so the set is
# parallel-safe — the same property e2e-smoke relies on. The exceptions are the
# 4-disk reliability / degraded-read fault-injection tests and the fixed-port
# Vault tests, both serialized below.
# KNOWN-FAILURE EXCLUSIONS (characterization run 29381309848, 2026-07-15:
# 341 ran / 32 failed on the suites' first automated run ever). Deterministic
# product failures cannot be quarantined away with retries, so each family is
# excluded here with its tracking issue, under the same discipline as the
# ci-profile quarantine (docs/testing/README.md): every entry MUST cite one
# OPEN issue, and the fixing PR MUST delete the exclusion. The passing
# negative-path siblings of each family stay in as regression guards.
# * rustfs#4843 — over-limit archive entry paths hard-reject the whole
# archive even under ignore-errors semantics.
# 4-disk reliability / degraded-read fault-injection tests, multi-node heal
# clusters, and the fixed-port Vault tests, all serialized below.
[profile.e2e-full]
default-filter = """
package(e2e_test)
& !test(/^protocols::/)
& !test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|heal_erasure_disk_rebuild_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
& !test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|degraded_listing_availability_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
& !test(/^distributed::/)
& !test(/^replication_extension_test::/)
& !test(/^multipart_auth_test::test_signed_put_object_extract_skips_invalid_entry_when_ignore_errors_enabled$/)
& !test(/^snowball_auto_extract_test::tests::snowball_auto_extract_(ignores_invalid_entries_when_requested|supports_standard_headers_with_combined_extract_options)$/)
& !test(/^replication_target_matrix_test::/)
& !test(/^on_demand_migration::(concurrency_test|fault_test|interop_test|real_source_test)::/)
"""
fail-fast = false
@@ -488,6 +711,12 @@ test-group = 'e2e-reliability'
filter = 'package(e2e_test) & test(/^inline_fast_path_cluster_test::/)'
test-group = 'e2e-inline-boundaries'
# Heal interruption cases start whole multi-node clusters. Bound their overlap
# just as the nightly lane does, without changing membership or retry policy.
[[profile.e2e-full.overrides]]
filter = 'package(e2e_test) & test(/^heal_erasure_disk_rebuild_test::/)'
test-group = 'e2e-cluster-nightly'
[[profile.e2e-full.overrides]]
filter = 'package(e2e_test) & (test(/^kms::kms_vault_test::/) | test(/^kms::kms_rekey_sweep_test::/) | test(/^kms::configured_roundtrip_test::test_configured_vault_kms_admin_and_versioned_cleanup$/))'
test-group = 'e2e-vault'
+449
View File
@@ -0,0 +1,449 @@
{
"schema": 2,
"cases": {
"background-target-restart": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_remote_shards_after_background_target_restart",
"oracle": "background-target-restart.json",
"evidence": "process-restart",
"unclean_shutdown_marker": false,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 4, "drives_per_node": 1},
"erasure": {"data_blocks": 2, "parity_blocks": 2},
"scope": "Target process restart, exact unversioned S3 bodies and replacement-disk shards; not power loss or EC8+4."
},
"background-target-crash": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_remote_shards_after_background_target_crash",
"oracle": "background-target-crash.json",
"evidence": "process-crash-restart",
"unclean_shutdown_marker": true,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 4, "drives_per_node": 1},
"erasure": {"data_blocks": 2, "parity_blocks": 2},
"scope": "Target process killed during partial background rebuild, real unclean-shutdown marker, exact unversioned S3 bodies and replacement-disk shards; not power loss or EC8+4."
},
"ec84-target-drive-restart": {
"gate": "G14",
"task": "W20/W21",
"lane": "e2e-distributed",
"suite": "e2e_test",
"name": "distributed::heal_test::three_node_four_drive_ec8_4_root_heal_rebuilds_replaced_drive_after_restart",
"oracle": "ec84-target-drive-restart.json",
"evidence": "process-restart",
"unclean_shutdown_marker": false,
"min_objects": 5,
"max_objects": 5,
"topology": {"nodes": 3, "drives_per_node": 4},
"erasure": {"data_blocks": 8, "parity_blocks": 4},
"erasure_set_drive_count": 12,
"scope": "3-node x 4-drive single-set EC8+4, graceful target restart, preformatted replacement drive, exact unversioned S3 bodies and physical target shards; not mixed-version, multi-pool or long-window ABBA."
},
"background-target-restart-ec8-4": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_ec84_shards_after_background_target_restart",
"oracle": "background-target-restart-ec8-4.json",
"evidence": "process-restart",
"unclean_shutdown_marker": false,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 3, "drives_per_node": 4},
"erasure": {"data_blocks": 8, "parity_blocks": 4},
"erasure_set_drive_count": 12,
"scope": "Target process restart during partial background rebuild on a single 3x4 EC8+4 set; exact unversioned S3 bodies and replacement-drive shards; not power loss, multi-set, or multi-pool."
},
"background-target-crash-ec8-4": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_ec84_shards_after_background_target_crash",
"oracle": "background-target-crash-ec8-4.json",
"evidence": "process-crash-restart",
"unclean_shutdown_marker": true,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 3, "drives_per_node": 4},
"erasure": {"data_blocks": 8, "parity_blocks": 4},
"erasure_set_drive_count": 12,
"scope": "Target process killed during partial background rebuild on a single 3x4 EC8+4 set; real unclean-shutdown marker, exact unversioned S3 bodies and replacement-drive shards; not power loss, multi-set, or multi-pool."
},
"background-target-restart-ec8-4-multi-set": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_ec84_shards_across_multi_set_after_background_target_restart",
"oracle": "background-target-restart-ec8-4-multi-set.json",
"evidence": "process-restart",
"unclean_shutdown_marker": false,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 3, "drives_per_node": 8},
"erasure": {"data_blocks": 8, "parity_blocks": 4},
"erasure_set_drive_count": 12,
"sets": 2,
"pools": 1,
"scope": "Target process restart during partial background rebuild on a 3x8 EC8+4 layout with two erasure sets in one pool; exact unversioned S3 bodies and replacement-drive shards; not power loss or multi-pool."
},
"background-target-crash-ec8-4-multi-pool": {
"gate": "G14",
"task": "W21",
"lane": "e2e-nightly",
"suite": "e2e_test",
"name": "heal_erasure_disk_rebuild_test::tests::test_cluster_root_heal_recovers_ec84_shards_across_multi_pool_after_background_target_crash",
"oracle": "background-target-crash-ec8-4-multi-pool.json",
"evidence": "process-crash-restart",
"unclean_shutdown_marker": true,
"min_objects": 9,
"max_objects": 65,
"topology": {"nodes": 3, "drives_per_node": 12},
"erasure": {"data_blocks": 8, "parity_blocks": 4},
"erasure_set_drive_count": 12,
"sets": 3,
"pools": 3,
"outage_target_manifest_required": false,
"scope": "Target process crash during partial background rebuild on three single-node EC8+4 pools; exact baseline S3 bodies and replacement-drive shards, with down-window outage PUT refusal recorded and a deferred post-rejoin outage object verified through S3 when the full target pool was offline."
}
},
"release_lanes": {
"single-set-restart": {
"status": "implemented",
"cases": ["background-target-restart", "background-target-crash"],
"covers": ["four-node one-drive topology", "unversioned objects", "target restart/crash"]
},
"authority-coverage": {
"status": "pending",
"gates": ["G01", "G12"],
"requires": ["root authority coverage", "quota authority coverage"]
},
"checkpoint-and-crash": {
"status": "pending",
"gates": ["G02", "G04", "R-E"],
"requires": ["bounded checkpoint progress", "boundary crash matrix", "fixed-budget restart evidence"]
},
"status-and-outcome": {
"status": "pending",
"gates": ["G05", "G06", "R-D"],
"requires": ["per-object outcomes", "legacy status clients", "manager/event/ledger disposition"]
},
"mrf-responsibility": {
"status": "pending",
"gates": ["G07", "G08", "P4"],
"requires": ["durable MRF responsibility", "disk-full and replica-loss matrix", "MRF replay cost"]
},
"mixed-version-rollback": {
"status": "pending",
"gates": ["G03", "G09", "R-L"],
"requires": ["mixed-version peers", "rollback payloads", "crash-safe source retirement"]
},
"scheduler-pressure": {
"status": "pending",
"gates": ["G10", "P1", "P2", "P3"],
"requires": ["bounded scheduling", "foreground latency and throughput", "two-hour pressure evidence"]
},
"maintenance-producers": {
"status": "pending",
"gates": ["G11", "G13"],
"requires": ["complete producer coverage", "quorum-minus-one and remount matrix"]
},
"ec8-4-multiset": {
"status": "pending",
"gates": ["G14"],
"requires": ["3x4 EC8+4 topology", "multi-set coverage", "multi-pool coverage"]
}
},
"release_requirements": [
{
"gate": "G01",
"task": "W02/W04",
"lane": "authority-coverage",
"status": "pending",
"description": "Complete root and quota authority coverage",
"requires": ["root authority evidence", "quota authority evidence"],
"evidence_fields": [
"root_authority_evidence",
"quota_authority_evidence"
]
},
{
"gate": "G02",
"task": "W03",
"lane": "checkpoint-and-crash",
"status": "pending",
"description": "Bounded checkpoint progress and independent version inventory",
"requires": ["bounded checkpoint oracle", "independent version inventory"],
"evidence_fields": [
"bounded_checkpoint_oracle",
"independent_version_inventory"
]
},
{
"gate": "G03",
"task": "W17/W18",
"lane": "mixed-version-rollback",
"status": "pending",
"description": "Exact scoped ACK with durable publication and mixed peers",
"requires": ["durable scoped ACK publication", "mixed-peer evidence"],
"evidence_fields": [
"durable_root_publication_proof",
"scoped_ack_request_identity",
"participating_peer_capability_snapshot",
"mixed_peer_ack_fallback_oracle"
]
},
{
"gate": "G04",
"task": "W03/W15/W16",
"lane": "checkpoint-and-crash",
"status": "pending",
"description": "Crash at every cache, root, floor and intent boundary",
"requires": ["cache boundary crash evidence", "root/floor/intent crash evidence"],
"evidence_fields": [
"cache_boundary_crash_evidence",
"root_floor_intent_crash_evidence"
]
},
{
"gate": "G05",
"task": "W06/W07",
"lane": "status-and-outcome",
"status": "pending",
"description": "Per-object outcomes and bounded terminal retention",
"requires": ["per-object outcome oracle", "terminal retention bounds"],
"evidence_fields": [
"per_object_outcome_oracle",
"terminal_retention_bounds"
]
},
{
"gate": "G06",
"task": "W06/W08/W23",
"lane": "status-and-outcome",
"status": "pending",
"description": "Concurrent status, legacy clients and truncation",
"requires": ["concurrent status evidence", "legacy client compatibility", "truncation behavior"],
"evidence_fields": [
"concurrent_status_evidence",
"legacy_client_compatibility",
"truncation_behavior"
]
},
{
"gate": "G07",
"task": "W12/W13/W14",
"lane": "mrf-responsibility",
"status": "pending",
"description": "Durable MRF responsibility at every commit boundary",
"requires": ["MRF responsibility oracle", "commit-boundary crash matrix"],
"evidence_fields": [
"mrf_responsibility_oracle",
"commit_boundary_crash_matrix"
]
},
{
"gate": "G08",
"task": "W12/W13/W14",
"lane": "mrf-responsibility",
"status": "pending",
"description": "MRF capacity, disk-full and replica-loss matrix",
"requires": ["MRF capacity evidence", "disk-full matrix", "replica-loss matrix"],
"evidence_fields": [
"mrf_capacity_evidence",
"disk_full_matrix",
"replica_loss_matrix"
]
},
{
"gate": "G09",
"task": "W13/W18/W23",
"lane": "mixed-version-rollback",
"status": "pending",
"description": "Actual mixed-version reader/writer and rollback payloads",
"requires": ["mixed-version reader evidence", "mixed-version writer evidence", "rollback payload evidence"],
"evidence_fields": [
"mixed_version_reader_evidence",
"mixed_version_writer_evidence",
"rollback_payload_evidence"
]
},
{
"gate": "G10",
"task": "W05/W09/W10/W11",
"lane": "scheduler-pressure",
"status": "pending",
"description": "Bounded scheduling and pressure recovery",
"requires": ["scheduler bound evidence", "pressure recovery evidence"],
"evidence_fields": [
"scheduler_bound_evidence",
"pressure_recovery_evidence"
]
},
{
"gate": "G11",
"task": "W04/W19/W24",
"lane": "maintenance-producers",
"status": "pending",
"description": "Maintenance and complete producer coverage",
"requires": [
"maintenance producer matrix",
"complete producer inventory",
"segment activation preflight"
],
"evidence_fields": [
"maintenance_producer_matrix",
"complete_producer_inventory",
"segment_activation_preflight"
]
},
{
"gate": "G12",
"task": "W02/W15/W16",
"lane": "authority-coverage",
"status": "pending",
"description": "Both quota paths during reset and settlement",
"requires": ["reset quota-path evidence", "settlement quota-path evidence"],
"evidence_fields": [
"reset_quota_path_evidence",
"settlement_quota_path_evidence"
]
},
{
"gate": "G13",
"task": "W07/W14",
"lane": "maintenance-producers",
"status": "pending",
"description": "Quorum-minus-one, unknown disks, remount, Object Lock, dry-run, grace and commit tail",
"requires": ["quorum-minus-one matrix", "unknown-disk/remount matrix", "Object Lock dry-run grace evidence"],
"evidence_fields": [
"quorum_minus_one_matrix",
"unknown_disk_remount_matrix",
"object_lock_dry_run_grace_evidence"
]
},
{
"gate": "G14",
"task": "W20/W21",
"lane": "ec8-4-multiset",
"status": "pending",
"description": "Same-window field evidence with 3x4 EC8+4 and multi-set/pool coverage",
"requires": [
"same-window field evidence",
"3x4 EC8+4 evidence",
"multi-set evidence",
"multi-pool evidence",
"distributed segment invalidation evidence"
],
"evidence_fields": [
"same_window_field_evidence",
"ec8_4_evidence",
"multi_set_evidence",
"multi_pool_evidence",
"distributed_segment_invalidation_evidence"
]
},
{
"gate": "P1",
"task": "W20",
"lane": "scheduler-pressure",
"status": "pending",
"description": "Measured cold-walk share and foreground latency/throughput",
"requires": ["cold-walk share measurement", "foreground latency/throughput measurement"],
"evidence_fields": [
"cold_walk_share_measurement",
"foreground_latency_throughput_measurement",
"profile_evidence"
]
},
{
"gate": "P2",
"task": "W20/W24",
"lane": "scheduler-pressure",
"status": "pending",
"description": "Measured post-stop convergence and cold segment reuse",
"requires": ["post-stop convergence measurement", "cold segment reuse measurement"],
"evidence_fields": [
"post_stop_convergence_measurement",
"cold_segment_reuse_measurement"
]
},
{
"gate": "P3",
"task": "W20",
"lane": "scheduler-pressure",
"status": "pending",
"description": "Measured two-hour pressure/heal capacity and recovery window",
"requires": ["two-hour pressure measurement", "heal capacity measurement", "recovery-window measurement"],
"evidence_fields": [
"two_hour_pressure_measurement",
"heal_capacity_measurement",
"recovery_window_measurement"
]
},
{
"gate": "P4",
"task": "W20",
"lane": "mrf-responsibility",
"status": "pending",
"description": "Measured MRF scale and replay cost with retained responsibility",
"requires": ["MRF scale measurement", "MRF replay-cost measurement", "retained responsibility evidence", "cleanup/GC soak evidence"],
"evidence_fields": [
"mrf_scale_measurement",
"mrf_replay_cost_measurement",
"retained_responsibility_evidence",
"mrf_cleanup_gc_soak_evidence"
]
},
{
"gate": "R-E",
"task": "W03/W05",
"lane": "checkpoint-and-crash",
"status": "pending",
"description": "Fixed-budget real process restart through enumeration and classification",
"requires": ["fixed-budget restart evidence", "enumeration evidence", "classification evidence"],
"evidence_fields": [
"fixed_budget_restart_evidence",
"enumeration_evidence",
"classification_evidence"
]
},
{
"gate": "R-D",
"task": "W07/W14",
"lane": "status-and-outcome",
"status": "pending",
"description": "Manager-to-event-to-ledger exact disposition, including grace",
"requires": ["manager disposition evidence", "event disposition evidence", "ledger disposition evidence", "grace handling"],
"evidence_fields": [
"manager_disposition_evidence",
"event_disposition_evidence",
"ledger_disposition_evidence",
"grace_handling"
]
},
{
"gate": "R-L",
"task": "W13/W14",
"lane": "mixed-version-rollback",
"status": "pending",
"description": "Legacy source conflicts, migration gaps and crash-safe source retirement",
"requires": ["legacy source-conflict evidence", "migration-gap evidence", "crash-safe source retirement evidence"],
"evidence_fields": [
"legacy_source_conflict_evidence",
"migration_gap_evidence",
"crash_safe_source_retirement_evidence"
]
}
]
}
+1 -1
View File
@@ -9,4 +9,4 @@
# if the selected count drops below this number, so a rename or removal that
# thins the security smoke gate must update this file in the same PR.
# Adding tests does not require a bump, but bumping keeps the guard tight.
18
28
@@ -43,6 +43,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
@@ -89,6 +90,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
@@ -135,6 +137,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
@@ -181,6 +184,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
@@ -26,6 +26,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
@@ -72,6 +73,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
@@ -118,6 +120,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
@@ -164,6 +167,7 @@ services:
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
@@ -201,6 +201,7 @@ services:
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=debug
platform: linux/amd64
ports:
@@ -220,6 +221,7 @@ services:
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=debug
platform: linux/amd64
ports:
@@ -239,6 +241,7 @@ services:
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=debug
platform: linux/amd64
ports:
@@ -258,6 +261,7 @@ services:
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_LOGGER_LEVEL=debug
platform: linux/amd64
ports:
+4
View File
@@ -47,6 +47,10 @@ Three pre-built Grafana dashboards are included for monitoring RustFS GET perfor
| **GET Resource Impact** | `grafana-get-resource-impact.json` | Monitors resource usage: concurrent requests, IO queue utilization, disk permit wait, RSS trend |
| **Object Data Cache** | `grafana-object-data-cache.json` | Monitors the GET body cache (`rustfs_object_data_cache_*`): hit ratio, lookup/plan/fill outcomes, fill duration quantiles, hit vs fill throughput, entries/weighted bytes, inflight fills, memory-pressure skips, invalidations, and size-class breakdowns |
### Storage Metrics
Storage panels require `prometheus-rules/rustfs-storage.yml` and the cluster resource attribute. See the [storage metrics guide](../../docs/operations/storage-metrics.md) for ownership, observer selection, freshness, and rolling upgrades.
### Prometheus Alert Rules
The file `prometheus-rules/rustfs-get-optimization-alerts.yaml` contains pre-configured alerting rules:
@@ -32,6 +32,7 @@ services:
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_OBS_LOGGER_LEVEL=info
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-rustfs.cluster.id=rustfs-dev}
- RUSTFS_OBS_PROFILING_ENDPOINT=http://pyroscope:4040
volumes:
- rustfs-data:/data/rustfs
@@ -223,14 +223,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_buckets_total{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_buckets_total\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Total Buckets",
"type": "stat"
"type": "stat",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -289,14 +290,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_objects_total{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_objects_total\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Total Objects",
"type": "stat"
"type": "stat",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -427,7 +429,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_capacity_used_bytes{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_used_bytes\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "Used",
"range": true,
"refId": "A"
@@ -438,7 +440,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_capacity_raw_total_bytes{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_raw_total_bytes\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"hide": false,
"legendFormat": "Total",
"range": true,
@@ -450,7 +452,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_capacity_used_bytes{job=~\"$job\"}) / sum(rustfs_cluster_capacity_raw_total_bytes{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_used_bytes\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"} / ignoring(source_metric) rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_raw_total_bytes\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"hide": false,
"instant": false,
"legendFormat": "Percent",
@@ -459,7 +461,8 @@
}
],
"title": "Capacity",
"type": "stat"
"type": "stat",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -525,7 +528,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_capacity_stale_drives{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_stale_drives\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "Stale Drives",
"range": true,
"refId": "A"
@@ -536,14 +539,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum(rustfs_cluster_capacity_missing_drives{job=~\"$job\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_capacity_missing_drives\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "Missing Drives",
"range": true,
"refId": "B"
}
],
"title": "Capacity Observation",
"type": "stat"
"type": "stat",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -1989,8 +1993,8 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (drive) (rustfs_system_drive_used_bytes{job=~\"$job\", drive=~\"$drive\"})",
"legendFormat": "{{drive}} (bytes)",
"expr": "rustfs:storage:current{source_metric=\"rustfs_system_drive_used_bytes\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\"}",
"legendFormat": "{{server}} | {{drive}} (bytes)",
"range": true,
"refId": "A"
},
@@ -2000,16 +2004,17 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (drive) (rustfs_system_drive_used_bytes{job=~\"$job\", drive=~\"$drive\"}) / sum by (drive)(rustfs_system_drive_total_bytes{job=~\"$job\", drive=~\"$drive\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_system_drive_used_bytes\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\"} / ignoring(source_metric) rustfs:storage:current{source_metric=\"rustfs_system_drive_total_bytes\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\"}",
"hide": false,
"instant": false,
"legendFormat": "{{drive}} (percent)",
"legendFormat": "{{server}} | {{drive}} (percent)",
"range": true,
"refId": "B"
}
],
"title": "System Drive Usage",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -2097,14 +2102,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_age_seconds{job=~\"$job\", drive=~\"$drive\"})",
"legendFormat": "{{drive}}",
"expr": "rustfs:storage:current{source_metric=\"rustfs_system_drive_capacity_observation_age_seconds\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\"}",
"legendFormat": "{{server}} | {{drive}}",
"range": true,
"refId": "A"
}
],
"title": "Drive Capacity Observation Age",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -2190,8 +2196,8 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_state{job=~\"$job\", drive=~\"$drive\", state=\"stale\"})",
"legendFormat": "{{drive}} stale",
"expr": "rustfs:storage:current{source_metric=\"rustfs_system_drive_capacity_observation_state\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\",state=\"stale\"}",
"legendFormat": "{{server}} | {{drive}} stale",
"range": true,
"refId": "A"
},
@@ -2201,14 +2207,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_state{job=~\"$job\", drive=~\"$drive\", state=\"missing\"})",
"legendFormat": "{{drive}} missing",
"expr": "rustfs:storage:current{source_metric=\"rustfs_system_drive_capacity_observation_state\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\",state=\"missing\"}",
"legendFormat": "{{server}} | {{drive}} missing",
"range": true,
"refId": "B"
}
],
"title": "Drive Capacity Observation State",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -4551,7 +4558,7 @@
"index": 0,
"text": "INACTIVE"
},
"to": 1e-9
"to": 1e-09
},
"type": "range"
}
@@ -6563,7 +6570,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "rustfs_cluster_health_drives_online_count{job=~\"$job\"}",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_health_drives_online_count\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "online - {{job}}",
"range": true,
"refId": "A"
@@ -6574,7 +6581,7 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "rustfs_cluster_health_drives_offline_count{job=~\"$job\"}",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_health_drives_offline_count\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "offline - {{job}}",
"range": true,
"refId": "B"
@@ -6585,14 +6592,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "rustfs_cluster_health_drives_count{job=~\"$job\"}",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_health_drives_count\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "total - {{job}}",
"range": true,
"refId": "C"
}
],
"title": "Cluster Drive Health Counts",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"collapsed": false,
@@ -8554,14 +8562,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "{__name__=~\"rustfs_system_drive_.*\",job=~\"$job\",drive=~\"$drive\"}",
"legendFormat": "{{__name__}} | {{drive}}",
"expr": "rustfs:storage:current{source_metric=~\"rustfs_system_drive_.*\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\"}",
"legendFormat": "{{server}} | {{source_metric}} | {{drive}}",
"range": true,
"refId": "A"
}
],
"title": "System Drive (All)",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -8942,14 +8951,15 @@
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "{__name__=~\"rustfs_cluster_erasure_set_.*\",job=~\"$job\"}",
"legendFormat": "{{__name__}}",
"expr": "rustfs:storage:current{source_metric=~\"rustfs_cluster_erasure_set_.*\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\"}",
"legendFormat": "{{source_metric}}",
"range": true,
"refId": "A"
}
],
"title": "Cluster Erasure Set (All)",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -11716,7 +11726,7 @@
"editorMode": "code",
"range": true,
"refId": "A",
"expr": "max by (server, drive, pool_index, set_index, drive_index, state) (rustfs_system_drive_runtime_state{job=~\"$job\",server=~\"$server\",drive=~\"$drive\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_drive_runtime_state\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\",drive=~\"$drive\"}",
"legendFormat": "{{server}} | {{drive}} | p{{pool_index}}/s{{set_index}}/d{{drive_index}} | {{state}}"
},
{
@@ -11727,12 +11737,13 @@
"editorMode": "code",
"range": true,
"refId": "B",
"expr": "max by (server, drive, pool_index, set_index, drive_index) (rustfs_system_drive_offline_duration_seconds{job=~\"$job\",server=~\"$server\",drive=~\"$drive\"})",
"expr": "rustfs:storage:current{source_metric=\"rustfs_cluster_drive_offline_duration_seconds\",collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",observer=\"$storage_observer\",drive=~\"$drive\"}",
"legendFormat": "{{server}} | {{drive}} | offline seconds"
}
],
"title": "Drive Runtime State and Offline Duration",
"type": "timeseries"
"title": "Observed Cluster Drive State and Offline Duration",
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -11825,12 +11836,13 @@
"editorMode": "code",
"range": true,
"refId": "A",
"expr": "sum by (server, drive, pool_index, set_index, drive_index, api) (rate(rustfs_system_drive_api_calls_total{job=~\"$job\",server=~\"$server\",drive=~\"$drive\",api=~\"$drive_api\"}[$__rate_interval]))",
"expr": "rate(rustfs_system_drive_api_calls_total{collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\",api=~\"$drive_api\"}[$__rate_interval]) and ignoring(source_metric) rustfs:storage:current{source_metric=\"rustfs_system_drive_api_calls_total\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\",server=~\"$server\",drive=~\"$drive\",api=~\"$drive_api\"}",
"legendFormat": "{{server}} | {{drive}} | p{{pool_index}}/s{{set_index}}/d{{drive_index}} | {{api}}"
}
],
"title": "Drive API Calls by Operation",
"type": "timeseries"
"type": "timeseries",
"description": "Storage snapshot rules are required. Local details come from each drive owner. Global values use the selected fresh cluster observer. Missing or expired observations show no data; select another observer if needed."
},
{
"datasource": {
@@ -12376,6 +12388,50 @@
"sort": 1,
"type": "query"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(rustfs:storage_snapshot:fresh, rustfs_cluster_id)",
"includeAll": false,
"label": "Storage cluster",
"multi": false,
"name": "storage_cluster",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(rustfs:storage_snapshot:fresh, rustfs_cluster_id)",
"refId": "PrometheusVariableQueryEditor-storage_cluster"
},
"refresh": 2,
"regex": "",
"sort": 1,
"type": "query"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "query_result(rustfs:storage_snapshot:fresh{collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\"})",
"includeAll": false,
"label": "Cluster observer",
"multi": false,
"name": "storage_observer",
"options": [],
"query": {
"qryType": 3,
"query": "query_result(rustfs:storage_snapshot:fresh{collection_scope=\"cluster\",rustfs_cluster_id=\"$storage_cluster\",job=~\"$job\"})",
"refId": "PrometheusVariableQueryEditor-storage_observer"
},
"refresh": 2,
"regex": "/observer=\"([^\"]+)\"/",
"sort": 1,
"type": "query"
},
{
"allValue": ".*",
"current": {
@@ -12386,7 +12442,7 @@
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(rustfs_system_drive_api_calls_total,api)",
"definition": "label_values(rustfs:storage:current{source_metric=\"rustfs_system_drive_api_calls_total\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\"}, api)",
"includeAll": true,
"label": "Drive API",
"multi": true,
@@ -12394,7 +12450,7 @@
"options": [],
"query": {
"qryType": 1,
"query": "label_values(rustfs_system_drive_api_calls_total,api)",
"query": "label_values(rustfs:storage:current{source_metric=\"rustfs_system_drive_api_calls_total\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\"}, api)",
"refId": "PrometheusVariableQueryEditor-drive_api"
},
"refresh": 2,
@@ -12455,7 +12511,7 @@
"text": "All",
"value": "$__all"
},
"definition": "label_values(rustfs_system_drive_used_bytes,drive)",
"definition": "label_values(rustfs:storage:current{source_metric=\"rustfs_system_drive_used_bytes\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\"}, drive)",
"includeAll": true,
"label": "Drive",
"multi": true,
@@ -12463,7 +12519,7 @@
"options": [],
"query": {
"qryType": 1,
"query": "label_values(rustfs_system_drive_used_bytes,drive)",
"query": "label_values(rustfs:storage:current{source_metric=\"rustfs_system_drive_used_bytes\",collection_scope=\"local\",rustfs_cluster_id=\"$storage_cluster\"}, drive)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
@@ -0,0 +1,575 @@
# Preserve OTLP timestamps in the Collector. Apply timestamp() directly to
# each raw selector, before label rewriting; functions such as label_replace()
# replace the evaluation timestamp and would make a cached value look new.
# Compare publication times at Prometheus millisecond precision so points
# published and exported within the same millisecond are not withheld.
groups:
- name: rustfs-storage-snapshots
interval: 15s
rules:
- record: rustfs:storage_snapshot:fresh
expr: |
rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id!="",collection_scope=~"local|cluster"}
and ((time() - rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id!="",collection_scope=~"local|cluster"}) <= rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id!="",collection_scope=~"local|cluster"})
and ((time() - rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id!="",collection_scope=~"local|cluster"}) >= 0)
and (rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id!="",collection_scope=~"local|cluster"} > 0)
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_buckets_total
expr: |
rustfs_cluster_buckets_total{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_buckets_total{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_free_bytes
expr: |
rustfs_cluster_capacity_free_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_free_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_missing_drives
expr: |
rustfs_cluster_capacity_missing_drives{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_missing_drives{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_raw_total_bytes
expr: |
rustfs_cluster_capacity_raw_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_raw_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_stale_drives
expr: |
rustfs_cluster_capacity_stale_drives{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_stale_drives{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_usable_total_bytes
expr: |
rustfs_cluster_capacity_usable_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_usable_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_capacity_used_bytes
expr: |
rustfs_cluster_capacity_used_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_capacity_used_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_capacity_observation_age_seconds
expr: |
rustfs_cluster_drive_capacity_observation_age_seconds{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_capacity_observation_age_seconds{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_capacity_observation_state
expr: |
rustfs_cluster_drive_capacity_observation_state{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_capacity_observation_state{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_free_bytes
expr: |
rustfs_cluster_drive_free_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_free_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_offline_duration_seconds
expr: |
rustfs_cluster_drive_offline_duration_seconds{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_offline_duration_seconds{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_present
expr: |
rustfs_cluster_drive_present{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_present{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_runtime_state
expr: |
rustfs_cluster_drive_runtime_state{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_runtime_state{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_total_bytes
expr: |
rustfs_cluster_drive_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_total_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_drive_used_bytes
expr: |
rustfs_cluster_drive_used_bytes{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_drive_used_bytes{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_data_shards
expr: |
rustfs_cluster_erasure_set_data_shards{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_data_shards{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_healing_drives_count
expr: |
rustfs_cluster_erasure_set_healing_drives_count{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_healing_drives_count{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_health
expr: |
rustfs_cluster_erasure_set_health{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_health{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_online_drives_count
expr: |
rustfs_cluster_erasure_set_online_drives_count{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_online_drives_count{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_overall_health
expr: |
rustfs_cluster_erasure_set_overall_health{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_overall_health{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_overall_write_quorum
expr: |
rustfs_cluster_erasure_set_overall_write_quorum{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_overall_write_quorum{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_parity
expr: |
rustfs_cluster_erasure_set_parity{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_parity{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_read_health
expr: |
rustfs_cluster_erasure_set_read_health{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_read_health{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_read_quorum
expr: |
rustfs_cluster_erasure_set_read_quorum{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_read_quorum{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_read_tolerance
expr: |
rustfs_cluster_erasure_set_read_tolerance{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_read_tolerance{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_size
expr: |
rustfs_cluster_erasure_set_size{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_size{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_write_health
expr: |
rustfs_cluster_erasure_set_write_health{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_write_health{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_write_quorum
expr: |
rustfs_cluster_erasure_set_write_quorum{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_write_quorum{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_erasure_set_write_tolerance
expr: |
rustfs_cluster_erasure_set_write_tolerance{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_erasure_set_write_tolerance{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_health_drives_count
expr: |
rustfs_cluster_health_drives_count{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_health_drives_count{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_health_drives_offline_count
expr: |
rustfs_cluster_health_drives_offline_count{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_health_drives_offline_count{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_health_drives_online_count
expr: |
rustfs_cluster_health_drives_online_count{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_health_drives_online_count{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_cluster_objects_total
expr: |
rustfs_cluster_objects_total{rustfs_cluster_id!="",collection_scope="cluster"}
and (timestamp(rustfs_cluster_objects_total{rustfs_cluster_id!="",collection_scope="cluster"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_node_disk_free_bytes
expr: |
rustfs_node_disk_free_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_node_disk_free_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_node_disk_total_bytes
expr: |
rustfs_node_disk_total_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_node_disk_total_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_node_disk_used_bytes
expr: |
rustfs_node_disk_used_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_node_disk_used_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_api_calls_total
expr: |
rustfs_system_drive_api_calls_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_api_calls_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_api_latency_by_api_micros
expr: |
rustfs_system_drive_api_latency_by_api_micros{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_api_latency_by_api_micros{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_api_latency_micros
expr: |
rustfs_system_drive_api_latency_micros{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_api_latency_micros{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_availability_errors_total
expr: |
rustfs_system_drive_availability_errors_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_availability_errors_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_capacity_observation_age_seconds
expr: |
rustfs_system_drive_capacity_observation_age_seconds{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_capacity_observation_age_seconds{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_capacity_observation_state
expr: |
rustfs_system_drive_capacity_observation_state{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_capacity_observation_state{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_count
expr: |
rustfs_system_drive_count{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_count{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_deletes_total
expr: |
rustfs_system_drive_deletes_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_deletes_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_free_bytes
expr: |
rustfs_system_drive_free_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_free_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_free_inodes
expr: |
rustfs_system_drive_free_inodes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_free_inodes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_healing
expr: |
rustfs_system_drive_healing{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_healing{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_health
expr: |
rustfs_system_drive_health{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_health{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_info
expr: |
rustfs_system_drive_info{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_info{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_io_errors_total
expr: |
rustfs_system_drive_io_errors_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_io_errors_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_offline_count
expr: |
rustfs_system_drive_offline_count{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_offline_count{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_offline_duration_seconds
expr: |
rustfs_system_drive_offline_duration_seconds{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_offline_duration_seconds{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_online_count
expr: |
rustfs_system_drive_online_count{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_online_count{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_perc_util
expr: |
rustfs_system_drive_perc_util{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_perc_util{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_present
expr: |
rustfs_system_drive_present{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_present{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_reads_await
expr: |
rustfs_system_drive_reads_await{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_reads_await{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_reads_kb_per_sec
expr: |
rustfs_system_drive_reads_kb_per_sec{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_reads_kb_per_sec{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_reads_per_sec
expr: |
rustfs_system_drive_reads_per_sec{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_reads_per_sec{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_runtime_state
expr: |
rustfs_system_drive_runtime_state{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_runtime_state{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_scanning
expr: |
rustfs_system_drive_scanning{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_scanning{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_timeout_errors_total
expr: |
rustfs_system_drive_timeout_errors_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_timeout_errors_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_total_bytes
expr: |
rustfs_system_drive_total_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_total_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_total_inodes
expr: |
rustfs_system_drive_total_inodes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_total_inodes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_used_bytes
expr: |
rustfs_system_drive_used_bytes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_used_bytes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_used_inodes
expr: |
rustfs_system_drive_used_inodes{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_used_inodes{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_waiting_io
expr: |
rustfs_system_drive_waiting_io{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_waiting_io{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_writes_await
expr: |
rustfs_system_drive_writes_await{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_writes_await{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_writes_kb_per_sec
expr: |
rustfs_system_drive_writes_kb_per_sec{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_writes_kb_per_sec{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_writes_per_sec
expr: |
rustfs_system_drive_writes_per_sec{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_writes_per_sec{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
- record: rustfs:storage:current
labels:
source_metric: rustfs_system_drive_writes_total
expr: |
rustfs_system_drive_writes_total{rustfs_cluster_id!="",collection_scope="local"}
and (timestamp(rustfs_system_drive_writes_total{rustfs_cluster_id!="",collection_scope="local"})
>= on (rustfs_cluster_id, observer, collection_scope, job, instance) group_left()
(floor(rustfs:storage_snapshot:fresh * 1000) / 1000))
@@ -0,0 +1,189 @@
rule_files:
- ../prometheus-rules/rustfs-storage.yml
evaluation_interval: 15s
tests:
- name: owners, pools, clusters and observer views remain distinct in mixed versions
interval: 1m
input_series:
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data",pool_index="0",set_index="0",drive_index="0"}
values: 100x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector",server="n1",drive="/data",pool_index="0",set_index="0",drive_index="1"}
values: 100x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n2",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n2",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="n2",collection_scope="local",job="rustfs",instance="collector",server="n2",drive="/data",pool_index="1",set_index="0",drive_index="0"}
values: 100x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n3",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n3",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="n3",collection_scope="local",job="rustfs",instance="collector",server="n3",drive="/data",pool_index="1",set_index="0",drive_index="1"}
values: 100x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="b",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="b",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="b",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data",pool_index="0",set_index="0",drive_index="0"}
values: 1000x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="b",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="b",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="b",observer="n1",collection_scope="local",job="rustfs",instance="collector",server="n1",drive="/data",pool_index="0",set_index="0",drive_index="1"}
values: 1000x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="b",observer="n2",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="b",observer="n2",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="b",observer="n2",collection_scope="local",job="rustfs",instance="collector",server="n2",drive="/data",pool_index="1",set_index="0",drive_index="0"}
values: 1000x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="b",observer="n3",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="b",observer="n3",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="b",observer="n3",collection_scope="local",job="rustfs",instance="collector",server="n3",drive="/data",pool_index="1",set_index="0",drive_index="1"}
values: 1000x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="cluster",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="cluster",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_cluster_capacity_raw_total_bytes{rustfs_cluster_id="a",observer="n0",collection_scope="cluster",job="rustfs",instance="collector"}
values: 400x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="cluster",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="cluster",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_cluster_capacity_raw_total_bytes{rustfs_cluster_id="a",observer="n1",collection_scope="cluster",job="rustfs",instance="collector"}
values: 300x8
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="old",server="n0",drive="/data",job="rustfs",instance="collector"}
values: 999x8
promql_expr_test:
- expr: sum by (rustfs_cluster_id) (rustfs:storage:current{source_metric="rustfs_system_drive_total_bytes",collection_scope="local"})
eval_time: 2m
exp_samples:
- labels: '{rustfs_cluster_id="a"}'
value: 400
- labels: '{rustfs_cluster_id="b"}'
value: 4000
- expr: count by (rustfs_cluster_id,pool_index) (rustfs:storage:current{source_metric="rustfs_system_drive_total_bytes"})
eval_time: 2m
exp_samples:
- labels: '{rustfs_cluster_id="a",pool_index="0"}'
value: 2
- labels: '{rustfs_cluster_id="a",pool_index="1"}'
value: 2
- labels: '{rustfs_cluster_id="b",pool_index="0"}'
value: 2
- labels: '{rustfs_cluster_id="b",pool_index="1"}'
value: 2
- expr: sum(rustfs:storage:current{source_metric="rustfs_cluster_capacity_raw_total_bytes",rustfs_cluster_id="a",observer="n0"})
eval_time: 2m
exp_samples:
- labels: '{}'
value: 400
- expr: sum(rustfs:storage:current{source_metric="rustfs_cluster_capacity_raw_total_bytes",rustfs_cluster_id="a",observer="n1"})
eval_time: 2m
exp_samples:
- labels: '{}'
value: 300
- name: removed values and identities cannot rejoin new snapshots; stalled sources expire
interval: 1m
input_series:
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 0 60 _ _ _ _ _ _ _
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_info{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data",disk_id="old"}
values: 1 1 _ _ _ _ _ _ _
- series: rustfs_system_drive_info{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data",disk_id="new"}
values: _ _ 1 1 1 1 1 1 1
- series: rustfs_system_drive_waiting_io{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data"}
values: 7 7 _ _ _ _ _ _ _
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector",server="n1",drive="/data"}
values: 100 100 _ _ _ _ _ _ _
promql_expr_test:
- expr: rustfs:storage:current{source_metric="rustfs_system_drive_info",disk_id="old"}
eval_time: 2m
exp_samples: []
- expr: rustfs:storage:current{source_metric="rustfs_system_drive_waiting_io"}
eval_time: 2m
exp_samples: []
- expr: count(rustfs:storage:current{source_metric="rustfs_system_drive_info",disk_id="new"})
eval_time: 2m
exp_samples:
- labels: '{}'
value: 1
- expr: rustfs:storage:current{source_metric="rustfs_system_drive_total_bytes"}
eval_time: 5m
exp_samples: []
- expr: rustfs:storage_snapshot:fresh{observer="n1"}
eval_time: 5m
exp_samples: []
- name: counters reset independently; filter versions before rate and sum rates across owners
interval: 1m
input_series:
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_api_calls_total{rustfs_cluster_id="a",observer="n0",collection_scope="local",job="rustfs",instance="collector",server="n0",drive="/data",disk_id="n0-disk",api="read_all"}
values: 0 60 120 30 90 150 210
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 0+60x8
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector"}
values: 180x8
- series: rustfs_system_drive_api_calls_total{rustfs_cluster_id="a",observer="n1",collection_scope="local",job="rustfs",instance="collector",server="n1",drive="/data",disk_id="n1-disk",api="read_all"}
values: 0+120x6
- series: rustfs_system_drive_api_calls_total{rustfs_cluster_id="a",server="n0",drive="/data",observer="old"}
values: 0+999x6
promql_expr_test:
- expr: sum(resets(rustfs_system_drive_api_calls_total{collection_scope="local"}[5m]))
eval_time: 5m
exp_samples:
- labels: '{}'
value: 1
- expr: sum(rate(rustfs_system_drive_api_calls_total{collection_scope="local"}[2m]) and ignoring(source_metric) rustfs:storage:current{source_metric="rustfs_system_drive_api_calls_total"})
eval_time: 5m
exp_samples:
- labels: '{}'
value: 3
- name: completed slow collection has no validity budget
interval: 1m
input_series:
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="slow",collection_scope="cluster",job="rustfs",instance="collector"}
values: 0+60x3
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="slow",collection_scope="cluster",job="rustfs",instance="collector"}
values: '0x3'
promql_expr_test:
- expr: rustfs:storage_snapshot:fresh{observer="slow"}
eval_time: 2m
exp_samples: []
- name: submillisecond publication and millisecond OTLP samples share a cutoff
interval: 1m
input_series:
- series: rustfs_storage_snapshot_last_success_timestamp_seconds{rustfs_cluster_id="a",observer="submillisecond",collection_scope="local",job="rustfs",instance="collector"}
values: 0.0009+60x3
- series: rustfs_storage_snapshot_max_age_seconds{rustfs_cluster_id="a",observer="submillisecond",collection_scope="local",job="rustfs",instance="collector"}
values: 180x3
- series: rustfs_system_drive_total_bytes{rustfs_cluster_id="a",observer="submillisecond",collection_scope="local",job="rustfs",instance="collector",server="submillisecond",drive="/data"}
values: 100x3
promql_expr_test:
- expr: count(rustfs:storage:current{source_metric="rustfs_system_drive_total_bytes",observer="submillisecond"})
eval_time: 2m15s
exp_samples:
- labels: '{}'
value: 1
+1
View File
@@ -6,3 +6,4 @@ self-hosted-runner:
- sm-standard-4
- dind-sm-standard-2
- smoke-testing
- pf-testing
@@ -0,0 +1,100 @@
name: On-demand migration interop report
description: >-
Merge the per-case JSON entries an on-demand-migration interop run wrote with
the nextest JUnit result into one provider report, and summarise it.
inputs:
provider:
description: Provider the run addressed (minio, aws, r2, gcs).
required: true
cases-dir:
description: Directory the cases wrote their JSON entries into.
required: true
junit:
description: nextest JUnit XML of the run.
required: true
output:
description: Path of the merged JSON report to write.
required: true
runs:
using: composite
steps:
# The JUnit file is authoritative for which cases ran and how they ended:
# a case that fails or panics never reaches its own report entry, so
# trusting the entries alone would silently shorten the report exactly when
# something went wrong. The entries only add what JUnit cannot know — the
# source request accounting and the bucket's migration counters.
- name: Merge interop case reports
shell: bash
env:
ODM_REPORT_PROVIDER: ${{ inputs.provider }}
ODM_REPORT_CASES_DIR: ${{ inputs.cases-dir }}
ODM_REPORT_JUNIT: ${{ inputs.junit }}
ODM_REPORT_OUTPUT: ${{ inputs.output }}
run: |
python3 - <<'PY'
import json
import os
import pathlib
import xml.etree.ElementTree as ElementTree
provider = os.environ["ODM_REPORT_PROVIDER"]
cases_dir = pathlib.Path(os.environ["ODM_REPORT_CASES_DIR"])
junit = pathlib.Path(os.environ["ODM_REPORT_JUNIT"])
output = pathlib.Path(os.environ["ODM_REPORT_OUTPUT"])
entries = {}
if cases_dir.is_dir():
for path in sorted(cases_dir.glob("*.json")):
entry = json.loads(path.read_text())
entries[entry["case"]] = entry
cases = []
for case in ElementTree.parse(junit).getroot().iter("testcase"):
name = case.get("name", "")
failed = [child for child in case if child.tag in ("failure", "error")]
skipped = [child for child in case if child.tag == "skipped"]
outcome = "failed" if failed else "skipped" if skipped else "passed"
entry = entries.get(name.rsplit("::", 1)[-1], {})
cases.append(
{
"name": name,
"outcome": outcome,
"junit_duration_ms": round(float(case.get("time", "0")) * 1000),
"case_duration_ms": entry.get("duration_ms"),
"source_requests": entry.get("source_requests"),
"odm_counters": entry.get("odm_counters"),
}
)
report = {
"provider": provider,
"repository": os.environ.get("GITHUB_REPOSITORY", ""),
"sha": os.environ.get("GITHUB_SHA", ""),
"run_id": os.environ.get("GITHUB_RUN_ID", ""),
"cases": cases,
"totals": {
"cases": len(cases),
"passed": sum(1 for case in cases if case["outcome"] == "passed"),
"failed": sum(1 for case in cases if case["outcome"] == "failed"),
},
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
summary = [f"### On-demand migration interop: `{provider}`", "", "| Case | Outcome | Duration | Source requests |", "|---|---|---|---|"]
for case in cases:
requests = case["source_requests"]
counted = f"{requests['total']} ({requests['counted_by']})" if requests else "not reported"
summary.append(f"| `{case['name']}` | {case['outcome']} | {case['junit_duration_ms']} ms | {counted} |")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
handle.write("\n".join(summary) + "\n\n")
# A passed case with no entry of its own means the harness stopped
# writing one: the report would keep looking complete while silently
# losing its request accounting.
unreported = [case["name"] for case in cases if case["outcome"] == "passed" and case["source_requests"] is None]
if unreported:
raise SystemExit(f"passed cases wrote no interop report entry: {', '.join(unreported)}")
PY
+116
View File
@@ -0,0 +1,116 @@
# Copyright 2024 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.
name: Quick Checks
description: Run the shared compile-free RustFS quality checks.
runs:
using: composite
steps:
- name: Install quality tools
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: |
ripgrep@15.2.0
shellcheck@0.11.0
- name: Install actionlint
shell: bash
run: |
actionlint_dir="$(mktemp -d "${RUNNER_TEMP}/actionlint.XXXXXX")"
curl --fail --location --silent --show-error \
--output "$actionlint_dir/actionlint.tar.gz" \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $actionlint_dir/actionlint.tar.gz" | sha256sum --check --status
tar -xzf "$actionlint_dir/actionlint.tar.gz" -C "$actionlint_dir" actionlint
rm "$actionlint_dir/actionlint.tar.gz"
echo "$actionlint_dir" >> "$GITHUB_PATH"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check workflow syntax and shell scripts
shell: bash
run: shellcheck --version && actionlint
- name: Check code formatting
shell: bash
run: cargo fmt --all --check
- name: Check unsafe code allowances
shell: bash
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
shell: bash
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
shell: bash
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
shell: bash
run: ./scripts/check_logging_guardrails.sh
- name: Check error other(format!) ratchet
shell: bash
run: ./scripts/check_error_other_format_ratchet.sh
- name: Check tokio io-uring feature guard
shell: bash
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
shell: bash
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
shell: bash
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
shell: bash
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
shell: bash
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
shell: bash
run: ./scripts/check_embedded_secrets.sh
- name: Run script contract tests
shell: bash
run: make script-tests
- name: Check test wiring
shell: bash
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/test_security_workflow.py
python3 ./scripts/test_nightly_candidate.py
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
shell: bash
run: ./scripts/check_no_planning_docs.sh
- name: Check io_uring lane --lib precondition
shell: bash
run: ./scripts/check_uring_lane_lib_only.sh
@@ -5,20 +5,38 @@ This is the single alerting mechanism for all timed pipelines
(rustfs/backlog#1149 ci-8, arbitration G3): scheduled workflows must consume
this action instead of inventing their own notification paths.
Issues are filed in **rustfs/backlog** (override with `target-repository`),
not the repository the workflow runs in, so scheduled-failure noise stays
out of the code repository's issue tracker.
## Behavior
- Issue title: `[scheduled-failure] <workflow name>` — the workflow name is
the dedupe key.
- If an **open** issue with that exact title exists, the failure is appended
as a comment; otherwise a new issue is created (labeled `infrastructure` by
default). Closing the issue resets the cycle: the next failure opens a
fresh one.
- The issue body / comment includes the run URL, run attempt, event, ref and
the names of the failed (or timed-out/cancelled) jobs of the current run
attempt.
- If an **open** issue with that exact title exists in the target
repository, the failure is appended as a comment; otherwise a new issue is
created (labeled `infrastructure` by default; the label must exist in the
target repository). Closing the issue resets the cycle: the next failure
opens a fresh one.
- The issue body / comment includes the source repository, run URL, run
attempt, event, ref and the names of the failed (or timed-out/cancelled)
jobs of the current run attempt.
- Requires `gh` and `jq` on the runner (both preinstalled on GitHub-hosted
runners such as `ubuntu-latest`).
## Tokens
Two tokens are involved:
- `github-token` authenticates issue create/comment/search against the
target repository. The workflow-scoped `secrets.GITHUB_TOKEN` is scoped to
the code repository and **cannot** open issues in rustfs/backlog, so this
must be a PAT or GitHub App token with `issues: write` on the target
repository, stored as the `BACKLOG_ISSUE_TOKEN` repository secret.
- `source-token` (defaults to `${{ github.token }}`) reads the failed-job
list from the reported run, which lives in the repository running the
workflow.
## Usage
Add a final job to the workflow. `always()` is required — without it the job
@@ -41,7 +59,7 @@ this job only; no other job may gain permissions.
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
```
Notes:
@@ -16,18 +16,31 @@ name: "Schedule Failure Issue"
description: >-
Open (or update) a tracking issue when a scheduled workflow run fails or
does not complete normally.
Issues are filed in the target repository (default rustfs/backlog), not
the repository the workflow runs in.
Dedupes by workflow name: if an open issue titled
"[scheduled-failure] <workflow name>" already exists, the result is
"[scheduled-failure] <workflow name>" already exists there, the result is
appended as a comment; otherwise a new issue is created. This is the
single alerting mechanism for all scheduled pipelines (backlog#1149 ci-8).
inputs:
github-token:
description: >-
Token with issues:write on the repository. Pass secrets.GITHUB_TOKEN
from a job that declares `permissions: issues: write` (scope the
permission to the alert job only, never workflow-wide).
Token with issues:write on the target repository. The workflow-scoped
secrets.GITHUB_TOKEN is scoped to the code repository and cannot open
issues in rustfs/backlog, so pass a PAT / GitHub App token stored as a
repository secret (BACKLOG_ISSUE_TOKEN).
required: true
target-repository:
description: "Repository the failure-tracking issue is filed in."
required: false
default: rustfs/backlog
source-token:
description: >-
Token used to read the failed-job list from the reported run. Defaults
to the workflow's own token, which can read runs of its own repository.
required: false
default: ${{ github.token }}
workflow-name:
description: "Workflow name used for the issue title (the dedupe key)."
required: false
@@ -35,8 +48,8 @@ inputs:
label:
description: >-
Label applied when a new issue is created. Must already exist in the
repository; if applying it fails, the issue is created without a label.
Set to an empty string to skip labeling.
target repository; if applying it fails, the issue is created without a
label. Set to an empty string to skip labeling.
required: false
default: "infrastructure"
source-run-id:
@@ -71,6 +84,8 @@ runs:
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
SOURCE_TOKEN: ${{ inputs.source-token }}
TARGET_REPOSITORY: ${{ inputs.target-repository }}
WORKFLOW_NAME: ${{ inputs.workflow-name }}
ISSUE_LABEL: ${{ inputs.label }}
SOURCE_RUN_ID: ${{ inputs.source-run-id }}
@@ -86,8 +101,10 @@ runs:
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}"
# Inspect the reported run attempt. It can be the current in-workflow
# failure or a completed run observed by the external watchdog.
failed_jobs="$(gh api \
# failure or a completed run observed by the external watchdog. The
# run lives in the repository running the workflow, so authenticate
# with the source token, not the target-repository token.
failed_jobs="$(GH_TOKEN="${SOURCE_TOKEN}" gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}/attempts/${SOURCE_RUN_ATTEMPT}/jobs" \
--paginate \
--jq '.jobs[]
@@ -109,6 +126,7 @@ runs:
body="$(cat <<EOF
Run of **${WORKFLOW_NAME}** did not complete successfully.
- Repository: ${GITHUB_REPOSITORY}
- Run: ${run_url} (attempt ${SOURCE_RUN_ATTEMPT})
- Event: \`${SOURCE_EVENT}\`
- Ref: \`${SOURCE_REF_NAME}\` @ \`${SOURCE_SHA}\`
@@ -120,26 +138,26 @@ runs:
EOF
)"
# Dedupe by exact title among OPEN issues (a closed issue means the
# earlier breakage was resolved; a new failure gets a fresh issue).
# GitHub search strips the [] characters, so search loosely and match
# the exact title with jq.
existing="$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open --limit 100 \
# Dedupe by exact title among OPEN issues in the target repository (a
# closed issue means the earlier breakage was resolved; a new failure
# gets a fresh issue). GitHub search strips the [] characters, so
# search loosely and match the exact title with jq.
existing="$(gh issue list --repo "${TARGET_REPOSITORY}" --state open --limit 100 \
--search "\"scheduled-failure\" in:title" --json number,title \
| jq -r --arg title "${title}" 'map(select(.title == $title)) | (.[0].number // empty)')"
if [ -n "${existing}" ]; then
echo "Appending comment to existing open issue #${existing}"
gh issue comment "${existing}" --repo "${GITHUB_REPOSITORY}" --body "${body}"
echo "Appending comment to existing open issue rustfs/backlog#${existing}"
gh issue comment "${existing}" --repo "${TARGET_REPOSITORY}" --body "${body}"
exit 0
fi
echo "Creating new issue: ${title}"
echo "Creating new issue in ${TARGET_REPOSITORY}: ${title}"
if [ -n "${ISSUE_LABEL}" ]; then
if gh issue create --repo "${GITHUB_REPOSITORY}" \
if gh issue create --repo "${TARGET_REPOSITORY}" \
--title "${title}" --body "${body}" --label "${ISSUE_LABEL}"; then
exit 0
fi
echo "::warning::issue creation with label '${ISSUE_LABEL}' failed (missing label?); retrying without a label"
fi
gh issue create --repo "${GITHUB_REPOSITORY}" --title "${title}" --body "${body}"
gh issue create --repo "${TARGET_REPOSITORY}" --title "${title}" --body "${body}"
+5 -5
View File
@@ -10,16 +10,16 @@ Use N/A when there is no related issue.
## Summary of Changes
<!--
Briefly explain what changed and why reviewers should accept it.
Focus on behavior, compatibility, and review-relevant context.
Describe the concrete problem and resulting behavior. For a behavior change, name the input or state that triggers it and the expected outcome. Explain any new dependency or abstraction that the change needs.
-->
## Verification
<!--
List the commands or checks you ran, for example:
- `make pre-commit`
Give 1–3 concrete pieces of evidence for the changed behavior: the test or command, its observed result, and the regression it catches. For a bug fix, record a failing-before/passing-after check or explain why it was unavailable.
Use N/A only when verification is not applicable.
Identify the tested commit and any local changes. When testing a prebuilt binary or external service, include its source/version and artifact identity; a successful run against a different build is not evidence for this change.
List relevant checks not run and the remaining risk. Use the validation tier in AGENTS.md; do not run broader checks solely to fill this section. For documentation-only changes, list the applicable documentation checks. Use N/A only when verification is not applicable.
-->
## Impact
+4
View File
@@ -55,6 +55,10 @@ secret_key = ${S3_SECRET_KEY}
## replace with key id obtained when secret is created, or delete if KMS not tested
#kms_keyid = 01234567-89ab-cdef-0123-456789abcdef
#kms_keyid2 = fedcba98-7654-3210-fedc-ba9876543210
## Expected service default for SSE-KMS requests without a key id; empty means none
#kms_default_keyid =
## Storage classes
#storage_classes = "LUKEWARM, FROZEN"
+10
View File
@@ -4,9 +4,19 @@
{ "workflow": ".github/workflows/ci.yml", "max_age_hours": 192 },
{ "workflow": ".github/workflows/coverage.yml", "max_age_hours": 192 },
{ "workflow": ".github/workflows/e2e-replication-nightly.yml", "max_age_hours": 36 },
{
"workflow": ".github/workflows/e2e-distributed.yml",
"max_age_hours": 36,
"never_ran_grace_until": "2026-09-18T00:00:00Z"
},
{ "workflow": ".github/workflows/e2e-s3tests.yml", "max_age_hours": 192 },
{ "workflow": ".github/workflows/fuzz.yml", "max_age_hours": 36 },
{ "workflow": ".github/workflows/mint.yml", "max_age_hours": 192 },
{
"workflow": ".github/workflows/minio-interop.yml",
"max_age_hours": 36,
"never_ran_grace_until": "2026-09-08T00:00:00Z"
},
{ "workflow": ".github/workflows/nightly-gnu.yml", "max_age_hours": 36 },
{ "workflow": ".github/workflows/performance-ab.yml", "max_age_hours": 36 },
{
+13 -1
View File
@@ -24,8 +24,11 @@ on:
- '.github/actions/**'
- '.github/workflows/**'
- 'scripts/release/create_or_update_release.sh'
- 'scripts/release/package_versions.sh'
- 'scripts/test_package_versions.sh'
- 'scripts/security/check_performance_ab_workflow.sh'
- 'scripts/security/check_preview_release_workflow.sh'
- 'scripts/security/check_tier_artifact_workflow.sh'
- 'scripts/security/check_workflow_pins.sh'
pull_request:
types: [ opened, synchronize, reopened, closed ]
@@ -37,8 +40,11 @@ on:
- '.github/actions/**'
- '.github/workflows/**'
- 'scripts/release/create_or_update_release.sh'
- 'scripts/release/package_versions.sh'
- 'scripts/test_package_versions.sh'
- 'scripts/security/check_performance_ab_workflow.sh'
- 'scripts/security/check_preview_release_workflow.sh'
- 'scripts/security/check_tier_artifact_workflow.sh'
- 'scripts/security/check_workflow_pins.sh'
schedule:
# Daily, not weekly. This schedule exists to catch RustSec advisories
@@ -146,6 +152,12 @@ jobs:
- name: Check performance A/B workflow trust boundary
run: ./scripts/security/check_performance_ab_workflow.sh
- name: Check tier evidence workflow isolation
run: ./scripts/security/check_tier_artifact_workflow.sh
- name: Check package version contract
run: ./scripts/test_package_versions.sh
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
@@ -195,4 +207,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+71 -25
View File
@@ -207,12 +207,12 @@ jobs:
fi
all='{"include":[
{"target_id":"linux-x86_64-musl","os":"sm-standard-2","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-musl","os":"sm-standard-2","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":""},
{"target_id":"linux-x86_64-gnu","os":"sm-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-gnu","os":"sm-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""},
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""}
{"target_id":"linux-x86_64-musl","os":"sm-standard-4","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":""},
{"target_id":"linux-aarch64-musl","os":"sm-standard-4","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":"","features":""},
{"target_id":"linux-x86_64-gnu","os":"sm-standard-4","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":"","features":"pyroscope"},
{"target_id":"linux-aarch64-gnu","os":"sm-standard-4","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope"},
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":"","features":"pyroscope"},
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":"","features":""}
]}'
if [[ "${selected}" == "all" ]]; then
@@ -244,7 +244,7 @@ jobs:
needs: [ build-check, prepare-platform-matrix ]
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
runs-on: ${{ matrix.os }}
timeout-minutes: 150
timeout-minutes: 180
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Release binaries ship without dial9 telemetry and therefore do not need
@@ -406,12 +406,25 @@ jobs:
# Force rebuild by touching build.rs
touch rustfs/build.rs
if [[ "${{ matrix.cross }}" == "true" ]]; then
# All cross targets in the matrix are Linux; zigbuild handles them.
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
else
cargo build --release --target ${{ matrix.target }} -p rustfs --bins
BINARY_NAMES=(rustfs)
if [[ "${{ matrix.platform }}" == "linux" ]]; then
BINARY_NAMES+=(rustfs-cli)
fi
FEATURE_ARGS=()
if [[ -n "${{ matrix.features }}" ]]; then
FEATURE_ARGS=(--features "${{ matrix.features }}")
fi
# Build one binary at a time so release LTO links cannot exhaust a
# self-hosted runner by running concurrently.
for binary in "${BINARY_NAMES[@]}"; do
if [[ "${{ matrix.cross }}" == "true" ]]; then
# All cross targets in the matrix are Linux; zigbuild handles them.
cargo zigbuild --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" -p rustfs --bin "$binary"
else
cargo build --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" -p rustfs --bin "$binary"
fi
done
- name: Create release package
id: package
@@ -498,17 +511,24 @@ jobs:
BINARY_NAME="rustfs"
fi
# Verify the binary exists before packaging
if [[ ! -f "$BINARY_NAME" ]]; then
echo "❌ Binary $BINARY_NAME not found in $(pwd)"
if [[ "${{ matrix.platform }}" == "windows" ]]; then
dir
else
ls -la
fi
exit 1
BINARY_FILES=("$BINARY_NAME")
if [[ "${{ matrix.platform }}" == "linux" ]]; then
BINARY_FILES+=("rustfs-cli")
fi
# Verify the binaries exist before packaging
for binary in "${BINARY_FILES[@]}"; do
if [[ ! -f "$binary" ]]; then
echo "❌ Binary $binary not found in $(pwd)"
if [[ "${{ matrix.platform }}" == "windows" ]]; then
dir
else
ls -la
fi
exit 1
fi
done
# Universal packaging function
package_zip() {
local src=$1
@@ -526,8 +546,12 @@ jobs:
}
# Create the zip package
echo "Start packaging: $BINARY_NAME -> ../../../${PACKAGE_NAME}.zip"
package_zip "$BINARY_NAME" "../../../${PACKAGE_NAME}.zip"
echo "Start packaging: ${BINARY_FILES[*]} -> ../../../${PACKAGE_NAME}.zip"
if [[ "${{ matrix.platform }}" == "linux" ]]; then
zip "../../../${PACKAGE_NAME}.zip" "${BINARY_FILES[@]}"
else
package_zip "$BINARY_NAME" "../../../${PACKAGE_NAME}.zip"
fi
cd ../../..
@@ -601,6 +625,28 @@ jobs:
echo "🔧 Build type: ${BUILD_TYPE}"
echo "📊 Version: ${VERSION}"
- name: Verify packaged Linux CLI
if: matrix.platform == 'linux'
shell: bash
run: |
set -euo pipefail
package_file="${{ steps.package.outputs.package_file }}"
archive_members="$(unzip -Z1 "$package_file" | sort)"
expected_members=$'rustfs\nrustfs-cli'
if [[ "$archive_members" != "$expected_members" ]]; then
echo "Linux package must contain exactly rustfs and rustfs-cli" >&2
printf '%s\n' "$archive_members" >&2
exit 1
fi
if [[ "${{ matrix.target }}" == "x86_64-unknown-linux-gnu" ]]; then
cli_dir="$(mktemp -d)"
trap 'rm -rf "$cli_dir"' EXIT
unzip -q "$package_file" rustfs-cli -d "$cli_dir"
"$cli_dir/rustfs-cli" --help >/dev/null
fi
- name: Verify packaged console
if: matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
@@ -1065,7 +1111,7 @@ jobs:
while IFS= read -r preview_tag; do
[[ -n "$preview_tag" ]] || continue
echo "🧹 Deleting preview release $preview_tag (tag kept)"
gh release delete "$preview_tag" --yes
gh release delete "$preview_tag" --repo "${GITHUB_REPOSITORY}" --yes
DELETED=$((DELETED + 1))
done < <(
jq -r --arg tag "$TAG" '
@@ -1100,4 +1146,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
-161
View File
@@ -1,161 +0,0 @@
# 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.
# Companion to ci.yml for required status checks.
#
# ci.yml skips docs-only pull requests via paths-ignore, but the branch ruleset
# requires a check named "Test and Lint" — without this workflow a docs-only PR
# would wait on it forever. This workflow triggers on exactly the paths ci.yml
# ignores and reports success under the same job name. Mixed PRs trigger both
# workflows and the real check still gates: a required check with any failing
# run blocks the merge.
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
#
# "Quick Checks" is mirrored here ahead of the ruleset change that will make it
# required too (rustfs/backlog#1599). Until that change lands this job is
# inert; mirroring it first is what lets the ruleset change happen without
# stranding docs-only PRs on a check nobody reports.
#
# Keep the paths list below in sync with the pull_request paths-ignore list
# in ci.yml, and keep the quick-checks steps below byte-identical to the
# quick-checks job in ci.yml.
name: Continuous Integration (docs only)
on:
pull_request:
types: [ opened, synchronize, reopened ]
branches: [ main ]
paths:
- "**.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"
- "flake.lock"
permissions:
contents: read
jobs:
# Deliberately NOT a bare `echo`. Once "Quick Checks" becomes a required
# check, ci.yml gates every expensive job behind it, so a mixed PR reports
# two check runs with this name: the real one (45-51s) and this companion.
# GitHub has no written contract for how it picks between same-named
# required check runs ("latest wins" vs "any failure blocks"), so instead of
# relying on ordering we make both runs execute the same commands against
# the same merge ref — their conclusions are then necessarily identical and
# the choice does not matter. Keep these steps byte-identical to the
# quick-checks job in ci.yml (a guard script that asserts this, and the paths
# sync below, is tracked in rustfs/backlog#1603).
#
# For a genuinely docs-only PR this adds no strictness (no code changed, so
# fmt and the guards always pass) and costs ~50s of ubuntu-latest.
quick-checks:
name: Quick Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Install ripgrep
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: ripgrep@15.2.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt --all --check
- name: Check unsafe code allowances
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
run: ./scripts/check_logging_guardrails.sh
- name: Check tokio io-uring feature guard
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
run: ./scripts/check_embedded_secrets.sh
- name: Check test wiring
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Check CI paths stay in sync
run: ./scripts/check_ci_paths_sync.sh
- name: Check io_uring lane --lib precondition
run: ./scripts/check_uring_lane_lib_only.sh
test-and-lint:
name: Test and Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
# Docs-only PRs skip the full code CI, but they are exactly where a
# planning-type document could be slipped in (git add -f bypasses
# .gitignore). Run the guard here so the required "Test and Lint" check
# stays meaningful for docs-only changes.
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Satisfy required check for docs-only changes
run: echo "Docs-only change — code CI is skipped by paths-ignore; planning-docs guard passed, reporting success for the required 'Test and Lint' check."
+239 -159
View File
@@ -37,25 +37,6 @@ on:
pull_request:
types: [ opened, synchronize, reopened, closed ]
branches: [ main ]
# Keep this list in sync with the `paths` list in ci-docs-only.yml, which
# reports the required "Test and Lint" check for PRs skipped here.
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"
- "flake.lock"
merge_group:
types: [ checks_requested ]
schedule:
@@ -88,6 +69,32 @@ jobs:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
classify-changes:
name: Select CI scope
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
mode: ${{ steps.scope.outputs.mode }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 2
persist-credentials: false
- name: Select scope using the base revision's policy
id: scope
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
printf '%s\n' 'mode=full' >> "$GITHUB_OUTPUT"
elif [[ "$CI_BASE_SHA" =~ ^[0-9a-f]{40}$ ]] && git show "$CI_BASE_SHA:scripts/ci_gate.py" > "$RUNNER_TEMP/ci-gate-base.py"; then
python3 -I "$RUNNER_TEMP/ci-gate-base.py" select
else
printf '%s\n' 'mode=full' >> "$GITHUB_OUTPUT"
echo "Base CI policy unavailable; running the full matrix."
fi
typos:
name: Typos
if: github.event_name != 'pull_request' || github.event.action != 'closed'
@@ -100,12 +107,7 @@ jobs:
- name: Typos check with custom config file
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
# Fast, compile-free checks that fail early so contributors get feedback in
# ~1 minute instead of waiting for the full test job.
#
# These steps are mirrored byte-for-byte in ci-docs-only.yml so that a mixed
# PR, which reports two check runs named "Quick Checks", cannot get one red
# and one green. Edit both jobs together.
# Fail early with compile-free checks for every pull request.
quick-checks:
name: Quick Checks
if: github.event_name != 'pull_request' || github.event.action != 'closed'
@@ -117,71 +119,13 @@ jobs:
with:
persist-credentials: false
- name: Install ripgrep
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: ripgrep@15.2.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt --all --check
- name: Check unsafe code allowances
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
run: ./scripts/check_logging_guardrails.sh
- name: Check error other(format!) ratchet
run: ./scripts/check_error_other_format_ratchet.sh
- name: Check tokio io-uring feature guard
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
run: ./scripts/check_embedded_secrets.sh
- name: Check test wiring
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Check CI paths stay in sync
run: ./scripts/check_ci_paths_sync.sh
- name: Check io_uring lane --lib precondition
run: ./scripts/check_uring_lane_lib_only.sh
- name: Run shared quick checks
uses: ./.github/actions/quick-checks
test-and-lint:
name: Test and Lint
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
name: Workspace Test and Lint
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 90
env:
@@ -269,6 +213,7 @@ jobs:
CARGO_BUILD_JOBS: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && '3' || '2' }}
run: |
mkdir -p artifacts/test-and-lint
rm -f target/nextest/ci/junit.xml
./scripts/ci/resource_sampler.sh start nextest
trap './scripts/ci/resource_sampler.sh stop' EXIT
set +e
@@ -277,6 +222,12 @@ jobs:
--status-level all --final-status-level all \
2>&1 | tee artifacts/test-and-lint/nextest.log
status=${PIPESTATUS[0]}
if [[ "${status}" -eq 0 ]]; then
cargo nextest list --profile ci --all --exclude e2e_test --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 \
&& test -s target/nextest/ci/junit.xml || status=$?
fi
{
echo "command=cargo nextest run --profile ci --all --exclude e2e_test"
echo "exit_status=${status}"
@@ -308,9 +259,6 @@ jobs:
} > artifacts/test-and-lint/doctest-diagnostics.txt
exit "${status}"
- name: Check offline enrollment E2E root boundary
run: ./scripts/check_offline_enrollment_e2e.sh
- name: Upload test reports and diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
@@ -345,44 +293,36 @@ jobs:
- name: Run rebalance/decommission migration proofs
run: ./scripts/check_migration_gate_count.sh
# Record the reason before this job completes as FAILURE. A separate
# dependent job cancels sibling lanes only after GitHub has preserved this
# required check's failure verdict.
- name: Annotate early-stop reason
if: >-
failure() && github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
run: |
{
echo "## CI early-stop"
echo "Job \`${GITHUB_JOB}\` (Test and Lint) failed; a follow-up job will cancel sibling lanes to free runners."
echo "Sibling jobs showing **cancelled** were stopped by the early-stop follow-up, not by their own failure."
} >> "$GITHUB_STEP_SUMMARY"
# Preserve the required Test and Lint FAILURE verdict before stopping sibling
# lanes. Cancelling from inside test-and-lint changed its own conclusion to
# CANCELLED and hid the actionable failure in the PR checks UI.
cancel-after-test-and-lint-failure:
name: Cancel siblings after Test and Lint failure
if: >-
failure() && needs.test-and-lint.result == 'failure'
&& github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
needs: [ test-and-lint ]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write
# This gate builds into a fresh target directory to isolate the E2E root.
# Give its cold build a separate budget from workspace tests and migration proofs.
offline-enrollment-e2e:
name: Offline Enrollment E2E Root Boundary
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 90
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Cancel remaining jobs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -fsS -X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/cancel"
- 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: 'false'
install-build-packaging-tools: 'false'
install-test-tools: 'false'
- name: Protect Connect test home
run: chmod go-w "$(realpath "$HOME")"
- name: Check offline enrollment E2E root boundary
run: ./scripts/check_offline_enrollment_e2e.sh
# Dedicated serial lane for the ILM / lifecycle integration tests. These tests
# drive the object layer through process-global singletons (the GLOBAL_ENV
@@ -396,8 +336,8 @@ jobs:
# See rustfs/backlog#1148 (ilm-1) and #1155.
test-ilm-integration-serial:
name: ILM Integration (serial)
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 90
env:
@@ -423,11 +363,7 @@ jobs:
# re-enabled by backlog#1304 (restore accepts serialize on a short CAS
# guard; the copy-back no longer holds the #4877 whole-copy-back lock,
# so the mid-restore ongoing read and fast 409 rejection it asserts are
# the implemented contract). The remaining exclusions each hit a
# DIFFERENT, independent issue (all tracked under rustfs/backlog#1148;
# they keep #[ignore] with a backlog reference):
# - test_noncurrent_{expiry,transition}_still_works_after_immediate_compensation_transition:
# noncurrent transition/expiry after an immediate compensation transition.
# the implemented contract).
- name: Run ignored ILM integration tests serially
env:
# Match the measured Test and Lint link budget. The default exposed
@@ -440,7 +376,7 @@ jobs:
NEXTEST_HIDE_PROGRESS_BAR=1 timeout --verbose --signal=TERM --kill-after=30s 80m \
cargo nextest run -j1 --run-ignored ignored-only \
-p rustfs-scanner -p rustfs \
-E '(binary(lifecycle_integration_test) or (package(rustfs) and test(lifecycle_transition_api_test))) and not (test(test_noncurrent_expiry_still_works_after_immediate_compensation_transition) or test(test_noncurrent_transition_still_works_after_immediate_compensation_transition))' \
-E 'binary(lifecycle_integration_test) or (package(rustfs) and test(lifecycle_transition_api_test))' \
--status-level all --final-status-level all \
2>&1 | tee artifacts/ilm-integration/nextest.log
status=${PIPESTATUS[0]}
@@ -464,8 +400,8 @@ jobs:
test-and-lint-rio-v2:
name: Test and Lint (rio-v2)
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 90
env:
@@ -505,8 +441,8 @@ jobs:
connect-short-credential-boundary:
name: Connect Short Credential Boundary
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 60
env:
@@ -563,8 +499,8 @@ jobs:
test-and-lint-protocols:
name: "Test and Lint (${{ matrix.features.name }})"
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 90
strategy:
@@ -617,8 +553,8 @@ jobs:
build-rustfs-debug-binary:
name: Build RustFS Debug Binary
if: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs: [ quick-checks, classify-changes ]
runs-on: sm-standard-4
timeout-minutes: 30
env:
@@ -638,13 +574,60 @@ jobs:
install-build-packaging-tools: 'false'
- name: Build debug binary
run: cargo build -p rustfs --bins --features e2e-test-hooks
run: |
python3 - <<'PYBUILD'
import hashlib
import json
import os
import pathlib
import subprocess
def git(*args):
return subprocess.check_output(["git", *args], text=True).strip()
def sha256(path):
digest = hashlib.sha256()
with pathlib.Path(path).open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
argv = ["python3", "scripts/e2e_binary.py", "build", "--bins", "--features", "e2e-test-hooks"]
commit, tree = git("rev-parse", "HEAD"), git("rev-parse", "HEAD^{tree}")
clean_before = not git("status", "--porcelain", "--untracked-files=normal")
if not clean_before:
raise SystemExit("hooks binary requires a clean build checkout")
lock_sha256 = sha256("Cargo.lock")
lock_git_blob = git("hash-object", "Cargo.lock")
rustc = subprocess.check_output(["rustc", "-vV"], text=True)
host = next(line.removeprefix("host: ") for line in rustc.splitlines() if line.startswith("host: "))
if os.environ.get("CARGO_BUILD_TARGET") or pathlib.Path(os.environ.get("CARGO_TARGET_DIR", "target")).resolve() != pathlib.Path("target").resolve():
raise SystemExit("this artifact requires the native target/debug output")
subprocess.run(argv, check=True)
clean_after = not git("status", "--porcelain", "--untracked-files=normal")
if not clean_after or commit != git("rev-parse", "HEAD") or tree != git("rev-parse", "HEAD^{tree}") or lock_sha256 != sha256("Cargo.lock"):
raise SystemExit("hooks binary source changed while building")
manifest = {
"schema": 1, "commit": commit, "tree": tree,
"clean_before": clean_before, "clean_after": clean_after,
"lock_sha256": lock_sha256, "lock_git_blob": lock_git_blob,
"argv": argv, "profile": "debug", "target": host,
"features": ["e2e-test-hooks"],
"rustc_verbose": rustc,
"build_flags": {key: os.environ[key] for key in ("RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "CARGO_BUILD_TARGET", "CARGO_TARGET_DIR", "RUSTUP_TOOLCHAIN") if key in os.environ},
"binary_sha256": sha256("target/debug/rustfs"),
}
pathlib.Path("target/debug/rustfs.e2e-startup-cas-build.json").write_text(json.dumps(manifest, indent=2) + "\n")
PYBUILD
- name: Upload debug binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-debug-binary
path: target/debug/rustfs
path: |
target/debug/rustfs
target/debug/rustfs.e2e.json
target/debug/rustfs.e2e-startup-cas-build.json
if-no-files-found: error
retention-days: 1
@@ -676,13 +659,15 @@ jobs:
install-build-packaging-tools: 'false'
- name: Build debug binary with rio-v2
run: cargo build -p rustfs --bins --features rio-v2,e2e-test-hooks
run: python3 scripts/e2e_binary.py build --bins --features rio-v2,e2e-test-hooks
- name: Upload debug binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-debug-binary-rio-v2
path: target/debug/rustfs
path: |
target/debug/rustfs
target/debug/rustfs.e2e.json
if-no-files-found: error
retention-days: 1
@@ -694,8 +679,8 @@ 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: github.event_name != 'pull_request' || github.event.action != 'closed'
needs: [ quick-checks ]
if: needs.classify-changes.outputs.mode == 'full' && (github.event_name != 'pull_request' || github.event.action != 'closed')
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
# the probe succeeds and the tests exercise the real UringBackend/FdCache/
@@ -831,7 +816,7 @@ jobs:
NEXTEST_ARCHIVE: ${{ runner.temp }}/rustfs-e2e-smoke.tar.zst
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-e2e-smoke-logs
run: |
cargo nextest run --profile e2e-smoke --archive-file "${NEXTEST_ARCHIVE}" \
python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke --archive-file "${NEXTEST_ARCHIVE}" \
--status-level all --final-status-level all --failure-output final
- name: Upload e2e smoke diagnostics
@@ -867,7 +852,7 @@ jobs:
RUSTFS_TEST_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')"
RUSTFS_TEST_PORT="${RUSTFS_TEST_PORT}" \
RUSTFS_TEST_LOG="${RUN_ROOT}/rustfs.log" \
./scripts/e2e-run.sh ./target/debug/rustfs "${RUN_ROOT}/data"
python3 scripts/e2e_binary.py run --features e2e-test-hooks -- ./scripts/e2e-run.sh ./target/debug/rustfs "${RUN_ROOT}/data"
- name: Upload test logs
if: failure()
@@ -906,6 +891,17 @@ jobs:
cache-save-if: 'false'
install-build-packaging-tools: 'false'
- name: Install network fault-injection tools
run: |
sudo apt-get install -y iptables
sudo -n iptables --version
# The endpoint-blackhole heal scenario needs CAP_NET_ADMIN. Containerised
# runners can run iptables but not touch the rule set; the test then logs
# a skip instead of failing, so surface that here where it is visible.
if ! sudo -n iptables -w 5 -S OUTPUT >/dev/null 2>&1; then
echo "::warning::iptables cannot read the OUTPUT chain on this runner (no CAP_NET_ADMIN); the endpoint-blackhole heal scenario will be skipped"
fi
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
@@ -957,6 +953,37 @@ jobs:
- name: Make binary executable
run: chmod +x ./target/debug/rustfs
- name: Preserve startup CAS binary input
env:
STARTUP_CAS_INPUT: ${{ runner.temp }}/rustfs-startup-cas-input
run: |
python3 - <<'PYINPUT'
import hashlib
import json
import os
import pathlib
import shutil
import subprocess
source = pathlib.Path("target/debug/rustfs")
manifest_path = source.with_name("rustfs.e2e-startup-cas-build.json")
manifest = json.loads(manifest_path.read_text())
target = pathlib.Path(os.environ["STARTUP_CAS_INPUT"])
target.mkdir(parents=True, exist_ok=True)
binary = target / "rustfs"
shutil.copy2(source, binary)
digest = hashlib.sha256()
with binary.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
if manifest["binary_sha256"] != digest.hexdigest() or manifest["commit"] != commit:
raise SystemExit("downloaded hooks binary identity mismatch")
shutil.copy2(manifest_path, target / manifest_path.name)
shutil.copy2(source.with_name("rustfs.e2e.json"), target / "rustfs.e2e.json")
binary.chmod(0o755)
PYINPUT
- name: Verify e2e full membership
env:
NEXTEST_LISTING: ${{ runner.temp }}/rustfs-e2e-full-list.json
@@ -969,7 +996,12 @@ jobs:
# extend that filter, never add ad-hoc e2e jobs here. Reuses the downloaded
# debug binary; each test spawns its own rustfs server on a random port.
- name: Run e2e full suite
run: cargo nextest run --profile e2e-full -p e2e_test
env:
CARGO_BIN_EXE_rustfs: ${{ runner.temp }}/rustfs-startup-cas-input/rustfs
RUSTFS_E2E_STARTUP_CAS_BINARY: ${{ runner.temp }}/rustfs-startup-cas-input/rustfs
RUSTFS_E2E_STARTUP_CAS_BUILD_MANIFEST: ${{ runner.temp }}/rustfs-startup-cas-input/rustfs.e2e-startup-cas-build.json
RUSTFS_E2E_STARTUP_CAS_ARTIFACT_DIR: ${{ runner.temp }}/rustfs-startup-cas-evidence
run: python3 scripts/e2e_binary.py run --binary "$RUSTFS_E2E_STARTUP_CAS_BINARY" --features e2e-test-hooks -- cargo nextest run --profile e2e-full -p e2e_test
- name: Upload junit
if: always()
@@ -981,6 +1013,17 @@ jobs:
${{ runner.temp }}/rustfs-e2e-full-list.json
retention-days: 7
- name: Upload startup CAS evidence
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: fresh-startup-cas-evidence-${{ github.run_number }}
path: |
${{ runner.temp }}/rustfs-startup-cas-evidence
${{ runner.temp }}/rustfs-startup-cas-input/rustfs.e2e-startup-cas-build.json
if-no-files-found: warn
retention-days: 7
e2e-tests-rio-v2:
name: End-to-End Tests (rio-v2)
# Inherits the schedule/dispatch-only gate through needs: on every other
@@ -1030,7 +1073,7 @@ jobs:
- name: Run end-to-end tests
run: |
s3s-e2e --version
./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs
python3 scripts/e2e_binary.py run --features rio-v2,e2e-test-hooks -- ./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs
- name: Upload test logs
if: failure()
@@ -1073,7 +1116,7 @@ jobs:
S3_PORT="${S3_PORT}" \
DATA_ROOT="${RUN_ROOT}" \
S3TESTS_CONF=artifacts/s3tests-single/s3tests.conf \
./scripts/s3-tests/run.sh
python3 scripts/e2e_binary.py run --features e2e-test-hooks -- ./scripts/s3-tests/run.sh
- name: Upload s3 test artifacts
if: always()
@@ -1155,7 +1198,7 @@ jobs:
S3_PORT="${S3_PORT}" \
DATA_ROOT="${RUN_ROOT}" \
S3TESTS_CONF=artifacts/s3tests-single/s3tests.conf \
./scripts/s3-tests/run.sh
python3 scripts/e2e_binary.py run --features e2e-test-hooks -- ./scripts/s3-tests/run.sh
- name: Upload s3 test artifacts
if: always()
@@ -1166,12 +1209,49 @@ jobs:
if-no-files-found: ignore
retention-days: 3
alert-on-failure:
name: Alert on scheduled failure
required-checks:
name: Test and Lint
if: always() && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs:
- classify-changes
- typos
- quick-checks
- test-and-lint
- offline-enrollment-e2e
- test-ilm-integration-serial
- test-and-lint-rio-v2
- connect-short-credential-boundary
- test-and-lint-protocols
- build-rustfs-debug-binary
- uring-integration
- e2e-tests
- s3-implemented-tests
- s3-lifecycle-behavior-tests
- build-rustfs-debug-binary-rio-v2
- e2e-tests-rio-v2
- e2e-full
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Require the expected result of every CI lane
env:
CI_NEEDS: ${{ toJSON(needs) }}
shell: bash
run: python3 scripts/ci_gate.py verify
alert-on-failure:
name: Alert on scheduled failure
needs:
- classify-changes
- connect-short-credential-boundary
- required-checks
- typos
- quick-checks
- test-and-lint
- offline-enrollment-e2e
- test-ilm-integration-serial
- test-and-lint-rio-v2
- test-and-lint-protocols
@@ -1198,4 +1278,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect performance.drive service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
performance-drive:
name: Verify native Linux x86_64 performance.drive service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_PERFORMANCE_DRIVE_EVIDENCE="$GITHUB_WORKSPACE/performance-drive-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=performance-drive
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('performance-drive-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
performance-drive-service-job-evidence.json >performance-drive-service-job-evidence.bound.json
mv performance-drive-service-job-evidence.bound.json performance-drive-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('performance-drive-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-performance-drive-evidence-${{ github.run_id }}
path: |
performance-drive-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect performance.network service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
performance-network:
name: Verify native Linux x86_64 performance.network service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_PERFORMANCE_NETWORK_EVIDENCE="$GITHUB_WORKSPACE/performance-network-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=performance-network
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('performance-network-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
performance-network-service-job-evidence.json >performance-network-service-job-evidence.bound.json
mv performance-network-service-job-evidence.bound.json performance-network-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('performance-network-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-performance-network-evidence-${{ github.run_id }}
path: |
performance-network-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect profile.cpu service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
profile-cpu:
name: Verify native Linux x86_64 profile.cpu service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_PROFILE_EVIDENCE="$GITHUB_WORKSPACE/profile-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=profile
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('profile-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
profile-service-job-evidence.json >profile-service-job-evidence.bound.json
mv profile-service-job-evidence.bound.json profile-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('profile-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-profile-cpu-evidence-${{ github.run_id }}
path: |
profile-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect profile.threads service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
profile-threads:
name: Verify native Linux x86_64 profile.threads service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_PROFILE_THREADS_EVIDENCE="$GITHUB_WORKSPACE/profile-threads-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=profile-threads
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('profile-threads-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
profile-threads-service-job-evidence.json >profile-threads-service-job-evidence.bound.json
mv profile-threads-service-job-evidence.bound.json profile-threads-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('profile-threads-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-profile-threads-evidence-${{ github.run_id }}
path: |
profile-threads-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect top.api service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
top-api:
name: Verify native Linux x86_64 top.api service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_TOP_API_EVIDENCE="$GITHUB_WORKSPACE/top-api-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=top-api
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('top-api-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
top-api-service-job-evidence.json >top-api-service-job-evidence.bound.json
mv top-api-service-job-evidence.bound.json top-api-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('top-api-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-top-api-evidence-${{ github.run_id }}
path: |
top-api-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,142 @@
# Copyright 2024 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.
name: Connect top.disk artifact acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Successful main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
permissions:
actions: read
contents: read
jobs:
top-disk:
name: Verify native Linux x86_64 top.disk artifact
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.conclusion' <<<"$run") == success ]]
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
- name: Run top.disk acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
BINARY_SHA256: ${{ inputs.binary_sha256 }}
run: |
set -euo pipefail
scripts/ci/check_connect_top_disk_artifact.sh \
binary/rustfs "$SOURCE_SHA" "$BINARY_SHA256" \
top-disk-runtime-evidence.json top-disk.zip
- name: Bind workflow and artifact provenance
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest}' \
top-disk-runtime-evidence.json >top-disk-runtime-evidence.bound.json
mv top-disk-runtime-evidence.bound.json top-disk-runtime-evidence.json
- name: Upload acceptance evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-top-disk-evidence-${{ github.run_id }}
path: |
top-disk-runtime-evidence.json
top-disk.zip
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect top.locks service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
top-locks:
name: Verify native Linux x86_64 top.locks service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_TOP_LOCKS_EVIDENCE="$GITHUB_WORKSPACE/top-locks-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=top-locks
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('top-locks-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
top-locks-service-job-evidence.json >top-locks-service-job-evidence.bound.json
mv top-locks-service-job-evidence.bound.json top-locks-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('top-locks-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-top-locks-evidence-${{ github.run_id }}
path: |
top-locks-service-job-evidence.json
retention-days: 14
if-no-files-found: error
@@ -0,0 +1,192 @@
# Copyright 2024 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.
name: Connect top.rpc service-job acceptance
on:
workflow_dispatch:
inputs:
build_run_id:
description: Main-branch Build and Release workflow run ID
required: true
type: string
artifact_id:
description: Linux x86_64 GNU artifact ID from that run
required: true
type: string
source_sha:
description: Exact 40-character source commit
required: true
type: string
artifact_digest:
description: GitHub artifact digest including sha256 prefix
required: true
type: string
binary_sha256:
description: Expected rustfs binary SHA-256
required: true
type: string
connect_sha:
description: Exact 40-character RustFS Connect harness commit
required: true
type: string
permissions:
actions: read
contents: read
jobs:
top-rpc:
name: Verify native Linux x86_64 top.rpc service job
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout acceptance harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Checkout exact RustFS source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
path: rustfs-source
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Checkout exact Connect harness
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/connect
path: connect-harness
persist-credentials: false
ref: ${{ inputs.connect_sha }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.22.2
cache: npm
cache-dependency-path: connect-harness/web/package-lock.json
- name: Verify source run and artifact identity
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
SOURCE_SHA: ${{ inputs.source_sha }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
run: |
set -euo pipefail
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
jq -e --arg name "$expected_job" '
[.[].jobs[] | select(.name == $name)] as $matches
| (($matches | length) == 1 and $matches[0].conclusion == "success")
' <<<"$jobs" >/dev/null
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
- name: Download exact build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
artifact-ids: ${{ inputs.artifact_id }}
path: artifact
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}
- name: Extract exact RustFS binary
shell: bash
env:
BINARY_SHA256: ${{ inputs.binary_sha256 }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
short_sha=${SOURCE_SHA:0:7}
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
[[ -n "$package" ]]
mkdir -p binary
unzip -qq "$package" rustfs -d binary
chmod +x binary/rustfs
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
- name: Run signed service job acceptance
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
npm --prefix connect-harness/web ci
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
make -C connect-harness e2e-connected-dispatch-check
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
CONNECT_E2E_TOP_RPC_EVIDENCE="$GITHUB_WORKSPACE/top-rpc-service-job-evidence.json" \
make -C connect-harness e2e-connected E2E_SCENARIO=top-rpc
- name: Bind workflow and artifact provenance
if: ${{ always() && hashFiles('top-rpc-service-job-evidence.json') != '' }}
shell: bash
env:
BUILD_RUN_ID: ${{ inputs.build_run_id }}
ARTIFACT_ID: ${{ inputs.artifact_id }}
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
CONNECT_SHA: ${{ inputs.connect_sha }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
jq \
--arg workflowRunId "$GITHUB_RUN_ID" \
--arg buildRunId "$BUILD_RUN_ID" \
--arg artifactId "$ARTIFACT_ID" \
--arg artifactDigest "$ARTIFACT_DIGEST" \
--arg connectSha "$CONNECT_SHA" \
--arg sourceSha "$SOURCE_SHA" \
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
top-rpc-service-job-evidence.json >top-rpc-service-job-evidence.bound.json
mv top-rpc-service-job-evidence.bound.json top-rpc-service-job-evidence.json
- name: Upload acceptance evidence
if: ${{ always() && hashFiles('top-rpc-service-job-evidence.json') != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: connect-top-rpc-evidence-${{ github.run_id }}
path: |
top-rpc-service-job-evidence.json
retention-days: 14
if-no-files-found: error
+1 -1
View File
@@ -140,4 +140,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+26 -8
View File
@@ -121,6 +121,22 @@ jobs:
create_latest=false
source_ref="$GITHUB_SHA"
# Pre-GA policy: until the first stable (vX.Y.Z) tag exists, every
# prerelease (alpha/beta/rc) also moves `latest`, so users pulling
# `latest` get the newest test build. Once a stable tag is published
# this returns false and `latest` follows stable releases only.
prerelease_moves_latest() {
local stable_tags
stable_tags=$(git ls-remote --tags --refs origin 2>/dev/null \
| awk '{print $2}' \
| grep -E '^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$' || true)
if [[ -z "$stable_tags" ]]; then
return 0
fi
echo "ℹ️ Stable release tag(s) already exist; prereleases no longer update latest"
return 1
}
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
# Triggered by build workflow completion
echo "🔗 Triggered by build workflow completion"
@@ -184,8 +200,8 @@ jobs:
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
build_type="prerelease"
is_prerelease=true
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
# Pre-GA policy: prereleases update latest until the first stable tag exists.
if prerelease_moves_latest; then
create_latest=true
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
else
@@ -243,8 +259,8 @@ jobs:
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
build_type="prerelease"
is_prerelease=true
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
# Pre-GA policy: prereleases update latest until the first stable tag exists.
if prerelease_moves_latest; then
create_latest=true
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
else
@@ -394,11 +410,13 @@ jobs:
TAG_BASE="${VERSION}${VARIANT_SUFFIX}"
TAGS="${{ env.REGISTRY_DOCKERHUB }}:$TAG_BASE,${{ env.REGISTRY_GHCR }}:$TAG_BASE,${{ env.REGISTRY_QUAY }}:$TAG_BASE"
# Add channel tags for prereleases and latest for stable
# Add latest when requested (stable releases, and prereleases before GA)
if [[ "$CREATE_LATEST" == "true" ]]; then
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
fi
# Always add the channel tag for prereleases, independent of latest
if [[ "$BUILD_TYPE" == "prerelease" ]]; then
# Prerelease channel tags (alpha, beta, rc)
if [[ "$VERSION" == *"alpha"* ]]; then
CHANNEL="alpha"
@@ -555,7 +573,7 @@ jobs:
"prerelease")
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
echo "⚠️ This is a prerelease image - use with caution"
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
# Prereleases move latest until the first stable tag exists (pre-GA policy).
if [[ "$CREATE_LATEST" == "true" ]]; then
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
else
+215
View File
@@ -0,0 +1,215 @@
# Copyright 2024 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/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.
# 4-node 4-disk distributed e2e lane.
#
# Each selected test starts a real localhost cluster via
# `RustFSTestClusterEnvironment` (4 processes; 4 drives per node unless the
# case is a two-site 4-node 1-drive pair or a 4-node upgrade). Membership is
# `[profile.e2e-distributed]` in `.config/nextest.toml`. Storage-sensitive PRs,
# nightly runs, and manual dispatches all execute the same fail-closed suite.
# Upgrade cases download the same pinned previous release as e2e-upgrade.yml.
#
# Isolated pool filesystems: expand/decommission/rebalance cases require
# independent `statfs` capacity. This job runs on GitHub-hosted
# `ubuntu-latest` because the self-hosted `sm-standard-4` ARC pods cannot
# create filesystems: `mount -o loop` fails with ENOENT (no
# `/dev/loop-control`), and `mount -t tmpfs` fails with "cannot mount tmpfs
# read-only" (no `CAP_SYS_ADMIN` in the initial namespace). The same reason
# `uring-integration` and `e2e-s3tests.yml` left that label. The prepare
# step mounts four 1 GiB tmpfs instances and exports `RUSTFS_E2E_POOL_ROOTS`.
name: e2e-distributed
on:
pull_request:
paths:
- "Cargo.lock"
- "Cargo.toml"
- ".config/nextest.toml"
- ".github/workflows/e2e-distributed.yml"
- "crates/audit/**"
- "crates/common/**"
- "crates/config/**"
- "crates/e2e_test/**"
- "crates/ecstore/**"
- "crates/filemeta/**"
- "crates/heal/**"
- "crates/iam/**"
- "crates/lock/**"
- "crates/madmin/**"
- "crates/notify/**"
- "crates/replication/**"
- "crates/s3-client/**"
- "crates/s3-ops/**"
- "crates/s3-types/**"
- "crates/scanner/**"
- "crates/storage-api/**"
- "crates/utils/**"
- "rustfs/**"
workflow_dispatch:
inputs:
filter:
description: "Optional nextest -E filter (default: the whole e2e-distributed profile)"
required: false
default: ""
schedule:
# 05:53 UTC nightly — clear of e2e-nightly (04:29) and ODM interop (05:23).
- cron: "53 5 * * *"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name != 'schedule' }}
jobs:
distributed:
name: Distributed 4-node 4-disk e2e
# GitHub-hosted VM: loop and tmpfs mounts work here. sm-standard-4 is an
# ARC pod and rejects both (`mount -o loop` ENOENT, tmpfs "read-only").
runs-on: ubuntu-latest
timeout-minutes: 180
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NO_PROXY: 127.0.0.1,localhost
HTTP_PROXY: ""
HTTPS_PROXY: ""
# Pinned previous release used by distributed::upgrade_test (same pin as e2e-upgrade.yml).
UPGRADE_SOURCE_VERSION: 1.0.0-rc.2
UPGRADE_SOURCE_ASSET: rustfs-linux-x86_64-gnu-v1.0.0-rc.2.zip
UPGRADE_SOURCE_SHA256: 7c789386bf85278f865b8e0d359bf4edb84d5aa408cc3fa54a18c25ca74cd6e7
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
# Dedicated key: ubuntu-latest and sm-standard-4 share runner.os, so
# a shared key would mix VM and ARC pod target/ artifacts.
cache-shared-key: ci-e2e-distributed-hosted
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
install-build-packaging-tools: 'false'
- name: Prepare isolated filesystems for pool movement
run: |
set -euo pipefail
mount_base="${RUNNER_TEMP}/rustfs-e2e-pools"
mkdir -p "${mount_base}"
roots=()
for pool in 0 1 2 3; do
mountpoint="${mount_base}/pool-${pool}"
mkdir -p "${mountpoint}"
# Sized tmpfs reports a distinct st_dev and independent 1G
# statfs capacity. Requires a VM runner (ubuntu-latest).
if ! sudo mount -t tmpfs -o size=1G,nosuid,nodev,mode=1777 tmpfs "${mountpoint}"; then
echo "tmpfs mount failed on $(uname -a)" >&2
findmnt || true
grep Cap /proc/self/status || true
exit 1
fi
sudo chmod 1777 "${mountpoint}"
roots+=("${mountpoint}")
done
printf -v joined_roots '%s:' "${roots[@]}"
echo "RUSTFS_E2E_POOL_ROOTS=${joined_roots%:}" >> "${GITHUB_ENV}"
findmnt --noheadings --output TARGET,SOURCE,FSTYPE,SIZE --target "${roots[0]}"
findmnt --noheadings --output TARGET,SOURCE,FSTYPE,SIZE --target "${roots[1]}"
findmnt --noheadings --output TARGET,SOURCE,FSTYPE,SIZE --target "${roots[2]}"
findmnt --noheadings --output TARGET,SOURCE,FSTYPE,SIZE --target "${roots[3]}"
- name: Download pinned previous release
env:
SOURCE_DIR: ${{ runner.temp }}/rustfs-upgrade-source
run: |
set -euo pipefail
mkdir -p "$SOURCE_DIR"
archive="$SOURCE_DIR/$UPGRADE_SOURCE_ASSET"
curl --fail --location --retry 3 --output "$archive" \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${UPGRADE_SOURCE_VERSION}/${UPGRADE_SOURCE_ASSET}"
echo "$UPGRADE_SOURCE_SHA256 $archive" | sha256sum --check --strict
unzip -q "$archive" -d "$SOURCE_DIR"
chmod +x "$SOURCE_DIR/rustfs"
test -x "$SOURCE_DIR/rustfs"
echo "RUSTFS_UPGRADE_SOURCE_BINARY=$SOURCE_DIR/rustfs" >> "$GITHUB_ENV"
- name: Build rustfs binary
run: |
python3 scripts/e2e_binary.py build --bins
- name: Verify distributed e2e membership
env:
NEXTEST_LISTING: ${{ runner.temp }}/rustfs-e2e-distributed-list.json
run: |
cargo nextest list --profile e2e-distributed -p e2e_test --message-format json > "${NEXTEST_LISTING}"
python3 ./scripts/check_test_wiring.py --check-profile e2e-distributed "${NEXTEST_LISTING}"
- name: Run distributed 4-node e2e suite
env:
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-e2e-distributed-logs
FILTER: ${{ inputs.filter }}
run: |
set -euo pipefail
if [ -n "${FILTER}" ]; then
python3 scripts/e2e_binary.py run -- cargo nextest run --profile e2e-distributed -p e2e_test -E "${FILTER}"
else
python3 scripts/e2e_binary.py run -- cargo nextest run --profile e2e-distributed -p e2e_test --no-tests=fail
fi
- name: Upload distributed e2e diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-distributed-${{ github.run_number }}
path: |
target/nextest/e2e-distributed/junit.xml
${{ runner.temp }}/rustfs-e2e-distributed-list.json
${{ runner.temp }}/rustfs-e2e-distributed-logs/
retention-days: 7
if-no-files-found: warn
- name: Unmount isolated pool filesystems
if: always()
run: |
set -euo pipefail
mount_base="${RUNNER_TEMP}/rustfs-e2e-pools"
for pool in 0 1 2 3; do
mountpoint="${mount_base}/pool-${pool}"
if mountpoint --quiet "${mountpoint}"; then
sudo umount "${mountpoint}"
fi
done
alert-on-failure:
name: Alert on scheduled failure
needs: [distributed]
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+11 -12
View File
@@ -89,14 +89,10 @@ jobs:
- name: Verify awscurl
run: test -x "$AWSCURL_PATH"
# Build the rustfs binary once up front. The e2e tests spawn it as a
# child process (crates/e2e_test/src/common.rs) and will build it on
# demand otherwise, but a single explicit build avoids several parallel
# nextest test processes racing to build it at once.
# Build once and carry its source/binary identity into the test invocation.
- name: Build rustfs binary
run: |
cargo build -p rustfs --bins
: > target/debug/rustfs.features
python3 scripts/e2e_binary.py build --bins
- name: Verify replication e2e membership
env:
@@ -108,7 +104,7 @@ jobs:
- name: Run replication e2e nightly suite
env:
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-e2e-repl-nightly-logs
run: cargo nextest run --profile e2e-repl-nightly -p e2e_test
run: python3 scripts/e2e_binary.py run -- cargo nextest run --profile e2e-repl-nightly -p e2e_test
- name: Upload nextest junit report
if: always()
@@ -144,8 +140,7 @@ jobs:
- name: Build rustfs binary
run: |
cargo build -p rustfs --bins --features e2e-test-hooks
: > target/debug/rustfs.features
python3 scripts/e2e_binary.py build --bins --features e2e-test-hooks
- name: Verify cluster fault e2e membership
env:
@@ -156,8 +151,9 @@ jobs:
- name: Run cluster fault e2e nightly suite
env:
CARGO_BIN_EXE_rustfs: ${{ github.workspace }}/target/debug/rustfs
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-e2e-nightly-logs
run: cargo nextest run --profile e2e-nightly -p e2e_test
run: python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-nightly -p e2e_test
- name: Upload cluster fault diagnostics
if: always()
@@ -198,6 +194,9 @@ jobs:
sudo apt-get install -y -qq iproute2
ss -tn state CLOSE-WAIT >/dev/null
- name: Build protocol server
run: python3 scripts/e2e_binary.py build --features "$RUSTFS_BUILD_FEATURES"
# The suite owns fixed protocol ports and serializes its internal cases.
- name: Verify protocol e2e membership
env:
@@ -210,7 +209,7 @@ jobs:
env:
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-protocol-e2e-logs
run: >-
cargo nextest run -j 1 --profile e2e-protocols -p e2e_test --no-capture
python3 scripts/e2e_binary.py run --features "$RUSTFS_BUILD_FEATURES" -- cargo nextest run -j 1 --profile e2e-protocols -p e2e_test --no-capture
- name: Upload protocol diagnostics
if: always()
@@ -243,4 +242,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+1 -1
View File
@@ -460,4 +460,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+61 -12
View File
@@ -19,7 +19,9 @@ on:
paths:
- ".github/workflows/e2e-upgrade.yml"
- "crates/e2e_test/src/common.rs"
- "crates/e2e_test/src/fake_s3_target/**"
- "crates/e2e_test/src/lib.rs"
- "crates/e2e_test/src/replication_extension_test.rs"
- "crates/e2e_test/src/upgrade_compatibility_test.rs"
- "crates/ecstore/**"
- "crates/filemeta/**"
@@ -44,13 +46,50 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
UPGRADE_SOURCE_VERSION: 1.0.0-rc.2
UPGRADE_SOURCE_ASSET: rustfs-linux-x86_64-gnu-v1.0.0-rc.2.zip
UPGRADE_SOURCE_SHA256: 7c789386bf85278f865b8e0d359bf4edb84d5aa408cc3fa54a18c25ca74cd6e7
UPGRADE_SOURCE_VERSION: 1.0.0-rc.5
UPGRADE_SOURCE_ASSET: rustfs-linux-x86_64-gnu-v1.0.0-rc.5.zip
UPGRADE_SOURCE_SHA256: 3ee8df71e8edcfada533be452c4135868f697bc515460ae97b027313eade7a3d
jobs:
direct-upgrade:
name: Direct upgrade from rc.2
upgrade:
name: ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
# The two `_from_rc2_` tests keep their names: they assert
# release-independent object contracts and pass unchanged against the
# newer pinned source, so renaming them would only churn history and
# the CI required-check names. UPGRADE_SOURCE_VERSION above is the
# single source of truth for which release they actually run against.
- name: Direct upgrade from the previous release
cache_key: e2e-direct-upgrade
test: direct_upgrade_from_rc2_preserves_object_contracts
artifact: direct-upgrade
- name: Mixed-version rolling upgrade from the previous release
cache_key: e2e-mixed-version-upgrade
test: rolling_upgrade_from_rc2_preserves_mixed_version_contracts
artifact: mixed-version-upgrade
- name: Bucket configuration survives the upgrade
cache_key: e2e-bucket-config-upgrade
test: direct_upgrade_from_previous_release_preserves_bucket_configuration
artifact: bucket-config-upgrade
- name: Rollback reads current bucket metadata
cache_key: e2e-bucket-config-rollback
test: rollback_to_previous_release_reads_current_bucket_metadata
artifact: bucket-config-rollback
- name: ODM configuration recovery after rc.5 rollback
cache_key: e2e-odm-config-rollback
test: rc5_rollback_requires_restoring_odm_configuration
artifact: odm-config-rollback
- name: Multipart layouts survive the rc.5 upgrade
cache_key: e2e-multipart-layout-upgrade
test: direct_upgrade_from_rc5_preserves_multipart_layouts
artifact: multipart-layout-upgrade
- name: rc.5 multipart replication baseline
cache_key: e2e-multipart-layout-baseline
test: rc5_baseline_replicates_multipart_layouts
artifact: multipart-layout-baseline
runs-on: ubuntu-latest
timeout-minutes: 60
env:
@@ -64,7 +103,7 @@ jobs:
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
cache-shared-key: e2e-direct-upgrade
cache-shared-key: ${{ matrix.cache_key }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
install-build-packaging-tools: "false"
@@ -86,20 +125,30 @@ jobs:
- name: Build current RustFS binary
run: |
cargo build --locked -p rustfs --bin rustfs
: > target/debug/rustfs.features
python3 scripts/e2e_binary.py build
- name: Run direct-upgrade compatibility test
- name: Run upgrade compatibility test
env:
RUSTFS_SCANNER_HEAL_G09_EVIDENCE_DIR: ${{ runner.temp }}/rustfs-upgrade-g09-evidence/${{ matrix.artifact }}
run: |
cargo test --locked -p e2e_test \
upgrade_compatibility_test::direct_upgrade_from_rc2_preserves_object_contracts \
python3 scripts/e2e_binary.py run -- cargo test --locked -p e2e_test \
"upgrade_compatibility_test::${{ matrix.test }}" \
-- --ignored --exact --nocapture
- name: Upload scanner/heal G09 evidence
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ matrix.artifact }}-g09-evidence-${{ github.run_number }}
path: ${{ runner.temp }}/rustfs-upgrade-g09-evidence/${{ matrix.artifact }}
if-no-files-found: ignore
retention-days: 14
- name: Upload server logs
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: direct-upgrade-server-logs-${{ github.run_number }}
name: ${{ matrix.artifact }}-server-logs-${{ github.run_number }}
path: ${{ runner.temp }}/rustfs-upgrade-logs
if-no-files-found: warn
retention-days: 14
@@ -0,0 +1,31 @@
name: Functional chain health
on:
schedule:
- cron: '23 * * * *'
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: functional-chain-health
cancel-in-progress: false
jobs:
collect:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Collect and publish verified chain health
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
NIGHTLY_SOURCE_REF: ${{ vars.NIGHTLY_BRANCH || 'main' }}
run: python3 scripts/functional_chain_health.py --source-ref "${NIGHTLY_SOURCE_REF}" --publish --output "${RUNNER_TEMP}/chain-health.json"
- name: Retain health observation
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-health-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-health.json
if-no-files-found: error
+1 -1
View File
@@ -252,4 +252,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+17 -17
View File
@@ -20,27 +20,27 @@
# each run with Docker and then runs the `#[ignore]` reader tests in
# rustfs/src/storage/minio_generated_read_test.rs.
#
# Scope: end-to-end MinIO-to-RustFS SSE interop is NOT implemented yet. Both
# envelope parsers reject MinIO's own wrapped-DEK shape — see
# `is_data_key_envelope` in crates/kms/src/encryption/dek.rs and the
# `deny_unknown_fields` `LocalSseDekEnvelope` in rustfs/src/storage/sse.rs — and
# closing that gap is tracked in rustfs/backlog#1638. Treat this job as the
# harness for #1638, not as standing evidence that a MinIO migration reads back.
# Scope: MinIO-to-RustFS SSE read interop is implemented behind the `rio-v2`
# feature for MinIO's builtin static-KMS deployments — SSE-S3 and SSE-KMS
# (single- and multipart) since rustfs/rustfs#6191, SSE-C detection since the
# rustfs/backlog#1638 D2 close-out. This job is the standing evidence: it
# regenerates real MinIO backend trees and proves byte-identical plaintext
# reconstruction. KES/MinKMS-backed MinIO objects remain unreadable by design
# (their envelopes are sealed by the KES service, not by a key RustFS can
# hold), and default RustFS builds do not include the read path — it is a
# special-purpose migration capability, not a default-build feature.
#
# Runner: GitHub-hosted `ubuntu-latest`. It reliably ships Docker + Python,
# unlike the self-hosted fleet, whose pods drift in Docker/pip availability
# (see the infra note in e2e-s3tests.yml). Nightly + manual only.
# DISABLED. This workflow is switched off in the repository's Actions settings
# (state: disabled_manually) and does not run on any trigger, including its cron
# and workflow_dispatch. That state lives in GitHub's UI and is invisible when
# reading this file, which has already misled at least one audit — hence this
# banner. Re-enabling is a UI action; anyone doing so should first check that the
# workflow still matches the current CI layout. See rustfs/backlog#1603.
#
# While disabled, this workflow is deliberately absent from
# .github/scheduled-validations.json — a disabled workflow can never satisfy the
# freshness check. Whoever re-enables it must re-add the entry in the same
# change so the freshness gate covers it again.
# Enablement: this workflow was long disabled in the repository's Actions
# settings (state: disabled_manually — a state that lives in GitHub's UI and is
# invisible in this file). The change that updated this banner also re-added
# the .github/scheduled-validations.json entry; both only make sense together
# with re-enabling the workflow in the Actions settings. If it is ever disabled
# again, remove the scheduled-validations entry in the same change — a disabled
# workflow can never satisfy the freshness check. See rustfs/backlog#1603.
#
name: minio-interop
@@ -143,4 +143,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+1 -1
View File
@@ -270,4 +270,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+194 -10
View File
@@ -19,30 +19,62 @@ on:
- cron: "7 0 * * *"
timezone: "Asia/Shanghai"
workflow_dispatch:
inputs:
branch:
description: 'Branch/ref to build and publish as the nightly (empty = scheduled source, see NIGHTLY_BUILD_REF)'
required: false
default: ''
permissions:
contents: read
# Scheduled builds follow the NIGHTLY_BRANCH repo variable so the channel can
# be pointed at e.g. `release` for the GA cycle and back to `main` afterwards
# without touching this file. Manual runs take the `branch` input, falling
# back to the branch the run was dispatched from.
concurrency:
group: nightly-gnu-build-main-${{ github.event_name }}
group: nightly-gnu-build-${{ github.event_name }}-${{ github.event_name == 'schedule' && (vars.NIGHTLY_BRANCH || 'main') || (inputs.branch || github.ref_name) }}
cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
NIGHTLY_BUILD_REF: ${{ github.event_name == 'schedule' && (vars.NIGHTLY_BRANCH || 'main') || (inputs.branch || github.ref_name) }}
jobs:
resolve-source:
name: Resolve nightly source
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
source_sha: ${{ steps.source.outputs.sha }}
source_ref: ${{ steps.source.outputs.ref }}
steps:
- name: Checkout selected source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ env.NIGHTLY_BUILD_REF }}
- name: Record immutable source
id: source
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "ref=${NIGHTLY_BUILD_REF}" >> "$GITHUB_OUTPUT"
build:
needs: resolve-source
name: Build x86_64 GNU
runs-on: sm-standard-4
timeout-minutes: 150
env:
NIGHTLY_BUILD_REF: ${{ needs.resolve-source.outputs.source_ref }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve-source.outputs.source_sha }}
- name: Setup Rust environment
uses: ./.github/actions/setup
@@ -152,13 +184,104 @@ jobs:
fakeroot dpkg-deb --build "${PKG_DIR}"
ls -lh "${DEB_FILE}"
echo "deb_date=${DEB_DATE}" >> "${GITHUB_OUTPUT}"
echo "deb_file=${DEB_FILE}" >> "${GITHUB_OUTPUT}"
# Same packaging scheme as .github/workflows/package.yml (fpm), but from
# the locally built nightly binary instead of a release artifact, with a
# date-based version that mirrors the DEB.
- name: Build RPM package
id: rpm
shell: bash
env:
DEB_DATE: ${{ steps.deb.outputs.deb_date }}
run: |
set -euo pipefail
if ! command -v fpm >/dev/null 2>&1; then
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} apt-get update -qq && ${SUDO} apt-get install -y -qq ruby ruby-dev build-essential rpm >/dev/null
${SUDO} gem install fpm --no-document >/dev/null
fi
RPM_FILE="rustfs-nightly-${DEB_DATE}.rpm"
RPM_VERSION="0"
RPM_RELEASE="0.nightly.${DEB_DATE//-/.}"
echo "Building RPM: ${RPM_FILE} (version ${RPM_VERSION}-${RPM_RELEASE})"
# fpm wants the config file to exist before packaging.
mkdir -p ./tmp-pkg/etc/default
cat > ./tmp-pkg/etc/default/rustfs << 'ENVEOF'
# RustFS Environment Configuration
# See https://rustfs.com/docs/ for more information
# RUSTFS_VOLUMES=""
# RUSTFS_ROOT_USER=""
# RUSTFS_ROOT_PASSWORD=""
ENVEOF
fpm -s dir -t rpm \
--name rustfs \
--version "$RPM_VERSION" \
--iteration "$RPM_RELEASE" \
--architecture x86_64 \
--package "$RPM_FILE" \
--depends "glibc >= 2.31" \
--maintainer "RustFS Team <support@rustfs.com>" \
--description "High-performance distributed object storage" \
--url "https://rustfs.com" \
--license "Apache-2.0" \
--after-install <(cat << 'POSTINST'
#!/bin/bash
set -e
if ! getent passwd rustfs > /dev/null 2>&1; then
useradd -r -s /bin/false -d /opt/rustfs rustfs
fi
mkdir -p /opt/rustfs /data/rustfs /var/log/rustfs
chown rustfs:rustfs /opt/rustfs /data/rustfs /var/log/rustfs
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
fi
POSTINST
) \
--before-remove <(cat << 'PRERM'
#!/bin/bash
set -e
if [ -d /run/systemd/system ] && systemctl is-active --quiet rustfs; then
systemctl stop rustfs
fi
PRERM
) \
--after-remove <(cat << 'POSTRM'
#!/bin/bash
set -e
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
fi
POSTRM
) \
--config-files /etc/default/rustfs \
"rustfs-nightly-${DEB_DATE}/usr/bin/rustfs=/usr/bin/rustfs" \
./tmp-pkg/etc/default/rustfs=/etc/default/rustfs \
deploy/build/rustfs.service=/lib/systemd/system/rustfs.service \
LICENSE=/usr/share/doc/rustfs/LICENSE \
README.md=/usr/share/doc/rustfs/README.md
[[ -f "$RPM_FILE" ]] || { echo "RPM build failed"; exit 1; }
rpm -qpl "$RPM_FILE" | grep -Fx '/usr/bin/rustfs' >/dev/null
stat --printf='%n %s bytes\n' "$RPM_FILE"
echo "rpm_file=$RPM_FILE" >> "$GITHUB_OUTPUT"
- name: Upload DEB artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ steps.deb.outputs.deb_file }}
path: ${{ steps.deb.outputs.deb_file }}
- name: Upload RPM artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ steps.rpm.outputs.rpm_file }}
path: ${{ steps.rpm.outputs.rpm_file }}
if-no-files-found: error
# Persist the nightly deb on Cloudflare R2 (same channel as package.yml)
@@ -166,8 +289,9 @@ jobs:
# e.g. https://dl.rustfs.com/artifacts/rustfs/packages/nightly/... .
# Skipped when the R2 secrets are not configured (artifact-only mode).
- name: Upload DEB to Cloudflare R2
if: env.R2_ACCESS_KEY_ID != ''
id: publish
env:
DEB_FILE: ${{ steps.deb.outputs.deb_file }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
@@ -182,28 +306,84 @@ jobs:
exit 0
fi
if ! command -v aws >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y -qq awscli
fi
export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
export AWS_DEFAULT_REGION="auto"
DEB_FILE="${{ steps.deb.outputs.deb_file }}"
# The candidate manifest must describe the tree that was actually
# built. With a ref override (NIGHTLY_BRANCH / dispatch input) that
# is not necessarily GITHUB_SHA, so always advertise HEAD.
SOURCE_SHA="$(git rev-parse HEAD)"
DEB_SHA256="$(sha256sum "${DEB_FILE}" | cut -d ' ' -f 1)"
CANDIDATE_KEY="artifacts/rustfs/packages/nightly/runs/${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}/${DEB_SHA256}/rustfs.deb"
CANDIDATE_URL="https://dl.rustfs.com/${CANDIDATE_KEY}"
# Old AWS CLI models lack conditional PutObject support. Never fall
# back to an overwriting upload for a candidate.
AWS_CLI=aws
if ! "${AWS_CLI}" s3api put-object --generate-cli-skeleton input | jq -e 'has("IfNoneMatch")' >/dev/null; then
sudo apt-get update
sudo apt-get install -y -qq python3-venv
AWS_CLI_DIR="$(mktemp -d "${RUNNER_TEMP}/nightly-awscli.XXXXXX")"
trap 'rm -rf "${AWS_CLI_DIR}"' EXIT
python3 -m venv "${AWS_CLI_DIR}"
"${AWS_CLI_DIR}/bin/python" -m pip install --disable-pip-version-check 'awscli==1.44.79'
AWS_CLI="${AWS_CLI_DIR}/bin/aws"
fi
"${AWS_CLI}" s3api put-object --generate-cli-skeleton input | jq -e 'has("IfNoneMatch")' >/dev/null
"${AWS_CLI}" --version
"${AWS_CLI}" s3api put-object --bucket "${R2_BUCKET}" --key "${CANDIDATE_KEY}" \
--body "${DEB_FILE}" --if-none-match '*' --endpoint-url "${R2_ENDPOINT}"
PUBLISHED_SHA256="$(curl -fsSL --retry 3 --connect-timeout 15 --max-time 300 "${CANDIDATE_URL}" | sha256sum | cut -d ' ' -f 1)"
if [[ "${PUBLISHED_SHA256}" != "${DEB_SHA256}" ]]; then
echo "Published candidate checksum does not match the built package" >&2
exit 1
fi
R2_PREFIX="s3://${R2_BUCKET}/artifacts/rustfs/packages/nightly/"
echo "📤 Uploading ${DEB_FILE} to ${R2_PREFIX}"
aws s3 cp "${DEB_FILE}" "${R2_PREFIX}" --endpoint-url "$R2_ENDPOINT" --only-show-errors
"${AWS_CLI}" s3 cp "${DEB_FILE}" "${R2_PREFIX}" --endpoint-url "$R2_ENDPOINT" --only-show-errors
# Stable "latest" alias so tests can fetch the newest nightly
# without knowing today's date.
echo "📤 Uploading latest alias"
aws s3 cp "${DEB_FILE}" "${R2_PREFIX}rustfs-nightly-latest.deb" \
"${AWS_CLI}" s3 cp "${DEB_FILE}" "${R2_PREFIX}rustfs-nightly-latest.deb" \
--endpoint-url "$R2_ENDPOINT" --only-show-errors
echo "✅ R2 upload complete"
CANDIDATE_FILE="${RUNNER_TEMP}/nightly-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
jq -n --arg source_sha "${SOURCE_SHA}" \
--arg workflow_sha "${GITHUB_SHA}" --arg source_ref "${NIGHTLY_BUILD_REF}" \
--argjson build_run_id "${GITHUB_RUN_ID}" --argjson build_run_attempt "${GITHUB_RUN_ATTEMPT}" \
--arg package_url "${CANDIDATE_URL}" --arg package_sha256 "${DEB_SHA256}" \
'{schema: 2, workflow_sha: $workflow_sha, source_ref: $source_ref, source_sha: $source_sha, build_run_id: $build_run_id, build_run_attempt: $build_run_attempt, package_url: $package_url, package_sha256: $package_sha256}' \
> "${CANDIDATE_FILE}"
echo "candidate_file=${CANDIDATE_FILE}" >> "${GITHUB_OUTPUT}"
- name: Upload nightly candidate manifest
if: ${{ steps.publish.outputs.candidate_file != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: nightly-candidate-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.publish.outputs.candidate_file }}
if-no-files-found: error
# Publish the deb/rpm pair to the auto-testing repo's `assets` branch so
# engineers can download and install the nightly directly. The branch is
# a single-commit orphan rewritten on every build, which keeps the repo
# small while the latest files stay reachable at stable raw URLs.
# Publish the deb/rpm pair as assets of the rolling `nightly` release on
# rustfs/auto-testing (see scripts/release/publish_nightly_assets.sh).
- name: Publish packages to auto-testing release assets
env:
ASSETS_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
DEB_FILE: ${{ steps.deb.outputs.deb_file }}
RPM_FILE: ${{ steps.rpm.outputs.rpm_file }}
DEB_DATE: ${{ steps.deb.outputs.deb_date }}
BUILD_REF: ${{ env.NIGHTLY_BUILD_REF }}
run: bash scripts/release/publish_nightly_assets.sh
# Live-Vault lane for the rustfs-kms suite (rustfs/backlog#1774).
#
# RUSTFS_KMS_VAULT_TOKEN is the single switch that adds the Vault KV2 and
@@ -220,6 +400,7 @@ jobs:
# self-hosted fleet is heterogeneous — a docker-dependent workflow has been
# burned by it before (see the banner in e2e-s3tests.yml, rustfs/backlog#1149).
kms-vault-lane:
needs: resolve-source
name: KMS live Vault lane
runs-on: ubuntu-latest
timeout-minutes: 90
@@ -241,6 +422,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve-source.outputs.source_sha }}
- name: Setup Rust environment
uses: ./.github/actions/setup
@@ -318,6 +500,7 @@ jobs:
# flake cannot mask the main lane's verdict, and vice versa. The script
# provisions and tears down its own Docker cluster.
kms-vault-ha-failover:
needs: resolve-source
name: KMS Vault HA failover lane
runs-on: ubuntu-latest
timeout-minutes: 60
@@ -329,6 +512,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve-source.outputs.source_sha }}
- name: Setup Rust environment
uses: ./.github/actions/setup
@@ -359,4 +543,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+2
View File
@@ -27,6 +27,7 @@ on:
paths:
- 'flake.nix'
- 'flake.lock'
- 'nix/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/nix.yml'
@@ -36,6 +37,7 @@ on:
paths:
- 'flake.nix'
- 'flake.lock'
- 'nix/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/nix.yml'
+1 -1
View File
@@ -107,4 +107,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
@@ -0,0 +1,315 @@
# Copyright 2024 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.
# On-demand migration provider interop (rustfs/backlog#2167, ODM-20).
#
# The in-process fake source that the merge-gate ODM suite runs against covers
# the protocol semantics, but real implementations differ in path-style vs
# virtual-host addressing, region handling, ETag shape, list pagination and
# rate limiting. This lane runs the same case bodies
# (crates/e2e_test/src/on_demand_migration/interop_test.rs) against real
# sources; the source is injected through RUSTFS_ODM_INTEROP_* environment
# variables, so nothing about the cases is duplicated per provider.
#
# Report-only and scheduled. It is never a required check and must not be
# promoted to one: it depends on third-party endpoints and on repository
# secrets that a fork does not have.
#
# Jobs:
# * minio-source runs the whole e2e-odm-interop profile — read-through,
# HEAD passthrough, merged list pagination and a backfill — against a
# pinned MinIO container. The backfill is sized at 5,000 objects here: the
# fake source retains at most 4,096 object versions and 4,096 journal
# entries, so the merge-gate backfill coverage cannot go past that, and a
# real source is where a production-shaped batch belongs.
# * cloud-source runs the three-case minimum (GET miss, HEAD miss, merged
# list pagination) against AWS S3, Cloudflare R2 and the GCS XML
# interoperability API. Each provider is skipped with a summary note when
# its ODM_INTEROP_* repository secrets are absent, which is the normal
# state on a fork and in any clone of this repository.
#
# Every job uploads one JSON report per provider naming the cases, their
# timings and the source request accounting.
name: on-demand-migration-interop
on:
workflow_dispatch:
schedule:
# Nightly at 05:23 UTC, offset from the other nightly lanes.
- cron: "23 5 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# The three cases a cloud provider is asked for. Named individually rather
# than by module so adding a fourth case does not silently start billing a
# cloud account for it.
CLOUD_CASE_FILTER: >-
package(e2e_test) & test(/^on_demand_migration::interop_test::(interop_get_miss_pulls_from_the_source_and_serves_locally|interop_head_miss_answers_from_the_source_without_persisting|interop_list_through_pages_the_source_namespace)$/)
CLOUD_CASE_COUNT: "3"
jobs:
minio-source:
name: MinIO source (read-through, list-through, backfill)
# Skip on forks: needs this repository's runners and is not a contributor
# gate.
if: github.repository == 'rustfs/rustfs'
runs-on: ubuntu-latest
timeout-minutes: 90
env:
NO_PROXY: 127.0.0.1,localhost
# Fixed credentials of the container this job starts and throws away;
# not a secret and deliberately not read from one, so the lane runs
# unattended in any clone that enables it.
MINIO_ROOT_USER: rustfsodminterop
MINIO_ROOT_PASSWORD: rustfsodminteropsecret
RUSTFS_ODM_INTEROP_PROVIDER: minio
RUSTFS_ODM_INTEROP_ENDPOINT: http://127.0.0.1:9100
RUSTFS_ODM_INTEROP_REGION: auto
RUSTFS_ODM_INTEROP_BUCKET: odm-interop-source
RUSTFS_ODM_INTEROP_PATH_STYLE: path
RUSTFS_ODM_INTEROP_ACCESS_KEY: rustfsodminterop
RUSTFS_ODM_INTEROP_SECRET_KEY: rustfsodminteropsecret
RUSTFS_ODM_INTEROP_BACKFILL_OBJECTS: "5000"
RUSTFS_ODM_INTEROP_REPORT_DIR: ${{ github.workspace }}/artifacts/odm-interop/minio/cases
NEXTEST_LISTING: ${{ github.workspace }}/artifacts/odm-interop/minio/selection.json
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
cache-shared-key: ci-odm-interop
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
install-build-packaging-tools: 'false'
- name: Start MinIO source
run: |
set -euo pipefail
mkdir -p artifacts/odm-interop/minio
docker run -d --name rustfs-odm-interop-minio \
-e "MINIO_ROOT_USER=${MINIO_ROOT_USER}" \
-e "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}" \
-p 9100:9000 \
minio/minio:RELEASE.2025-09-07T16-13-09Z server /data
for _ in $(seq 1 120); do
curl -fsS http://127.0.0.1:9100/minio/health/live >/dev/null 2>&1 && break
sleep 1
done
curl -fsS http://127.0.0.1:9100/minio/health/live
# The harness never creates a bucket, so that pointing it at a cloud
# account cannot create one there either. The source bucket for the
# container is created here instead.
- name: Create the MinIO source bucket
env:
AWS_ACCESS_KEY_ID: ${{ env.MINIO_ROOT_USER }}
AWS_SECRET_ACCESS_KEY: ${{ env.MINIO_ROOT_PASSWORD }}
AWS_DEFAULT_REGION: us-east-1
run: |
aws --endpoint-url "${RUSTFS_ODM_INTEROP_ENDPOINT}" \
s3api create-bucket --bucket "${RUSTFS_ODM_INTEROP_BUCKET}"
- name: Build the RustFS binary under test
run: python3 scripts/e2e_binary.py build --bins
# The lane selects tests by module, so a rename would quietly shrink it.
# The committed digest in .config/e2e-odm-interop-selection.txt fails
# closed on that.
- name: Verify interop lane membership
run: |
cargo nextest list --profile e2e-odm-interop -p e2e_test --message-format json > "${NEXTEST_LISTING}"
python3 ./scripts/check_test_wiring.py --check-profile e2e-odm-interop "${NEXTEST_LISTING}"
- name: Run the interop cases against MinIO
run: python3 scripts/e2e_binary.py run -- cargo nextest run --profile e2e-odm-interop -p e2e_test --no-tests=fail
- name: Build the MinIO interop report
if: always()
uses: ./.github/actions/odm-interop-report
with:
provider: minio
cases-dir: ${{ env.RUSTFS_ODM_INTEROP_REPORT_DIR }}
junit: target/nextest/e2e-odm-interop/junit.xml
output: artifacts/odm-interop/minio/report.json
- name: Collect MinIO logs
if: always()
run: |
docker logs --tail 500 rustfs-odm-interop-minio \
> artifacts/odm-interop/minio/minio.log 2>&1 || true
- name: Stop MinIO source
if: always()
run: docker rm -f rustfs-odm-interop-minio >/dev/null 2>&1 || true
- name: Upload the MinIO interop report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: odm-interop-minio-${{ github.run_number }}-${{ github.run_attempt }}
path: |
artifacts/odm-interop/minio
target/nextest/e2e-odm-interop/junit.xml
retention-days: 14
# Unlike a production migration source, which needs read access only, the
# credentials here also seed the objects each case reads back, so they need
# write and delete on the interop bucket. Every run seeds under
# `odm-interop/<case>/<uuid>/` and deletes what it seeded when the case
# passes; give the bucket an expiration lifecycle rule so the prefixes a
# failing case leaves behind cannot accumulate.
cloud-source:
name: ${{ matrix.provider }} source (three-case minimum)
if: github.repository == 'rustfs/rustfs'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- provider: aws
secret_prefix: AWS
path_style: virtual
- provider: r2
secret_prefix: R2
path_style: virtual
- provider: gcs
secret_prefix: GCS_HMAC
path_style: virtual
env:
RUSTFS_ODM_INTEROP_PROVIDER: ${{ matrix.provider }}
RUSTFS_ODM_INTEROP_PATH_STYLE: ${{ matrix.path_style }}
RUSTFS_ODM_INTEROP_ENDPOINT: ${{ secrets[format('ODM_INTEROP_{0}_ENDPOINT', matrix.secret_prefix)] }}
RUSTFS_ODM_INTEROP_REGION: ${{ secrets[format('ODM_INTEROP_{0}_REGION', matrix.secret_prefix)] }}
RUSTFS_ODM_INTEROP_BUCKET: ${{ secrets[format('ODM_INTEROP_{0}_BUCKET', matrix.secret_prefix)] }}
RUSTFS_ODM_INTEROP_ACCESS_KEY: ${{ secrets[format('ODM_INTEROP_{0}_ACCESS_KEY_ID', matrix.secret_prefix)] }}
RUSTFS_ODM_INTEROP_SECRET_KEY: ${{ secrets[format('ODM_INTEROP_{0}_SECRET_ACCESS_KEY', matrix.secret_prefix)] }}
RUSTFS_ODM_INTEROP_REPORT_DIR: ${{ github.workspace }}/artifacts/odm-interop/${{ matrix.provider }}/cases
NEXTEST_LISTING: ${{ github.workspace }}/artifacts/odm-interop/${{ matrix.provider }}/selection.json
steps:
# Absent secrets are the normal state, not a failure: the lane reports
# which providers it could reach and skips the rest. An empty value is
# what an unset repository secret expands to, so it is checked, not the
# secret's existence.
- name: Check for provider credentials
id: credentials
run: |
set -euo pipefail
if [ -z "${RUSTFS_ODM_INTEROP_ENDPOINT}" ] \
|| [ -z "${RUSTFS_ODM_INTEROP_REGION}" ] \
|| [ -z "${RUSTFS_ODM_INTEROP_BUCKET}" ] \
|| [ -z "${RUSTFS_ODM_INTEROP_ACCESS_KEY}" ] \
|| [ -z "${RUSTFS_ODM_INTEROP_SECRET_KEY}" ]; then
echo "present=false" >> "$GITHUB_OUTPUT"
{
echo "### On-demand migration interop: \`${{ matrix.provider }}\`"
echo
echo "Skipped: the \`ODM_INTEROP_${{ matrix.secret_prefix }}_*\` repository secrets"
echo "(\`_ENDPOINT\`, \`_REGION\`, \`_BUCKET\`, \`_ACCESS_KEY_ID\`, \`_SECRET_ACCESS_KEY\`)"
echo "are not configured, so no real \`${{ matrix.provider }}\` source was reached."
echo
} >> "$GITHUB_STEP_SUMMARY"
else
echo "present=true" >> "$GITHUB_OUTPUT"
fi
- name: Checkout repository
if: steps.credentials.outputs.present == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Setup Rust environment
if: steps.credentials.outputs.present == 'true'
uses: ./.github/actions/setup
with:
cache-shared-key: ci-odm-interop
cache-save-if: 'false'
install-build-packaging-tools: 'false'
- name: Build the RustFS binary under test
if: steps.credentials.outputs.present == 'true'
run: python3 scripts/e2e_binary.py build --bins
# A filterset that matches nothing is valid, so the count is asserted
# rather than inferred from a green run.
- name: Verify the three-case minimum still selects three cases
if: steps.credentials.outputs.present == 'true'
run: |
set -euo pipefail
mkdir -p "$(dirname "${NEXTEST_LISTING}")"
cargo nextest list --profile e2e-odm-interop -p e2e_test \
-E "${CLOUD_CASE_FILTER}" --message-format json > "${NEXTEST_LISTING}"
selected="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(sum(1 for suite in d.get("rust-suites", {}).values() for test in suite.get("testcases", {}).values() if test.get("filter-match", {}).get("status") == "matches"))' "${NEXTEST_LISTING}")"
echo "cloud interop cases selected: ${selected}"
if [ "${selected}" != "${CLOUD_CASE_COUNT}" ]; then
echo "::error::CLOUD_CASE_FILTER selected ${selected} cases, expected ${CLOUD_CASE_COUNT}; the interop cases were renamed or moved. Context: rustfs/backlog#2167."
exit 1
fi
- name: Run the three-case minimum
if: steps.credentials.outputs.present == 'true'
run: |
python3 scripts/e2e_binary.py run -- cargo nextest run --profile e2e-odm-interop -p e2e_test \
-E "${CLOUD_CASE_FILTER}" --no-tests=fail
- name: Build the ${{ matrix.provider }} interop report
if: always() && steps.credentials.outputs.present == 'true'
uses: ./.github/actions/odm-interop-report
with:
provider: ${{ matrix.provider }}
cases-dir: ${{ env.RUSTFS_ODM_INTEROP_REPORT_DIR }}
junit: target/nextest/e2e-odm-interop/junit.xml
output: artifacts/odm-interop/${{ matrix.provider }}/report.json
- name: Upload the ${{ matrix.provider }} interop report
if: always() && steps.credentials.outputs.present == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: odm-interop-${{ matrix.provider }}-${{ github.run_number }}-${{ github.run_attempt }}
path: |
artifacts/odm-interop/${{ matrix.provider }}
target/nextest/e2e-odm-interop/junit.xml
retention-days: 14
alert-on-failure:
name: Alert on scheduled failure
needs: [minio-source, cloud-source]
if: >-
always() && github.event_name == 'schedule' &&
(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled'))
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+170 -97
View File
@@ -21,10 +21,10 @@
# - workflow_run: automatically package after "Build and Release" completes
# for a release tag (the mac/windows/linux binaries are already uploaded
# to the GitHub release before packaging starts)
# - workflow_dispatch: manual fallback (backfill / re-run) with optional tag/run_id
# - workflow_dispatch: manual fallback with a release tag and/or exact build run ID
#
# Flow:
# 1. Resolve the triggering Build workflow run for the release tag
# 1. Resolve and validate the selected Build workflow run and source identity
# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu) from build artifacts
# 3. Build DEB packages for amd64 and arm64
# 4. Build RPM packages for x86_64 and aarch64
@@ -51,7 +51,7 @@ on:
required: false
type: string
build_run_id:
description: "Build workflow run ID (overrides tag lookup)"
description: "Build workflow run ID (when combined with tag, both must identify the same release commit)"
required: false
type: string
@@ -82,6 +82,9 @@ jobs:
version: ${{ steps.resolve.outputs.version }}
build_type: ${{ steps.resolve.outputs.build_type }}
build_run_id: ${{ steps.resolve.outputs.build_run_id }}
build_run_number: ${{ steps.resolve.outputs.build_run_number }}
head_sha: ${{ steps.resolve.outputs.head_sha }}
dev_sequence: ${{ steps.resolve.outputs.dev_sequence }}
tag: ${{ steps.resolve.outputs.tag }}
steps:
- name: Resolve build run
@@ -89,90 +92,129 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
REPOSITORY: ${{ github.repository }}
INPUT_TAG: ${{ github.event.inputs.tag }}
INPUT_RUN_ID: ${{ github.event.inputs.build_run_id }}
run: |
set -euo pipefail
# Determine tag
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
TAG="${HEAD_BRANCH}"
elif [[ -n "$INPUT_TAG" ]]; then
TAG="$INPUT_TAG"
fail() {
echo "❌ $1" >&2
exit 1
}
TAG=""
BUILD_RUN_ID=""
case "$EVENT_NAME" in
workflow_run)
TAG="$HEAD_BRANCH"
BUILD_RUN_ID="$WORKFLOW_RUN_ID"
;;
workflow_dispatch)
TAG="$INPUT_TAG"
BUILD_RUN_ID="$INPUT_RUN_ID"
;;
*) fail "unsupported event: $EVENT_NAME" ;;
esac
# Validate and classify tags before using them in API paths or logs.
semver_core='(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)'
prerelease_id='(alpha|beta|rc)\.(0|[1-9][0-9]*)'
if [[ -n "$TAG" ]]; then
if [[ "$TAG" =~ ^${semver_core}-${prerelease_id}-preview\.(0|[1-9][0-9]*)$ ]]; then
BUILD_TYPE=preview
elif [[ "$TAG" =~ ^${semver_core}-${prerelease_id}$ ]]; then
BUILD_TYPE=prerelease
elif [[ "$TAG" =~ ^${semver_core}$ ]]; then
BUILD_TYPE=release
else
fail "tag is not a supported strict package version"
fi
else
TAG=""
BUILD_TYPE=development
fi
echo "Tag: ${TAG:-<none>}"
# Determine build run ID
BUILD_RUN_ID=""
if [[ -n "$INPUT_RUN_ID" ]]; then
# Explicit run ID takes priority
BUILD_RUN_ID="$INPUT_RUN_ID"
echo "Using explicit build run ID: $BUILD_RUN_ID"
elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then
# Use the Build and Release run that triggered this workflow
BUILD_RUN_ID="${WORKFLOW_RUN_ID}"
echo "Using triggering workflow run: $BUILD_RUN_ID"
if [[ -n "$BUILD_RUN_ID" ]]; then
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] || fail "build run ID must be a positive decimal integer"
echo "Using selected build run: $BUILD_RUN_ID"
elif [[ -n "$TAG" ]]; then
# Find the build run that produced this tag
echo "Looking for build run for tag: $TAG"
BUILD_RUN_ID=$(gh api \
"repos/${{ github.repository }}/actions/workflows/build.yml/runs?branch=${TAG}&status=success&per_page=1" \
--jq '.workflow_runs[0].id' 2>/dev/null || echo "")
BUILD_RUN_ID=$(gh api --method GET \
"repos/${REPOSITORY}/actions/workflows/build.yml/runs" \
-f branch="$TAG" -f status=success -F per_page=1 \
--jq '.workflow_runs[0].id // empty' 2>/dev/null || true)
if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then
# Tag might not be a branch; try event=push with head_branch matching
BUILD_RUN_ID=$(gh api \
"repos/${{ github.repository }}/actions/workflows/build.yml/runs?event=push&status=success&per_page=100" \
--jq ".workflow_runs[] | select(.head_branch == \"$TAG\") | .id" 2>/dev/null | head -1 || echo "")
fi
if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then
echo "❌ No successful build run found for tag: $TAG"
exit 1
if [[ -z "$BUILD_RUN_ID" ]]; then
BUILD_RUN_ID=$(gh api --method GET \
"repos/${REPOSITORY}/actions/workflows/build.yml/runs" \
-f event=push -f status=success -F per_page=100 2>/dev/null |
jq -r --arg tag "$TAG" \
'[.workflow_runs[] | select(.head_branch == $tag)][0].id // empty' || true)
fi
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] || fail "no successful build run found for tag"
echo "Found build run: $BUILD_RUN_ID"
else
# No tag — latest successful main build
echo "No tag specified, looking for latest main build"
BUILD_RUN_ID=$(gh api \
"repos/${{ github.repository }}/actions/workflows/build.yml/runs?branch=main&status=success&per_page=1" \
--jq '.workflow_runs[0].id' 2>/dev/null || echo "")
if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then
echo "❌ No successful main build found"
exit 1
fi
BUILD_RUN_ID=$(gh api --method GET \
"repos/${REPOSITORY}/actions/workflows/build.yml/runs" \
-f branch=main -f status=success -F per_page=1 \
--jq '.workflow_runs[0].id // empty' 2>/dev/null || true)
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] || fail "no successful main build found"
echo "Latest main build: $BUILD_RUN_ID"
fi
# Determine version and build type
# Fetch once and use the same immutable run metadata for identity,
# ordering, workflow provenance, and release-channel validation.
RUN_JSON=$(gh api "repos/${REPOSITORY}/actions/runs/${BUILD_RUN_ID}") ||
fail "cannot read selected build run"
RUN_ID=$(jq -r '.id // empty' <<<"$RUN_JSON")
RUN_NUMBER=$(jq -r '.run_number // empty' <<<"$RUN_JSON")
RUN_STATUS=$(jq -r '.status // empty' <<<"$RUN_JSON")
RUN_CONCLUSION=$(jq -r '.conclusion // empty' <<<"$RUN_JSON")
RUN_PATH=$(jq -r '.path // empty' <<<"$RUN_JSON")
HEAD_SHA=$(jq -r '.head_sha // empty' <<<"$RUN_JSON")
RUN_HEAD_BRANCH=$(jq -r '.head_branch // empty' <<<"$RUN_JSON")
[[ "$RUN_ID" == "$BUILD_RUN_ID" ]] || fail "run metadata ID mismatch"
[[ "$RUN_NUMBER" =~ ^[1-9][0-9]*$ ]] || fail "build run number must be a positive decimal integer"
[[ "$RUN_STATUS" == completed && "$RUN_CONCLUSION" == success ]] || fail "selected build run is not successful"
[[ "$RUN_PATH" == .github/workflows/build.yml ]] || fail "selected run is not Build and Release"
[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || fail "selected build run has an invalid head SHA"
[[ "$RUN_HEAD_BRANCH" != *$'\n'* && -n "$RUN_HEAD_BRANCH" ]] || fail "selected build run has an invalid head branch"
if [[ -n "$TAG" ]]; then
[[ "$RUN_HEAD_BRANCH" == "$TAG" ]] || fail "tag and build run head branch do not match"
TAG_REF_JSON=$(gh api "repos/${REPOSITORY}/git/ref/tags/${TAG}") ||
fail "cannot resolve release tag ref"
TAG_OBJECT_TYPE=$(jq -r '.object.type // empty' <<<"$TAG_REF_JSON")
TAG_OBJECT_SHA=$(jq -r '.object.sha // empty' <<<"$TAG_REF_JSON")
depth=0
while [[ "$TAG_OBJECT_TYPE" == tag && $depth -lt 5 ]]; do
TAG_OBJECT_JSON=$(gh api "repos/${REPOSITORY}/git/tags/${TAG_OBJECT_SHA}") ||
fail "cannot peel annotated release tag"
TAG_OBJECT_TYPE=$(jq -r '.object.type // empty' <<<"$TAG_OBJECT_JSON")
TAG_OBJECT_SHA=$(jq -r '.object.sha // empty' <<<"$TAG_OBJECT_JSON")
depth=$((depth + 1))
done
[[ "$TAG_OBJECT_TYPE" == commit && "$TAG_OBJECT_SHA" =~ ^[0-9a-f]{40}$ ]] ||
fail "release tag does not resolve to a commit"
[[ "$TAG_OBJECT_SHA" == "$HEAD_SHA" ]] || fail "release tag commit and build run head SHA do not match"
VERSION="$TAG"
if [[ "$TAG" == *"-preview"* ]]; then
BUILD_TYPE="preview"
elif [[ "$TAG" == *"alpha"* || "$TAG" == *"beta"* || "$TAG" == *"rc"* ]]; then
BUILD_TYPE="prerelease"
else
BUILD_TYPE="release"
fi
DEV_SEQUENCE=""
else
SHORT_SHA=$(gh api "repos/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}" \
--jq '.head_sha' 2>/dev/null | head -c 7)
VERSION="dev-${SHORT_SHA}"
BUILD_TYPE="development"
VERSION="dev-${HEAD_SHA}"
DEV_SEQUENCE="$RUN_NUMBER"
fi
{
echo "version=$VERSION"
echo "build_type=$BUILD_TYPE"
echo "build_run_id=$BUILD_RUN_ID"
echo "build_run_number=$RUN_NUMBER"
echo "head_sha=$HEAD_SHA"
echo "dev_sequence=$DEV_SEQUENCE"
echo "tag=${TAG}"
} >> "$GITHUB_OUTPUT"
@@ -180,6 +222,7 @@ jobs:
echo " Version: $VERSION"
echo " Build type: $BUILD_TYPE"
echo " Build run ID: $BUILD_RUN_ID"
echo " Build run number: $RUN_NUMBER"
# Build DEB and RPM packages for each architecture
package:
@@ -206,6 +249,22 @@ jobs:
with:
persist-credentials: false
- name: Normalize package metadata
id: versions
shell: bash
env:
BUILD_TYPE: ${{ needs.resolve.outputs.build_type }}
SOURCE_VERSION: ${{ needs.resolve.outputs.version }}
DEV_SEQUENCE: ${{ needs.resolve.outputs.dev_sequence }}
DEB_ARCH: ${{ matrix.deb_arch }}
RPM_ARCH: ${{ matrix.rpm_arch }}
run: |
set -euo pipefail
normalized=$(./scripts/release/package_versions.sh \
"$BUILD_TYPE" "$SOURCE_VERSION" "$DEV_SEQUENCE" "$DEB_ARCH" "$RPM_ARCH")
printf '%s\n' "$normalized" >> "$GITHUB_OUTPUT"
- name: Download binary artifact from build run
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
@@ -224,7 +283,7 @@ jobs:
ZIP_FILE=$(find ./binary-artifact -name "*.zip" -type f | head -1)
if [[ -z "$ZIP_FILE" ]]; then
echo "❌ No binary artifact found"
ls -la ./binary-artifact/ || true
find ./binary-artifact -mindepth 1 -maxdepth 1 -print 2>/dev/null || true
exit 1
fi
@@ -239,24 +298,22 @@ jobs:
fi
chmod +x ./bin/rustfs
ls -lh ./bin/rustfs
stat --printf='%n %s bytes\n' ./bin/rustfs
echo "✅ Binary extracted"
- name: Build DEB package
id: deb
shell: bash
env:
DEB_VERSION: ${{ steps.versions.outputs.deb_version }}
DEB_ARCH: ${{ matrix.deb_arch }}
DEB_FILE: ${{ steps.versions.outputs.deb_file }}
run: |
set -euo pipefail
VERSION="${{ needs.resolve.outputs.version }}"
DEB_ARCH="${{ matrix.deb_arch }}"
# DEB version: replace - with ~ (1.0.0-beta.12 -> 1.0.0~beta.12)
# Use a variable for ~ to prevent tilde expansion by bash
TILDE='~'
DEB_VERSION="${VERSION/-/$TILDE}"
PKG_DIR="rustfs_${DEB_VERSION}_${DEB_ARCH}"
PKG_DIR="${DEB_FILE%.deb}"
echo "Building DEB: ${PKG_DIR}.deb"
echo "Building DEB: ${DEB_FILE}"
mkdir -p "${PKG_DIR}/DEBIAN"
mkdir -p "${PKG_DIR}/usr/bin"
@@ -333,26 +390,32 @@ jobs:
cp LICENSE "${PKG_DIR}/usr/share/doc/rustfs/"
cp README.md "${PKG_DIR}/usr/share/doc/rustfs/"
fakeroot dpkg-deb --build "${PKG_DIR}"
fakeroot dpkg-deb --build "${PKG_DIR}" "$DEB_FILE"
DEB_FILE="${PKG_DIR}.deb"
ls -lh "$DEB_FILE"
[[ $(dpkg-deb -f "$DEB_FILE" Package) == rustfs ]]
[[ $(dpkg-deb -f "$DEB_FILE" Version) == "$DEB_VERSION" ]]
[[ $(dpkg-deb -f "$DEB_FILE" Architecture) == "$DEB_ARCH" ]]
dpkg-deb --fsys-tarfile "$DEB_FILE" | tar -tf - | grep -Fx './usr/bin/rustfs' >/dev/null
stat --printf='%n %s bytes\n' "$DEB_FILE"
echo "deb_file=$DEB_FILE" >> "$GITHUB_OUTPUT"
echo "✅ DEB built: $DEB_FILE"
- name: Build RPM package
id: rpm
shell: bash
env:
RPM_VERSION: ${{ steps.versions.outputs.rpm_version }}
RPM_RELEASE: ${{ steps.versions.outputs.rpm_release }}
RPM_ARCH: ${{ matrix.rpm_arch }}
RPM_FILE: ${{ steps.versions.outputs.rpm_file }}
run: |
set -euo pipefail
VERSION="${{ needs.resolve.outputs.version }}"
RPM_ARCH="${{ matrix.rpm_arch }}"
echo "Building RPM for ${RPM_ARCH}"
sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential
sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential rpm
sudo gem install fpm
./scripts/test_package_versions.sh --require-package-managers
# Create config file for fpm (DEB build creates it in its package dir structure,
# but fpm needs the file to exist before packaging)
@@ -367,8 +430,10 @@ jobs:
fpm -s dir -t rpm \
--name rustfs \
--version "$VERSION" \
--version "$RPM_VERSION" \
--iteration "$RPM_RELEASE" \
--architecture "$RPM_ARCH" \
--package "$RPM_FILE" \
--depends "glibc >= 2.31" \
--maintainer "RustFS Team <support@rustfs.com>" \
--description "High-performance distributed object storage" \
@@ -410,13 +475,16 @@ jobs:
LICENSE=/usr/share/doc/rustfs/LICENSE \
README.md=/usr/share/doc/rustfs/README.md
RPM_FILE=$(ls -1 rustfs-*.rpm 2>/dev/null | head -1)
if [[ -z "$RPM_FILE" ]]; then
if [[ ! -f "$RPM_FILE" ]]; then
echo "❌ RPM build failed"
exit 1
fi
ls -lh "$RPM_FILE"
RPM_METADATA=$(rpm -qp --qf '%{NAME}\n%{VERSION}\n%{RELEASE}\n%{ARCH}\n' "$RPM_FILE")
EXPECTED_METADATA=$(printf 'rustfs\n%s\n%s\n%s' "$RPM_VERSION" "$RPM_RELEASE" "$RPM_ARCH")
[[ "$RPM_METADATA" == "$EXPECTED_METADATA" ]]
rpm -qpl "$RPM_FILE" | grep -Fx '/usr/bin/rustfs' >/dev/null
stat --printf='%n %s bytes\n' "$RPM_FILE"
echo "rpm_file=$RPM_FILE" >> "$GITHUB_OUTPUT"
echo "✅ RPM built: $RPM_FILE"
@@ -437,6 +505,9 @@ jobs:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
AWS_EC2_METADATA_DISABLED: true
BUILD_TYPE: ${{ needs.resolve.outputs.build_type }}
DEB_FILE: ${{ steps.deb.outputs.deb_file }}
RPM_FILE: ${{ steps.rpm.outputs.rpm_file }}
shell: bash
run: |
set -euo pipefail
@@ -454,7 +525,6 @@ jobs:
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
export AWS_DEFAULT_REGION="auto"
BUILD_TYPE="${{ needs.resolve.outputs.build_type }}"
if [[ "$BUILD_TYPE" == "development" ]]; then
R2_PREFIX="artifacts/rustfs/packages/dev"
else
@@ -464,9 +534,6 @@ jobs:
echo "📤 Uploading to $R2_PATH"
DEB_FILE="${{ steps.deb.outputs.deb_file }}"
RPM_FILE="${{ steps.rpm.outputs.rpm_file }}"
for f in "$DEB_FILE" "$RPM_FILE"; do
if [[ -n "$f" && -f "$f" ]]; then
echo "Uploading: $f"
@@ -492,14 +559,13 @@ jobs:
if: needs.resolve.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.resolve.outputs.tag }}
DEB_FILE: ${{ steps.deb.outputs.deb_file }}
RPM_FILE: ${{ steps.rpm.outputs.rpm_file }}
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
DEB_FILE="${{ steps.deb.outputs.deb_file }}"
RPM_FILE="${{ steps.rpm.outputs.rpm_file }}"
# Upload the packages, then refresh the release checksums so the new
# assets are covered, matching the binary release flow.
for f in "$DEB_FILE" "$RPM_FILE"; do
@@ -551,12 +617,19 @@ jobs:
steps:
- name: Print summary
shell: bash
env:
SUMMARY_VERSION: ${{ needs.resolve.outputs.version }}
SUMMARY_BUILD_TYPE: ${{ needs.resolve.outputs.build_type }}
SUMMARY_BUILD_RUN_ID: ${{ needs.resolve.outputs.build_run_id }}
SUMMARY_PACKAGE_STATUS: ${{ needs.package.result }}
run: |
echo "## 📦 Package Summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Item | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|------|-------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Version | \`${{ needs.resolve.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Build Type | ${{ needs.resolve.outputs.build_type }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Build Run | #${{ needs.resolve.outputs.build_run_id }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Package Status | ${{ needs.package.result }} |" >> "$GITHUB_STEP_SUMMARY"
{
echo "## 📦 Package Summary"
echo ""
echo "| Item | Value |"
echo "|------|-------|"
echo "| Version | \`${SUMMARY_VERSION}\` |"
echo "| Build Type | ${SUMMARY_BUILD_TYPE} |"
echo "| Build Run | #${SUMMARY_BUILD_RUN_ID} |"
echo "| Package Status | ${SUMMARY_PACKAGE_STATUS} |"
} >> "$GITHUB_STEP_SUMMARY"
+1 -1
View File
@@ -378,4 +378,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
+1 -1
View File
@@ -78,4 +78,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}
@@ -0,0 +1,365 @@
# RustFS Fault-Tolerance (degradation) Test
#
# Scenario suite for the 2026-09 degradation report: verifies read/write
# behavior under drive and node loss against the erasure-coding contract and
# snapshots health-endpoint responses at every tier.
#
# A single-node 4 drives (SNMD): hide 1/2/3 drives, restore
# B multi-node 4x1 (one drive per node): stop 1/2/3 nodes, restore
# C multi-node 4x4 (16 drives, EC:4): stop 1 node (read-quorum boundary),
# stop 2 nodes, restore
# C2 multi-node 4x4 with EC:8: 2 nodes down puts 8 drives online -- reads
# satisfy the EC read quorum while the lock majority is broken (the
# reported divergence window: reads 503 with lock_quorum_unavailable)
#
# Expectations come from product source (default_parity_count, erasure set
# sizing). By default a "reads refused although the read quorum is met"
# observation is reported as known-divergence without failing the suite; the
# strict input turns those into failures once the product behavior changes.
name: RustFS Fault-Tolerance Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
package_url:
description: 'Direct .deb URL. Required unless the nightly default is wanted.'
required: false
type: string
strict:
description: 'Fail the suite when reads are refused despite a met read quorum'
type: boolean
default: false
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
default: true
cleanup_after:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
repository_dispatch:
# Chain handoff: dispatched when the replication suite finishes, ahead of
# the performance suite.
types: [rustfs-chain-fault-tolerance]
permissions:
contents: read
# The suite stops services and hides drive dirs on the shared fleet; only one
# functional suite may touch the environment at a time.
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
jobs:
fault-tolerance-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-fault-tolerance-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch" "${FUNCTIONAL_ARTIFACTS_DIR}/evidence"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'EVIDENCE_DIR=%s/evidence\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
# Pin the private suite checkout when the chain supplies a candidate.
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
timeout-minutes: 5
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
aws --version
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
if: ${{ inputs.cleanup_before != 'false' }}
run: |
./auto-testing/rustfs-fault-tolerance-test.sh --cleanup -y --log-file "${LOG_FILE}"
- name: Stage verified candidate package
id: chain_package
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/prepare_functional_package.py prepare
- name: Run fault-tolerance scenarios (A, B, C, C2)
timeout-minutes: 45
id: test
# Standalone case failures are reported by the backlog manager.
# Chain evidence separately requires complete passing cases.
continue-on-error: true
run: |
ARGS=(--all -y --package-url "${{ steps.chain_package.outputs.package_url || inputs.package_url || env.RUSTFS_NIGHTLY_PACKAGE_URL }}" --log-file "${LOG_FILE}")
if [ "${{ inputs.strict }}" = "true" ]; then
ARGS+=(--strict)
fi
./auto-testing/rustfs-fault-tolerance-test.sh "${ARGS[@]}"
- name: Generate report
timeout-minutes: 2
id: chain_report
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
{
echo "# RustFS fault-tolerance test report"
echo ""
echo "- Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Package: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo "- Strict mode: ${{ inputs.strict || 'false' }}"
echo ""
echo "## Per-probe results"
echo ""
echo '```'
grep -E '^FT-(CASE|SUMMARY|REPORT)' "${LOG_FILE}" || echo "(no FT-CASE lines found)"
echo '```'
echo ""
echo "## Health snapshots"
echo ""
for f in "${FUNCTIONAL_ARTIFACTS_DIR}"/evidence/*.code; do
[ -e "${f}" ] || continue
printf '%s -> %s\n' "$(basename "${f}" .code)" "$(cat "${f}")"
done
} > "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Product gate: failing FT cases keep the run green — the report above and
# the backlog issue manager carry the signal. Only harness/environment
# breakdowns (no case verdicts at all) turn the workflow red.
FT_CASES="$(grep -cE '^FT-CASE:' "${LOG_FILE}" 2>/dev/null || true)"
FT_FAILED="$(grep -cE '^FT-CASE: .* verdict=UNEXPECTED' "${LOG_FILE}" 2>/dev/null || true)"
FT_PASSED=$(( ${FT_CASES:-0} - ${FT_FAILED:-0} ))
HARNESS_OK=0
if [ "${{ steps.test.outcome }}" = "success" ]; then
HARNESS_OK=1
elif [ "${{ steps.test.outcome }}" = "failure" ] && [ "${FT_CASES}" -gt 0 ] && [ "${FT_PASSED}" -ge 1 ]; then
HARNESS_OK=1
fi
[ "${HARNESS_OK}" = "1" ]
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Signal-based lifecycle: dedups against open issues by label
# (fault-tolerance + FT case), labels new issues (functional-test,
# category, case IDs, env), and closes fixed issues after a fully
# green run. Never acts on cancelled runs. Logic lives in
# auto-testing/scripts/issue_manager.py, which parses the
# FT-CASE verdict lines from the suite log.
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'fault-tolerance'
SUITE_LABEL: 'Fault-Tolerance'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
REPORT_FILE: ${{ env.REPORT_FILE }}
SUITE: fault-tolerance
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
echo "dashboard updated: ${REPORT_PATH}"
- name: Upload test logs & evidence
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/evidence/
if-no-files-found: warn
- name: Cleanup environment (after)
timeout-minutes: 5
if: ${{ always() && inputs.cleanup_after != 'false' }}
run: |
./auto-testing/rustfs-fault-tolerance-test.sh --cleanup -y --log-file "${LOG_FILE}" || true
- name: "Continue functional chain (next: Performance)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff retries, then files an alert issue in rustfs/backlog.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-performance' \
-F 'client_payload[from_suite]=fault-tolerance'; then
echo "dispatched next suite Performance (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Performance after 3 attempts" >&2
TITLE="[functional][chain] stalled after fault-tolerance (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **fault-tolerance** to **Performance** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-performance'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-performance'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS fault-tolerance test failed"
echo "Package source: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo "See the uploaded log artifact and FT-CASE lines for details."
- name: Remove verified candidate package
if: ${{ always() && inputs.chain_manifest != '' && steps.chain.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/prepare_functional_package.py cleanup
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite fault-tolerance
--report "${FUNCTIONAL_ARTIFACTS_DIR}/suite.log"
--output "${RUNNER_TEMP}/chain-fault-tolerance-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/fault-tolerance.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}/fault-tolerance.json
if-no-files-found: error
@@ -0,0 +1,235 @@
# Copyright 2024 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.
# Reusable workflows run from this driver's commit in one Actions run. Each
# suite still runs after an earlier suite fails; the final job requires all twelve.
name: RustFS Functional Chain
on:
workflow_dispatch:
inputs:
build_run_id:
description: Successful nightly build run on main
type: string
required: true
build_run_attempt:
description: Exact successful build attempt
type: string
required: true
workflow_run:
workflows: ["Nightly GNU Build"]
types: [completed]
permissions:
contents: read
actions: read
concurrency:
group: rustfs-functional-chain-runs
cancel-in-progress: false
jobs:
prepare:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'schedule' }}
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
manifest: ${{ steps.candidate.outputs.manifest }}
performance_ready: ${{ steps.perf_probe.outputs.performance_ready }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Resolve published candidate
id: candidate
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
BUILD_RUN_ATTEMPT: ${{ inputs.build_run_attempt }}
CHAIN_OUTPUT: ${{ runner.temp }}/chain-candidate.json
run: python3 scripts/resolve_functional_candidate.py
- name: Retain candidate identity
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-candidate-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-candidate.json
if-no-files-found: error
- name: Check shared functional fleet runner before scheduling suites
# The eleven functional suites run on the smoke-testing fleet; a hard
# requirement. The performance lane runs on pf-testing, which is a
# separate fleet - probe it without failing the whole chain so an
# offline performance runner skips only the performance lane.
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: python3 scripts/check_functional_runners.py smoke-testing
- name: Probe performance fleet runner
id: perf_probe
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
if python3 scripts/check_functional_runners.py pf-testing; then
echo "performance_ready=online" >> "$GITHUB_OUTPUT"
else
echo "performance_ready=offline" >> "$GITHUB_OUTPUT"
fi
upgrade:
needs: [prepare]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-upgrade-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
s3:
needs: [prepare, upgrade]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-s3-compat-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
kms:
needs: [prepare, s3]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-kms-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
tier:
needs: [prepare, kms]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-tier-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
storage:
needs: [prepare, tier]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-storage-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
heal:
needs: [prepare, storage]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-heal-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
pool:
needs: [prepare, heal]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-pool-expand-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
security:
needs: [prepare, pool]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-security-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
replication:
needs: [prepare, security]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-replication-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
fault-tolerance:
needs: [prepare, replication]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-fault-tolerance-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
table:
needs: [prepare, fault-tolerance]
if: ${{ always() && needs.prepare.result == 'success' }}
uses: ./.github/workflows/rustfs-table-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
performance:
needs: [prepare, table]
if: ${{ always() && needs.prepare.result == 'success' && needs.prepare.outputs.performance_ready == 'online' }}
uses: ./.github/workflows/rustfs-performance-test.yml
with:
chain_manifest: ${{ needs.prepare.outputs.manifest }}
secrets: inherit
complete-chain:
needs: [prepare, upgrade, s3, kms, tier, storage, heal, pool, security, replication, fault-tolerance, table, performance]
if: ${{ always() && needs.prepare.result != 'skipped' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Download suite evidence
id: download
continue-on-error: true
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: functional-chain-*-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-evidence
merge-multiple: true
- name: Summarize every suite including failures and missing evidence
if: always()
env:
CHAIN_MANIFEST: ${{ needs.prepare.outputs.manifest }}
CHAIN_NEEDS: ${{ toJSON(needs) }}
run: >-
python3 scripts/functional_chain_evidence.py summarize
--directory "${RUNNER_TEMP}/chain-evidence"
--output "${RUNNER_TEMP}/chain-report.json"
- name: Retain chain report regardless of test verdict
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-report-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/chain-report.json
${{ runner.temp }}/chain-report.md
if-no-files-found: error
- name: Verify every required suite
if: always()
env:
CHAIN_MANIFEST: ${{ needs.prepare.outputs.manifest }}
CHAIN_NEEDS: ${{ toJSON(needs) }}
run: >-
python3 scripts/functional_chain_evidence.py aggregate
--allow-skipped "${{ needs.prepare.outputs.performance_ready == 'online' && '' || 'performance' }}"
--directory "${RUNNER_TEMP}/chain-evidence"
--output "${RUNNER_TEMP}/chain-complete.json"
- name: Upload complete-chain evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-complete-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-complete.json
if-no-files-found: error
+372 -24
View File
@@ -1,6 +1,12 @@
name: RustFS Heal Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
package_url:
@@ -15,10 +21,6 @@ on:
description: 'Stop warp when surviving nodes reach N GiB'
required: false
default: '40'
heal_target_gb:
description: 'Outage node must reach N GiB after heal to pass'
required: false
default: '40'
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
@@ -27,6 +29,11 @@ on:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
repository_dispatch:
# Chain handoff: dispatched when the storage suite finishes. Heal runs
# exactly once per chain; the pool expansion workflow no longer embeds
# its own heal pass.
types: [rustfs-chain-heal]
permissions:
contents: read
@@ -34,7 +41,7 @@ permissions:
# Only one test at a time: both this and the pool-expansion workflow mutate
# the same test environment, so they share one concurrency group.
concurrency:
group: rustfs-pool-expansion-test
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
@@ -47,18 +54,53 @@ env:
RUSTFS_API_ENDPOINT: ${{ secrets.RUSTFS_API_ENDPOINT || vars.RUSTFS_API_ENDPOINT || vars.RUSTFS_RC_ENDPOINT }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
jobs:
heal-test:
runs-on: smoke-testing
timeout-minutes: 480
timeout-minutes: 60
# Standalone manual run, or one link of the nightly functional chain
# (storage -> heal -> pool). Pool expansion no longer re-runs heal.
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout
- name: Checkout chain tooling
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-heal-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'RUSTFS_WARP_LOG_FILE=%s/warp.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
@@ -67,13 +109,28 @@ jobs:
warp --version || true
df -h /data | tail -1
- name: Reset test environment (before)
- name: Cleanup environment (before)
timeout-minutes: 5
if: ${{ inputs.cleanup_before != 'false' }}
run: |
chmod +x scripts/test/rustfs_heal_test.sh
./scripts/test/rustfs_heal_test.sh --reset -y
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: Install RustFS package & start cluster
timeout-minutes: 5
run: |
ARGS=(--steps "1,2" -y --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
if [ -n "${{ inputs.package_url }}" ]; then
@@ -81,9 +138,10 @@ jobs:
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_heal_test.sh "${ARGS[@]}"
./auto-testing/rustfs_heal_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
- name: Preflight checks
timeout-minutes: 5
run: |
ARGS=(--preflight --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
if [ -n "${{ inputs.package_url }}" ]; then
@@ -91,32 +149,301 @@ jobs:
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_heal_test.sh "${ARGS[@]}"
./auto-testing/rustfs_heal_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
- name: Run heal test (write -> outage -> heal -> verify)
timeout-minutes: 45
id: test
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
./scripts/test/rustfs_heal_test.sh \
./auto-testing/rustfs_heal_test.sh \
--steps "3,4,5,6,7" -y \
--endpoint "${{ env.RUSTFS_API_ENDPOINT }}" \
--stop-node-gb "${{ inputs.stop_node_gb }}" \
--warp-stop-gb "${{ inputs.warp_stop_gb }}" \
--heal-target-gb "${{ inputs.heal_target_gb }}" \
--log-file /tmp/rustfs-heal-test.log
--stop-node-gb "${{ inputs.stop_node_gb || '15' }}" \
--warp-stop-gb "${{ inputs.warp_stop_gb || '40' }}" \
--log-file "${LOG_FILE}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
STEPS_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/steps.md"
CASE_RESULT=success
python3 - "${LOG_FILE}" "${STEPS_TABLE}" <<'PY' || CASE_RESULT=failure
import re
import sys
log_file, out_file = sys.argv[1], sys.argv[2]
ansi = re.compile(r'\x1b\[[0-9;]*m')
step_re = re.compile(r'^\[HEAL-STEP\]\s+(\d+)\s+(.+?)\s+(PASS|FAIL|SKIP)\s*$')
ver_re = re.compile(r'^\[HEAL-VERSION\]\s+(\S+)(?:\s+\(node\s+(\S+)\))?\s*$')
result_re = re.compile(r'^\[HEAL-RESULT\]\s+(PASS|FAIL)\s+(.*)$')
steps = {}
order = []
status_rank = {'SKIP': 0, 'PASS': 1, 'FAIL': 2}
version = None
version_node = None
verdict = None
verdict_detail = ''
try:
with open(log_file, 'r', encoding='utf-8', errors='replace') as fh:
for raw in fh:
line = ansi.sub('', raw).strip()
m = step_re.match(line)
if m:
n, desc, status = m.group(1), m.group(2), m.group(3)
if n not in steps:
order.append(n)
if n not in steps or status_rank[status] > status_rank[steps[n][1]]:
steps[n] = (desc, status)
continue
m = ver_re.match(line)
if m:
version, version_node = m.group(1), m.group(2)
continue
m = result_re.match(line)
if m and verdict != 'FAIL':
verdict, verdict_detail = m.group(1), m.group(2)
except FileNotFoundError:
pass
with open(out_file, 'w', encoding='utf-8') as out:
out.write('## Step Results\n\n')
if version:
node_note = f' (captured via `rustfs --version` on {version_node})' if version_node else ''
out.write(f'- Version under test: **{version}**{node_note}\n')
if verdict:
out.write(f'- Overall result: **{verdict}** — {verdict_detail}\n')
out.write('\n')
out.write('| Step | Description | Result |\n')
out.write('| --- | --- | --- |\n')
for n in sorted(order, key=int):
desc, status = steps[n]
out.write(f'| {n} | {desc} | {status} |\n')
if not order:
out.write('| - | - | NOT RUN (no step result lines found) |\n')
complete = set(steps) == {str(n) for n in range(1, 8)}
sys.exit(0 if complete and verdict != 'FAIL' and all(status == 'PASS' for _, status in steps.values()) else 1)
PY
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing steps keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns turn the workflow red.
STEPS_TOTAL="$(grep -cE '^\[HEAL-STEP\]' "${LOG_FILE}" 2>/dev/null || true)"
STEPS_FAIL="$(grep -cE '^\[HEAL-STEP\].* FAIL$' "${LOG_FILE}" 2>/dev/null || true)"
STEPS_TOTAL=$(( ${STEPS_TOTAL:-0} + 0 )); STEPS_FAIL=$(( ${STEPS_FAIL:-0} + 0 ))
STEPS_PASS=$(( STEPS_TOTAL - STEPS_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${STEPS_TOTAL}" -gt 0 ] && [ "${STEPS_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS heal test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${STEPS_TABLE}" ]; then
cat "${STEPS_TABLE}"
echo ""
fi
echo "- Product result: ${STEPS_PASS} passed, ${STEPS_FAIL} failed (failing steps are tracked in rustfs/backlog)"
echo ""
if [ -s "${STEPS_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial step results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; step failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: heal
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'heal'
SUITE_LABEL: 'Heal'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload test logs
if: always()
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-heal-test-${{ github.run_id }}
name: rustfs-heal-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
/tmp/rustfs-heal-test*.log
/tmp/rustfs-warp.*.log
if-no-files-found: warn
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/warp.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/steps.md
if-no-files-found: error
- name: Reset test environment (after)
- name: Cleanup environment (after)
timeout-minutes: 5
if: ${{ always() && inputs.cleanup_after != 'false' }}
run: |
./scripts/test/rustfs_heal_test.sh --reset -y
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: "Continue functional chain (next: Pool expansion)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-pool' \
-F 'client_payload[from_suite]=heal'; then
echo "dispatched next suite Pool expansion (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Pool expansion after 3 attempts" >&2
TITLE="[functional][chain] stalled after heal (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **heal** to **Pool expansion** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-pool'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-pool'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
@@ -124,3 +451,24 @@ jobs:
echo "RustFS heal test failed"
echo "Package source: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo "See the uploaded log artifact for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite heal
--report "${FUNCTIONAL_ARTIFACTS_DIR}/steps.md"
--output "${RUNNER_TEMP}/chain-heal-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/heal.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-heal-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-heal-${{ github.run_id }}-${{ github.run_attempt }}/heal.json
if-no-files-found: error
+421
View File
@@ -0,0 +1,421 @@
name: RustFS KMS Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
enforce_sse_key_policy:
description: 'Enable RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY (runs KMS-401/402)'
type: boolean
default: false
frame_v2:
description: 'Enable RUSTFS_ENCRYPTION_FRAME_V2 (runs KMS-318)'
type: boolean
default: false
config_secret:
description: 'Set RUSTFS_KMS_CONFIG_SECRET (runs KMS-107 config sealing)'
required: false
type: string
repository_dispatch:
# Chain handoff: dispatched when the S3 compatibility suite finishes.
types: [rustfs-chain-kms]
permissions:
contents: read
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
kms-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-kms-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
docker --version || true
- name: Cleanup environment (before)
timeout-minutes: 5
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: Ensure docker (Vault container)
run: |
if ! command -v docker >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y docker.io
fi
sudo systemctl enable --now docker
docker info >/dev/null 2>&1 || sudo docker info >/dev/null 2>&1
- name: Run KMS suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-kms-test.sh
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
ARGS=(--all-topologies --backends "local,vault-kv2" -y --log-file "${LOG_FILE}")
EXTRA_ENV=""
if [ "${{ inputs.enforce_sse_key_policy }}" = "true" ]; then
EXTRA_ENV+="RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY=true"$'\n'
fi
if [ "${{ inputs.frame_v2 }}" = "true" ]; then
EXTRA_ENV+="RUSTFS_ENCRYPTION_FRAME_V2=true"$'\n'
fi
if [ -n "${{ inputs.config_secret }}" ]; then
EXTRA_ENV+="RUSTFS_KMS_CONFIG_SECRET=${{ inputs.config_secret }}"$'\n'
fi
if [ -n "${EXTRA_ENV}" ]; then
ARGS+=(--extra-env "${EXTRA_ENV}")
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-kms-test.sh "${ARGS[@]}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS KMS test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: kms
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'kms'
SUITE_LABEL: 'KMS'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-kms-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
if-no-files-found: error
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: "Continue functional chain (next: Tier)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-tier' \
-F 'client_payload[from_suite]=kms'; then
echo "dispatched next suite Tier (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Tier after 3 attempts" >&2
TITLE="[functional][chain] stalled after kms (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **kms** to **Tier** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-tier'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-tier'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS KMS suite failed"
echo "See the uploaded report and log artifacts for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite kms
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-kms-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/kms.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-kms-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-kms-${{ github.run_id }}-${{ github.run_attempt }}/kms.json
if-no-files-found: error
@@ -0,0 +1,353 @@
name: RustFS Performance Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
package_url:
description: 'Direct .deb URL (nightly/R2). Defaults to the latest nightly deb.'
required: false
type: string
test_method:
description: 'Benchmark method(s) to run (manual runs only; "all" = GET+PUT+MIXED)'
type: choice
options:
- all
- get
- put
- mixed
default: 'all'
object_size:
description: 'Object size(s) to test (manual runs only; "all" = all 10 sizes)'
type: choice
options:
- all
- 1KiB
- 4KiB
- 16KiB
- 128KiB
- 1MiB
- 4MiB
- 8MiB
- 16MiB
- 32MiB
- 64MiB
default: 'all'
warp_duration:
description: 'warp duration per round (e.g. 5m, 30s)'
required: false
default: '5m'
warp_concurrency:
description: 'warp concurrency'
required: false
default: '64'
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
default: true
cleanup_after:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
repository_dispatch:
# Chain handoff: dispatched when the replication suite finishes.
types: [rustfs-chain-performance]
permissions:
contents: read
# The default performance nodes overlap the other suites' remote VMs, even
# though the runner differs. Hold the shared lock through cleanup as well.
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
# Performance test uses its own node list (4 nodes); the shared
# RUSTFS_NODES secret is used by the 3-node pool-expansion / heal tests.
RUSTFS_NODES: ${{ secrets.RUSTFS_PERF_NODES || vars.RUSTFS_PERF_NODES || 'vm000 vm001 vm002 vm003' }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
# Package used by the nightly run (workflow_dispatch inputs are empty for
# workflow_run events), i.e. the latest nightly deb published by nightly-gnu.yml.
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
# Cross-repo token for uploading reports to rustfs/dashboard (set in repo settings)
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
runner-check:
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Check performance runner before entering its queue
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: python3 scripts/check_functional_runners.py pf-testing
performance-test:
needs: runner-check
runs-on: pf-testing
timeout-minutes: 900
env:
RUSTFS_BENCH_SCRIPT: ${{ github.workspace }}/auto-testing/rustfs_performance_testing.sh
RUSTFS_WARP_METHODS: ${{ inputs.test_method }}
RUSTFS_WARP_SIZES: ${{ inputs.object_size }}
RUSTFS_WARP_DURATION: ${{ inputs.warp_duration || '5m' }}
RUSTFS_WARP_CONCURRENCY: ${{ inputs.warp_concurrency || '64' }}
# Run on manual dispatch, or when the nightly build completed successfully.
# Skipped when nightly failed.
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout chain tooling
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-performance-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'RUSTFS_RESULT_DIR=%s/results\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'VERSION_FILE=%s/version.txt\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
warp --version || true
df -h /data | tail -1
- name: Reset test environment (before)
if: ${{ inputs.cleanup_before != 'false' }}
run: |
chmod +x auto-testing/rustfs_performance_test.sh
./auto-testing/rustfs_performance_test.sh --step 1 -y --log-file "${LOG_FILE:-/dev/null}"
- name: Install RustFS package & start cluster (4x4)
run: |
ARGS=(--steps "2,3,4" -y)
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./auto-testing/rustfs_performance_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
- name: Preflight checks
run: |
ARGS=(--preflight)
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./auto-testing/rustfs_performance_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
- name: Run benchmark (GET/PUT/MIXED)
id: benchmark
run: |
./auto-testing/rustfs_performance_test.sh \
--step 5 -y \
--log-file "${LOG_FILE}"
- name: Analyze results
if: ${{ steps.benchmark.conclusion == 'success' }}
run: |
export WARP_METHODS="${RUSTFS_WARP_METHODS}" WARP_SIZES="${RUSTFS_WARP_SIZES}"
export WARP_DURATION="${RUSTFS_WARP_DURATION}" WARP_CONCURRENCY="${RUSTFS_WARP_CONCURRENCY}"
./auto-testing/rustfs_performance_test.sh --step 6 -y --log-file "${LOG_FILE:-/dev/null}"
- name: Collect RustFS version info
if: ${{ steps.benchmark.conclusion == 'success' }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES}"
[ "${#NODES[@]}" -gt 0 ] || { echo "RUSTFS_NODES is empty"; exit 1; }
NODE="${NODES[0]}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
{
echo "Node: ${NODE}"
echo "Command: rustfs --version"
echo ""
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${NODE}" 'rustfs --version'
} > "${VERSION_FILE}"
- name: Upload report to dashboard (reports/YYYY-MM-DD.md)
id: chain_report
if: ${{ steps.benchmark.conclusion == 'success' }}
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
RESULT_DIR: ${{ env.RUSTFS_RESULT_DIR }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping report upload"
exit 0
fi
SUMMARY="${RESULT_DIR}/summary.md"
[ -s "${SUMMARY}" ] || { echo "summary.md not found at ${SUMMARY}"; exit 1; }
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="reports/${DATE}.md"
{
echo "# RustFS nightly build performance testing report"
echo ""
echo "- **Date**: ${DATE}"
echo "- **Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- **Attempt**: ${GITHUB_RUN_ATTEMPT}"
echo "- **Workflow Commit**: ${GITHUB_SHA}"
echo "- **Trigger**: ${{ github.event_name }}"
echo "- **Package**: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo ""
cat "${SUMMARY}"
echo ""
echo "## RustFS version"
echo '```text'
cat "${VERSION_FILE}"
echo '```'
} > "${REPORT_FILE}"
CONTENT="$(python3 -c 'import base64,sys; print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}")"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report: ${DATE}" --arg content "${CONTENT}" --arg sha "${SHA}" \
'{message:$msg, content:$content, sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
echo "updated ${REPORT_PATH} in rustfs/dashboard"
else
jq -n --arg msg "report: ${DATE}" --arg content "${CONTENT}" \
'{message:$msg, content:$content}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
echo "created ${REPORT_PATH} in rustfs/dashboard"
fi
- name: Manage backlog issues (dedup / label / auto-close)
# Signal-based lifecycle: dedups against open issues by label,
# labels new issues (functional-test, category, case IDs, env),
# closes fixed issues after a fully green run. Never acts on
# cancelled runs. Logic lives in auto-testing/scripts/.
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'performance'
SUITE_LABEL: 'Performance'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.benchmark.outcome }}" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload test logs & results
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-perf-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/version.txt
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/master.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/summary.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/summary.tsv
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/get_*.txt
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/put_*.txt
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/results/mixed_*.txt
if-no-files-found: error
- name: Reset test environment (after)
if: ${{ always() && inputs.cleanup_after != 'false' }}
run: |
./auto-testing/rustfs_performance_test.sh --step 7 -y --log-file "${LOG_FILE:-/dev/null}"
- name: Notify on failure
if: failure()
run: |
echo "RustFS performance test failed"
echo "Package source: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo "See the uploaded log artifact for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.benchmark.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite performance
--report "${RUSTFS_RESULT_DIR}/summary.tsv"
--output "${RUNNER_TEMP}/chain-performance-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/performance.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-performance-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-performance-${{ github.run_id }}-${{ github.run_attempt }}/performance.json
if-no-files-found: error
+644 -116
View File
@@ -1,12 +1,17 @@
name: RustFS Pool Expansion / Decommission Test
name: RustFS Pool Expansion Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test (e.g. 1.0.0-rc.3)'
description: 'RustFS release tag to test (leave empty to use the latest nightly deb)'
required: false
default: '1.0.0-rc.3'
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
@@ -26,22 +31,14 @@ on:
description: 'warp write duration (e.g. 5m, 10m)'
required: false
default: '10m'
warp_concurrent:
description: 'Pool fill: concurrent warp operations'
required: false
default: '32'
run_decommission:
description: 'Run the pool decommission step (3-pool topology only)'
type: boolean
default: true
stop_node_gb:
description: 'Heal: stop the outage node when surviving nodes reach N GiB'
required: false
default: '15'
warp_stop_gb:
description: 'Heal: stop warp when surviving nodes reach N GiB'
required: false
default: '40'
heal_target_gb:
description: 'Heal: outage node must reach N GiB after heal'
required: false
default: '40'
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
@@ -50,18 +47,18 @@ on:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
workflow_run:
# Run after the nightly build completes: pool expansion first, then heal.
workflows: ["Nightly GNU Build"]
types: [completed]
repository_dispatch:
# Chain handoff: dispatched when the heal suite finishes.
types: [rustfs-chain-pool]
permissions:
contents: read
# Only one pool-expansion test at a time: the workflow mutates a shared
# test environment, so concurrent runs must not clobber each other.
# Only one test run at a time: the job mutates the same shared test
# environment (vm000/vm001/vm002), so concurrent runs must not clobber each
# other.
concurrency:
group: rustfs-pool-expansion-test
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
@@ -72,25 +69,56 @@ env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_API_ENDPOINT: ${{ secrets.RUSTFS_API_ENDPOINT || vars.RUSTFS_API_ENDPOINT || vars.RUSTFS_RC_ENDPOINT }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES || 'vm000 vm001 vm002' }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
# Package used by the nightly run (workflow_dispatch inputs are empty for
# workflow_run events), i.e. the latest nightly deb published by nightly-gnu.yml.
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
jobs:
# Pool expansion: dispatched by the heal suite's chain handoff. Heal
# itself lives in rustfs-heal-test.yml and runs exactly once per chain.
pool-expansion-test:
name: Pool expansion / decommission test
runs-on: smoke-testing
timeout-minutes: 360
# Run on manual dispatch, or when the nightly build completed successfully
# (its deb is what the tests install). Skipped when nightly failed.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
env:
RUSTFS_POOL_ADMIN_ENDPOINT: ${{ secrets.RUSTFS_POOL_ADMIN_ENDPOINT || vars.RUSTFS_POOL_ADMIN_ENDPOINT || 'http://rustfs-node1:9000' }}
RUSTFS_POOL_PROXY_ENDPOINT: http://127.0.0.1:19000
RUSTFS_POOL_WARP_ENDPOINT: http://127.0.0.1:19000
RUSTFS_SHARED_PROXY_ENDPOINT: ${{ secrets.RUSTFS_API_ENDPOINT || vars.RUSTFS_API_ENDPOINT || vars.RUSTFS_RC_ENDPOINT }}
RUSTFS_POOL_NODE_ENDPOINTS: ${{ secrets.RUSTFS_POOL_NODE_ENDPOINTS || vars.RUSTFS_POOL_NODE_ENDPOINTS || 'http://rustfs-node1:9000 http://rustfs-node2:9000 http://rustfs-node3:9000 http://rustfs-node4:9000' }}
steps:
- name: Checkout
- name: Checkout chain tooling
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Initialize pool test artifacts
id: evidence
run: |
set -euo pipefail
ARTIFACT_DIR="${RUNNER_TEMP}/rustfs-pool-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -p "${ARTIFACT_DIR}"
echo "POOL_ARTIFACT_DIR=${ARTIFACT_DIR}" >> "${GITHUB_ENV}"
- name: Show environment
run: |
@@ -100,15 +128,45 @@ jobs:
warp --version || true
df -h /data | tail -1
- name: Reset test environment (before)
- name: Validate pool topology before destructive cleanup
id: topology
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES}"
read -r -a ENDPOINTS <<< "${RUSTFS_POOL_NODE_ENDPOINTS}"
if [ "${#NODES[@]}" -lt 3 ] || [ "${#ENDPOINTS[@]}" -lt "${#NODES[@]}" ]; then
echo "Pool tests require at least three nodes and a direct endpoint for every node" >&2
exit 1
fi
- name: Cleanup environment (before)
timeout-minutes: 5
if: ${{ inputs.cleanup_before != 'false' }}
run: |
chmod +x scripts/test/rustfs_pool_expand.sh
./scripts/test/rustfs_pool_expand.sh --reset -y
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: Install RustFS package & start first pool
- name: Install RustFS package & start cluster
timeout-minutes: 5
run: |
ARGS=(--steps "1,2,3" -y --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
ARGS=(--steps "1,2,3" -y \
--admin-endpoint "${RUSTFS_POOL_ADMIN_ENDPOINT}" \
--warp-endpoint "${RUSTFS_POOL_WARP_ENDPOINT}" \
--node-endpoints "${RUSTFS_POOL_NODE_ENDPOINTS}" \
--log-file "${POOL_ARTIFACT_DIR}/pool-test.log")
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
elif [ -n "${{ inputs.rustfs_version }}" ]; then
@@ -116,11 +174,16 @@ jobs:
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_pool_expand.sh "${ARGS[@]}"
./auto-testing/rustfs_pool_expand.sh "${ARGS[@]}"
- name: Preflight checks
timeout-minutes: 5
run: |
ARGS=(--preflight --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
ARGS=(--preflight \
--admin-endpoint "${RUSTFS_POOL_ADMIN_ENDPOINT}" \
--warp-endpoint "${RUSTFS_POOL_WARP_ENDPOINT}" \
--node-endpoints "${RUSTFS_POOL_NODE_ENDPOINTS}" \
--log-file "${POOL_ARTIFACT_DIR}/pool-test.log")
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
elif [ -n "${{ inputs.rustfs_version }}" ]; then
@@ -128,10 +191,70 @@ jobs:
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_pool_expand.sh "${ARGS[@]}"
./auto-testing/rustfs_pool_expand.sh "${ARGS[@]}"
- name: Reset dedicated pool proxy
timeout-minutes: 2
run: |
set -euo pipefail
RUSTFS_POOL_NGINX_CONFIG_PATH=/etc/nginx/conf.d/rustfs-pool-test.conf \
RUSTFS_POOL_NGINX_LISTEN="${RUSTFS_POOL_PROXY_ENDPOINT#http://}" \
RUSTFS_POOL_NGINX_ACCESS_LOG=/var/log/nginx/rustfs-pool-test-access.log \
RUSTFS_POOL_NGINX_ERROR_LOG=/var/log/nginx/rustfs-pool-test-error.log \
./auto-testing/rustfs_pool_nginx_stage.sh cleanup
- name: Capture pool test baseline
timeout-minutes: 2
run: |
set -uo pipefail
BASELINE_FILE="${POOL_ARTIFACT_DIR}/pool-baseline.log"
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
read -r -a DIRECT_ENDPOINTS <<< "${RUSTFS_POOL_NODE_ENDPOINTS}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
failed=0
: > "${BASELINE_FILE}"
if [ "${#DIRECT_ENDPOINTS[@]}" -lt "${#NODES[@]}" ]; then
echo "not enough direct endpoints for the configured nodes" | tee -a "${BASELINE_FILE}" >&2
exit 1
fi
for index in "${!NODES[@]}"; do
node="${NODES[$index]}"
endpoint="${DIRECT_ENDPOINTS[$index]}"
body_file="${POOL_ARTIFACT_DIR}/ready-baseline-$((index + 1)).body"
{
echo "--- node=${node} endpoint=${endpoint} ---"
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
echo "--- rustfs version ---"
rustfs --version
echo "--- systemd state ---"
${SUDO} systemctl show rustfs --no-pager \
--property=ActiveState,SubState,Result,ExecMainPID,ExecMainStartTimestamp,NRestarts
'; then
echo "baseline collection failed for ${node}"
failed=1
fi
curl -sS --connect-timeout 5 --max-time 15 -o "${body_file}" \
-w "baseline_ready=${endpoint} http=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
"${endpoint%/}/health/ready" || true
echo "--- readiness body ---"
cat "${body_file}" 2>/dev/null || true
echo
} >> "${BASELINE_FILE}" 2>&1
done
[ "${failed}" -eq 0 ] || exit 1
- name: Run pool expansion & decommission test
id: pool_test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
set -o pipefail
STEPS="4,5,6"
@@ -141,27 +264,483 @@ jobs:
STEPS="$STEPS,9"
fi
fi
./scripts/test/rustfs_pool_expand.sh \
--steps "$STEPS" --with-warp -y \
--endpoint "${{ env.RUSTFS_API_ENDPOINT }}" \
ARGS=(--steps "$STEPS" --with-warp -y \
--admin-endpoint "${RUSTFS_POOL_ADMIN_ENDPOINT}" \
--warp-endpoint "${RUSTFS_POOL_WARP_ENDPOINT}" \
--node-endpoints "${RUSTFS_POOL_NODE_ENDPOINTS}" \
--storage-threshold "${{ inputs.storage_threshold || '50' }}" \
--warp-duration "${{ inputs.warp_duration || '10m' }}" \
--log-file /tmp/rustfs-pool-test.log
--warp-concurrent "${{ inputs.warp_concurrent || '32' }}" \
--log-file "${POOL_ARTIFACT_DIR}/pool-test.log")
if [ -n "${RUSTFS_POOL_PROXY_ENDPOINT}" ]; then
ARGS+=(--proxy-endpoint "${RUSTFS_POOL_PROXY_ENDPOINT}")
fi
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
elif [ -n "${{ inputs.rustfs_version }}" ]; then
ARGS+=(--version "${{ inputs.rustfs_version }}")
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
RUSTFS_WARP_LOG_FILE="${POOL_ARTIFACT_DIR}/warp.log" \
RUSTFS_PROXY_STAGE_HOOK=./auto-testing/rustfs_pool_nginx_stage.sh \
RUSTFS_POOL_NGINX_CONFIG_PATH=/etc/nginx/conf.d/rustfs-pool-test.conf \
RUSTFS_POOL_NGINX_LISTEN="${RUSTFS_POOL_PROXY_ENDPOINT#http://}" \
RUSTFS_POOL_NGINX_ACCESS_LOG=/var/log/nginx/rustfs-pool-test-access.log \
RUSTFS_POOL_NGINX_ERROR_LOG=/var/log/nginx/rustfs-pool-test-error.log \
./auto-testing/rustfs_pool_expand.sh "${ARGS[@]}"
- name: Collect pool test diagnostics
timeout-minutes: 2
if: always()
run: |
set -uo pipefail
ARTIFACT_DIR="${POOL_ARTIFACT_DIR:-${RUNNER_TEMP}/rustfs-pool-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}}"
mkdir -p "${ARTIFACT_DIR}"
echo "POOL_ARTIFACT_DIR=${ARTIFACT_DIR}" >> "${GITHUB_ENV}"
redact() {
sed -E \
-e 's/(RUSTFS_(ACCESS_KEY|SECRET_KEY)=).*/\1[REDACTED]/Ig' \
-e 's/(Authorization:).*/\1 [REDACTED]/Ig' \
-e 's/(X-Amz-Signature=)[^&[:space:]]+/\1[REDACTED]/Ig' \
-e 's/(proxy_set_header[[:space:]]+Authorization[[:space:]]+).*/\1[REDACTED];/Ig' \
-e 's/^.*(password|secret|token).*/[REDACTED SENSITIVE LINE]/Ig'
}
if [ "$(id -u)" -eq 0 ]; then
SUDO=()
else
SUDO=(sudo -n)
fi
{
echo "captured_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "run_id=${GITHUB_RUN_ID}"
echo "run_attempt=${GITHUB_RUN_ATTEMPT}"
if command -v nginx >/dev/null 2>&1; then
"${SUDO[@]}" nginx -T 2>&1 || echo "nginx -T failed"
else
echo "nginx is not installed on the runner"
fi
} | redact > "${ARTIFACT_DIR}/nginx-config-redacted.txt"
for log_path in \
/var/log/nginx/access.log \
/var/log/nginx/error.log \
/var/log/nginx/rustfs-pool-test-access.log \
/var/log/nginx/rustfs-pool-test-error.log; do
log_name="$(basename "${log_path}")"
if "${SUDO[@]}" test -r "${log_path}" 2>/dev/null; then
"${SUDO[@]}" cat "${log_path}" 2>&1 | redact \
> "${ARTIFACT_DIR}/nginx-${log_name%.log}-redacted.log"
else
echo "unavailable: ${log_path}" > "${ARTIFACT_DIR}/nginx-${log_name%.log}-redacted.log"
fi
done
"${SUDO[@]}" journalctl -u nginx --no-pager -n 5000 2>&1 | redact \
> "${ARTIFACT_DIR}/nginx-journal-redacted.log" || true
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
safe_node="${node//[^A-Za-z0-9_.-]/_}"
{
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${node}" '
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
echo "--- rustfs version ---"
rustfs --version 2>&1 || true
echo "--- systemd state ---"
${SUDO} systemctl show rustfs --no-pager \
--property=ActiveState,SubState,Result,ExecMainPID,ExecMainStartTimestamp,NRestarts 2>&1 || true
echo "--- rustfs journal ---"
${SUDO} journalctl -u rustfs --no-pager -n 10000 2>&1 || true
echo "--- rustfs file logs ---"
if ${SUDO} test -d /var/log/rustfs; then
${SUDO} find /var/log/rustfs -maxdepth 2 -type f -print 2>/dev/null | while IFS= read -r file; do
echo "--- ${file} (last 5000 lines) ---"
${SUDO} tail -n 5000 "${file}" 2>&1 || true
done
else
echo "/var/log/rustfs is unavailable"
fi
'; then
echo "SSH diagnostics failed for ${node}"
fi
} 2>&1 | redact > "${ARTIFACT_DIR}/${safe_node}-rustfs-redacted.log"
done
: > "${ARTIFACT_DIR}/endpoint-ready-probes.log"
read -r -a DIRECT_ENDPOINTS <<< "${RUSTFS_POOL_NODE_ENDPOINTS}"
probe_index=0
for endpoint in "${DIRECT_ENDPOINTS[@]}"; do
probe_index=$((probe_index + 1))
curl -sS --connect-timeout 5 --max-time 15 -o "${ARTIFACT_DIR}/ready-direct-${probe_index}.body" \
-w "direct[${probe_index}]=${endpoint} http=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
"${endpoint%/}/health/ready" >> "${ARTIFACT_DIR}/endpoint-ready-probes.log" 2>&1 || true
done
if [ -n "${RUSTFS_POOL_PROXY_ENDPOINT}" ]; then
curl -sS --connect-timeout 5 --max-time 15 -o "${ARTIFACT_DIR}/ready-proxy.body" \
-w "proxy=${RUSTFS_POOL_PROXY_ENDPOINT} http=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
"${RUSTFS_POOL_PROXY_ENDPOINT%/}/health/ready" >> "${ARTIFACT_DIR}/endpoint-ready-probes.log" 2>&1 || true
fi
if [ -n "${RUSTFS_SHARED_PROXY_ENDPOINT}" ]; then
curl -sS --connect-timeout 5 --max-time 15 -o "${ARTIFACT_DIR}/ready-shared-proxy.body" \
-w "shared_proxy=${RUSTFS_SHARED_PROXY_ENDPOINT} http=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
"${RUSTFS_SHARED_PROXY_ENDPOINT%/}/health/ready" >> "${ARTIFACT_DIR}/endpoint-ready-probes.log" 2>&1 || true
fi
- name: Generate report
id: chain_report
timeout-minutes: 2
if: always()
run: |
set -euo pipefail
LOG_FILE="${POOL_ARTIFACT_DIR}/pool-test.log"
REPORT_FILE="${POOL_ARTIFACT_DIR}/pool-report.md"
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
STEPS_TABLE="${POOL_ARTIFACT_DIR}/pool-steps.md"
python3 - "${LOG_FILE}" "${STEPS_TABLE}" <<'PY'
import re
import sys
log_file, out_file = sys.argv[1], sys.argv[2]
ansi = re.compile(r'\x1b\[[0-9;]*m')
step_re = re.compile(r'^\[POOL-STEP\]\s+(\d+)\s+(.+?)\s+(PASS|FAIL|SKIP)\s*$')
ver_re = re.compile(r'^\[POOL-VERSION\]\s+(\S+)(?:\s+\(node\s+(\S+)\))?\s*$')
result_re = re.compile(r'^\[POOL-RESULT\]\s+(PASS|FAIL)\s+(.*)$')
steps = {}
order = []
version = None
version_node = None
verdict = None
verdict_detail = ''
try:
with open(log_file, 'r', encoding='utf-8', errors='replace') as fh:
for raw in fh:
line = ansi.sub('', raw).strip()
m = step_re.match(line)
if m:
n, desc, status = m.group(1), m.group(2), m.group(3)
if n not in steps:
order.append(n)
steps[n] = (desc, status) # later lines win (fail after pass)
continue
m = ver_re.match(line)
if m:
version, version_node = m.group(1), m.group(2)
continue
m = result_re.match(line)
if m:
verdict, verdict_detail = m.group(1), m.group(2)
except FileNotFoundError:
pass
with open(out_file, 'w', encoding='utf-8') as out:
out.write('## Step Results\n\n')
if version:
node_note = f' (captured via `rustfs --version` on {version_node})' if version_node else ''
out.write(f'- Version under test: **{version}**{node_note}\n')
if verdict:
out.write(f'- Overall result: **{verdict}** — {verdict_detail}\n')
out.write('\n')
out.write('| Step | Description | Result |\n')
out.write('| --- | --- | --- |\n')
for n in sorted(order, key=int):
desc, status = steps[n]
out.write(f'| {n} | {desc} | {status} |\n')
if not order:
out.write('| - | - | NOT RUN (no step result lines found) |\n')
PY
{
echo "# RustFS pool expansion test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- Warp concurrent: ${{ inputs.warp_concurrent || '32' }}"
echo "- Test Step Outcome: ${{ steps.pool_test.outcome }}"
echo ""
cat "${STEPS_TABLE}" || true
echo ""
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" || true
echo '```'
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
- name: Validate pool diagnostic completeness
timeout-minutes: 2
if: always()
run: |
set -euo pipefail
failed=0
require_nonempty() {
if [ ! -s "$1" ]; then
echo "required diagnostic is missing or empty: $1" >&2
failed=1
fi
}
require_available() {
if [ ! -e "$1" ]; then
echo "required diagnostic is missing: $1" >&2
failed=1
elif grep -Fq 'unavailable:' "$1" 2>/dev/null; then
echo "required diagnostic could not be collected: $1" >&2
failed=1
fi
}
require_nonempty "${POOL_ARTIFACT_DIR}/pool-test.log"
# warp is stopped early (SIGINT/SIGTERM) once the storage threshold
# is reached, and it only writes its final report on a clean exit —
# an empty warp.log is the expected shape of a healthy run, so only
# require that the redirect target exists.
require_available "${POOL_ARTIFACT_DIR}/warp.log"
require_nonempty "${POOL_ARTIFACT_DIR}/pool-report.md"
require_nonempty "${POOL_ARTIFACT_DIR}/pool-baseline.log"
require_nonempty "${POOL_ARTIFACT_DIR}/nginx-config-redacted.txt"
require_nonempty "${POOL_ARTIFACT_DIR}/nginx-rustfs-pool-test-access-redacted.log"
require_available "${POOL_ARTIFACT_DIR}/nginx-rustfs-pool-test-access-redacted.log"
require_available "${POOL_ARTIFACT_DIR}/nginx-rustfs-pool-test-error-redacted.log"
require_nonempty "${POOL_ARTIFACT_DIR}/endpoint-ready-probes.log"
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
if grep -Fq 'baseline collection failed' "${POOL_ARTIFACT_DIR}/pool-baseline.log" 2>/dev/null; then
echo "one or more node baselines could not be collected" >&2
failed=1
fi
for node in "${NODES[@]}"; do
safe_node="${node//[^A-Za-z0-9_.-]/_}"
node_log="${POOL_ARTIFACT_DIR}/${safe_node}-rustfs-redacted.log"
require_nonempty "${node_log}"
if grep -Fq "SSH diagnostics failed for ${node}" "${node_log}" 2>/dev/null; then
echo "node diagnostics failed: ${node_log}" >&2
failed=1
fi
if ! grep -Eq '^rustfs @' "${node_log}" 2>/dev/null \
|| ! grep -Eq '^NRestarts=[0-9]+$' "${node_log}" 2>/dev/null; then
echo "node version or restart evidence is incomplete: ${node_log}" >&2
failed=1
elif grep -Eq '^NRestarts=[1-9][0-9]*$' "${node_log}"; then
echo "RustFS restarted unexpectedly during the run: ${node_log}" >&2
failed=1
fi
done
if ! grep -Fq "upstream_status=\"\$upstream_status\"" \
"${POOL_ARTIFACT_DIR}/nginx-config-redacted.txt"; then
echo "Nginx config does not expose upstream status fields" >&2
failed=1
fi
if ! grep -Eq '^proxy=.* http=200([[:space:]]|$)' "${POOL_ARTIFACT_DIR}/endpoint-ready-probes.log"; then
echo "dedicated proxy readiness probe did not return HTTP 200" >&2
failed=1
fi
if grep -Eq 'status=50(2|4)|upstream_status="[^"]*50(2|4)' \
"${POOL_ARTIFACT_DIR}/nginx-rustfs-pool-test-access-redacted.log"; then
echo "dedicated proxy access log contains a 502/504 response" >&2
failed=1
fi
if grep -Eiq 'upstream prematurely closed connection|upstream timed out|(connect\(\)|recv\(\)|send\(\)) failed.*upstream|connection reset by peer.*upstream' \
"${POOL_ARTIFACT_DIR}/nginx-rustfs-pool-test-error-redacted.log"; then
echo "dedicated proxy error log contains an upstream timeout or connection failure" >&2
failed=1
fi
# A mid-script abort is a product/harness signal: the run step's
# continue-on-error would otherwise keep the workflow green, so the
# validator must turn it red. Aborts go through die(), which prints
# an [ERROR] line; a step that dies never emits its [POOL-STEP]
# verdict, so "any FAIL verdict" cannot be the trigger.
if grep -q '^\[POOL-STEP\] .* FAIL' "${POOL_ARTIFACT_DIR}/pool-test.log" 2>/dev/null \
|| grep -q '\[ERROR\]' "${POOL_ARTIFACT_DIR}/pool-test.log" 2>/dev/null; then
echo "pool test script reported a failure (step FAIL verdict or die() abort)" >&2
failed=1
fi
[ "${failed}" -eq 0 ] || exit 1
- name: Upload functional report to dashboard
timeout-minutes: 2
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: pool
run: |
set -euo pipefail
REPORT_FILE="${POOL_ARTIFACT_DIR}/pool-report.md"
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'pool'
SUITE_LABEL: 'Pool expansion'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.pool_test.outcome }}" \
--report-file "${POOL_ARTIFACT_DIR}/pool-report.md" \
--log "${POOL_ARTIFACT_DIR}/pool-test.log" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload test logs
timeout-minutes: 2
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-pool-test-${{ github.run_id }}
path: |
/tmp/rustfs-pool-test*.log
/tmp/rustfs-warp.*.log
name: rustfs-pool-test-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/rustfs-pool-${{ github.run_id }}-${{ github.run_attempt }}
if-no-files-found: warn
- name: Reset test environment (after)
if: ${{ always() && inputs.cleanup_after != 'false' }}
- name: Restore dedicated pool proxy
timeout-minutes: 2
if: always()
run: |
./scripts/test/rustfs_pool_expand.sh --reset -y
set -euo pipefail
RUSTFS_POOL_NGINX_CONFIG_PATH=/etc/nginx/conf.d/rustfs-pool-test.conf \
RUSTFS_POOL_NGINX_LISTEN="${RUSTFS_POOL_PROXY_ENDPOINT#http://}" \
RUSTFS_POOL_NGINX_ACCESS_LOG=/var/log/nginx/rustfs-pool-test-access.log \
RUSTFS_POOL_NGINX_ERROR_LOG=/var/log/nginx/rustfs-pool-test-error.log \
./auto-testing/rustfs_pool_nginx_stage.sh cleanup
- name: Cleanup environment (after)
timeout-minutes: 5
if: ${{ always() && steps.topology.outcome == 'success' && inputs.cleanup_after != 'false' }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: "Continue functional chain (next: Security)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-security' \
-F 'client_payload[from_suite]=pool'; then
echo "dispatched next suite Security (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Security after 3 attempts" >&2
TITLE="[functional][chain] stalled after pool (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **pool** to **Security** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-security'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-security'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
@@ -170,74 +749,23 @@ jobs:
echo "Package source: ${{ inputs.package_url || inputs.rustfs_version || 'nightly (R2 latest)' }}"
echo "See the uploaded log artifact for details."
# Heal regression runs after the pool test regardless of its outcome: a pool
# failure must be reported (it makes the run red) but must not block heal.
heal-test:
name: Heal test (after pool test)
runs-on: smoke-testing
timeout-minutes: 480
needs: pool-expansion-test
if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.pool_test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite pool
--report "${POOL_ARTIFACT_DIR}/pool-steps.md"
--output "${RUNNER_TEMP}/chain-pool-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/pool.json"
- name: Reset test environment (before)
if: ${{ inputs.cleanup_before != 'false' }}
run: |
chmod +x scripts/test/rustfs_heal_test.sh
./scripts/test/rustfs_heal_test.sh --reset -y
- name: Install RustFS package & start cluster
run: |
ARGS=(--steps "1,2" -y --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_heal_test.sh "${ARGS[@]}"
- name: Preflight checks
run: |
ARGS=(--preflight --endpoint "${{ env.RUSTFS_API_ENDPOINT }}")
if [ -n "${{ inputs.package_url }}" ]; then
ARGS+=(--package-url "${{ inputs.package_url }}")
else
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
fi
./scripts/test/rustfs_heal_test.sh "${ARGS[@]}"
- name: Run heal test (write -> outage -> heal -> verify)
run: |
./scripts/test/rustfs_heal_test.sh \
--steps 3,4,5,6,7 -y \
--endpoint "${{ env.RUSTFS_API_ENDPOINT }}" \
--stop-node-gb "${{ inputs.stop_node_gb || '15' }}" \
--warp-stop-gb "${{ inputs.warp_stop_gb || '40' }}" \
--heal-target-gb "${{ inputs.heal_target_gb || '40' }}" \
--log-file /tmp/rustfs-heal-test.log
- name: Upload test logs
if: always()
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-heal-test-${{ github.run_id }}
path: |
/tmp/rustfs-heal-test.log
/tmp/rustfs-warp.*.log
if-no-files-found: warn
- name: Reset test environment (after)
if: ${{ always() && inputs.cleanup_after != 'false' }}
run: |
./scripts/test/rustfs_heal_test.sh --reset -y
- name: Notify on failure
if: failure()
run: |
echo "RustFS heal test failed"
echo "See the uploaded log artifact for details."
name: functional-chain-pool-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-pool-${{ github.run_id }}-${{ github.run_attempt }}/pool.json
if-no-files-found: error
@@ -0,0 +1,426 @@
# Copyright 2024 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.
name: RustFS Replication Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
suite:
description: 'Suite to run (all = bucket REP-* then site SITE-*)'
type: choice
options:
- all
- bucket
- site
default: all
repository_dispatch:
# Chain handoff: dispatched when the security suite finishes.
types: [rustfs-chain-replication]
permissions:
contents: read
# The replication suite uses the same shared VMs as the other functional
# tests, so it must serialize with them instead of running in parallel.
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
replication-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-replication-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
aws --version
df -h /data | tail -1 || true
- name: Cleanup environment (before)
timeout-minutes: 5
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs rustfs-rep2 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /data/rustfs-rep2 /var/log/rustfs /var/log/rustfs-rep2 /var/lib/rustfs/kms
'
done
- name: Run replication suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-replication-test.sh
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
SUITE='${{ inputs.suite }}'
ARGS=(-y --log-file "${LOG_FILE}")
if [ "${SUITE}" = "all" ] || [ -z "${SUITE}" ] || [ "${SUITE}" = "null" ]; then
ARGS+=(--suite all)
else
ARGS+=(--suite "${SUITE}")
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-replication-test.sh "${ARGS[@]}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
RUSTFS_VERSION_INFO="N/A"
if [ "${#NODES[@]}" -gt 0 ]; then
DETECTED_VERSION="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${NODES[0]}" 'rustfs --version' 2>/dev/null | tr -d '\r' | head -n 1 || true)"
if [ -n "${DETECTED_VERSION}" ]; then
RUSTFS_VERSION_INFO="${DETECTED_VERSION}"
fi
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS replication test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- RustFS Version: ${RUSTFS_VERSION_INFO}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: replication
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'replication'
SUITE_LABEL: 'Replication (bucket + site)'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-replication-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
if-no-files-found: error
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs rustfs-rep2 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /data/rustfs-rep2 /var/log/rustfs /var/log/rustfs-rep2
'
done
- name: "Continue functional chain (next: Fault tolerance)"
if: ${{ always() && github.event_name == 'repository_dispatch' }}
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-fault-tolerance' \
-F 'client_payload[from_suite]=replication'; then
echo "dispatched next suite Fault tolerance (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Fault tolerance after 3 attempts" >&2
TITLE="[functional][chain] stalled after replication (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
trap 'rm -f "${BODY_FILE}"' EXIT
{
echo "The functional chain could not hand off from **replication** to **Fault tolerance** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-fault-tolerance'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-fault-tolerance'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS replication suite failed"
echo "Package source: ${{ inputs.package_url || inputs.rustfs_version || 'nightly (R2 latest)' }}"
echo "See the uploaded report and log artifacts for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite replication
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-replication-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/replication.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-replication-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-replication-${{ github.run_id }}-${{ github.run_attempt }}/replication.json
if-no-files-found: error
+398
View File
@@ -0,0 +1,398 @@
name: RustFS S3 Compatibility Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
repository_dispatch:
# Chain handoff: dispatched when the upgrade suite finishes.
types: [rustfs-chain-s3]
permissions:
contents: read
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
s3-compat-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-s3-compat-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs
'
done
- name: Run S3 compatibility suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-s3-compat-test.sh
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
ARGS=(--all-topologies -y --log-file "${LOG_FILE}")
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-s3-compat-test.sh "${ARGS[@]}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
RUSTFS_VERSION_INFO="N/A"
if [ "${#NODES[@]}" -gt 0 ]; then
DETECTED_VERSION="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${NODES[0]}" 'rustfs --version' 2>/dev/null | tr -d '\r' | head -n 1 || true)"
if [ -n "${DETECTED_VERSION}" ]; then
RUSTFS_VERSION_INFO="${DETECTED_VERSION}"
fi
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS S3 compatibility test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- RustFS Version: ${RUSTFS_VERSION_INFO}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: s3
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 's3'
SUITE_LABEL: 'S3 compatibility'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-s3-compat-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
if-no-files-found: error
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs
'
done
- name: "Continue functional chain (next: KMS)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-kms' \
-F 'client_payload[from_suite]=s3'; then
echo "dispatched next suite KMS (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch KMS after 3 attempts" >&2
TITLE="[functional][chain] stalled after s3 (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **s3** to **KMS** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-kms'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-kms'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS S3 compatibility suite failed"
echo "See the uploaded report and log artifacts for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite s3
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-s3-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/s3.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-s3-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-s3-${{ github.run_id }}-${{ github.run_attempt }}/s3.json
if-no-files-found: error
+402
View File
@@ -0,0 +1,402 @@
# Copyright 2024 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.
name: RustFS Security Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
topology:
description: 'Topology to run (all = SNSD, SNMD, MNMD)'
type: choice
options:
- all
- single-single
- single-multi
- multi-multi
default: all
oidc_live:
description: 'Run the live Keycloak OIDC/SSO gate as part of the suite'
type: boolean
default: true
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
default: true
cleanup_after:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
repository_dispatch:
# Chain handoff: dispatched when the pool expansion suite finishes.
types: [rustfs-chain-security]
permissions:
contents: read
# The security suite uses the same shared VMs as the other functional tests,
# so it must serialize with them instead of running in parallel.
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
security-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
# Checkout the repository into its own subdirectory. Checking out at
# the workspace root would wipe the auto-testing clone above (that is
# exactly how run 33934141181 lost rustfs-security-test.sh).
- name: Checkout repository (for the OIDC live gate script)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
path: rustfs-repo
- name: Initialize security evidence
id: evidence
run: |
set -euo pipefail
umask 077
SECURITY_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-security-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${SECURITY_ARTIFACTS_DIR}" "${SECURITY_ARTIFACTS_DIR}-scratch"
printf 'SECURITY_ARTIFACTS_DIR=%s\n' "${SECURITY_ARTIFACTS_DIR}" >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
aws --version || true
docker --version || true
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
if: ${{ inputs.cleanup_before != 'false' || github.event_name != 'workflow_dispatch' }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms
'
done
- name: Run security suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
env:
REPORT_FILE: ${{ env.SECURITY_ARTIFACTS_DIR }}/suite-report.md
TMPDIR: ${{ env.SECURITY_ARTIFACTS_DIR }}-scratch
RUSTFS_SECURITY_OIDC_LIVE_SCRIPT: ${{ github.workspace }}/rustfs-repo/scripts/test/oidc_keycloak_live.sh
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-security-test.sh
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
TOPOLOGY='${{ inputs.topology }}'
ARGS=(-y)
if [ "${TOPOLOGY}" = "all" ] || [ -z "${TOPOLOGY}" ] || [ "${TOPOLOGY}" = "null" ]; then
ARGS+=(--all-topologies)
else
ARGS+=(--topology "${TOPOLOGY}")
fi
if [ "${{ inputs.oidc_live }}" = "true" ] || [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
ARGS+=(--oidc-live)
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
GITHUB_STEP_SUMMARY=/dev/null ./auto-testing/rustfs-security-test.sh "${ARGS[@]}" 2>&1 | tee "${SECURITY_ARTIFACTS_DIR}/suite.log"
- name: Generate report
id: report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
env:
TEST_OUTCOME: ${{ steps.test.outcome }}
run: |
set -euo pipefail
LOG_FILE="${SECURITY_ARTIFACTS_DIR}/suite.log"
RESULT=failure
if [ "${TEST_OUTCOME}" = "success" ] && [ -s "${SECURITY_ARTIFACTS_DIR}/suite-report.md" ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns turn the workflow red. The suite log lines carry ANSI
# color escapes both before and after the verdict tag
# (e.g. "\e[1;31m[FAIL]\e[0m STS-105 ..."), so the pattern allows
# any number of escapes on each side of it.
ESC=$'\033'
VERDICTS_TOTAL="$(grep -cE "^(${ESC}\[[0-9;]*m)*\[(PASS|FAIL|UNSUPPORTED)\](${ESC}\[[0-9;]*m)* [A-Z]" "${LOG_FILE}" 2>/dev/null || true)"
VERDICTS_FAIL="$(grep -cE "^(${ESC}\[[0-9;]*m)*\[FAIL\](${ESC}\[[0-9;]*m)* [A-Z]" "${LOG_FILE}" 2>/dev/null || true)"
VERDICTS_TOTAL=$(( ${VERDICTS_TOTAL:-0} + 0 )); VERDICTS_FAIL=$(( ${VERDICTS_FAIL:-0} + 0 ))
VERDICTS_PASS=$(( VERDICTS_TOTAL - VERDICTS_FAIL ))
VERDICTS_SKIPPED="$(grep -cE "^(${ESC}\[[0-9;]*m)*\[SKIP\](${ESC}\[[0-9;]*m)* [A-Z]" "${LOG_FILE}" 2>/dev/null || true)"
VERDICTS_SKIPPED=$(( ${VERDICTS_SKIPPED:-0} + 0 ))
HARNESS_OK=0
if [ "${TEST_OUTCOME}" = "success" ]; then
HARNESS_OK=1
elif [ "${TEST_OUTCOME}" = "failure" ] && [ "${VERDICTS_TOTAL}" -gt 0 ] && [ "${VERDICTS_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS security test report"
echo ""
echo "- Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${GITHUB_EVENT_NAME}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${TEST_OUTCOME}"
echo ""
# The dashboard prioritizes case rows over the step outcome.
# Keep partial case results in the artifact when the suite fails.
if [ -s "${SECURITY_ARTIFACTS_DIR}/suite-report.md" ]; then
cat "${SECURITY_ARTIFACTS_DIR}/suite-report.md"
echo ""
fi
if [ "${VERDICTS_SKIPPED}" -gt 0 ]; then
echo "- Product result: ${VERDICTS_PASS} passed, ${VERDICTS_FAIL} failed, ${VERDICTS_SKIPPED} skipped (failing cases are tracked in rustfs/backlog)"
else
echo "- Product result: ${VERDICTS_PASS} passed, ${VERDICTS_FAIL} failed (failing cases are tracked in rustfs/backlog)"
fi
} > "${SECURITY_ARTIFACTS_DIR}/report.md"
cat "${SECURITY_ARTIFACTS_DIR}/report.md" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
REPORT_FILE: ${{ env.SECURITY_ARTIFACTS_DIR }}/report.md
SUITE: security
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'security'
SUITE_LABEL: 'Security'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report-file "${SECURITY_ARTIFACTS_DIR}/report.md" \
--log "${SECURITY_ARTIFACTS_DIR}/suite.log" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-security-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.SECURITY_ARTIFACTS_DIR }}/report.md
${{ env.SECURITY_ARTIFACTS_DIR }}/suite.log
${{ env.SECURITY_ARTIFACTS_DIR }}/suite-report.md
if-no-files-found: error
retention-days: 3
- name: Cleanup environment (after)
timeout-minutes: 5
if: ${{ always() && (inputs.cleanup_after != 'false' || github.event_name != 'workflow_dispatch') }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms
'
done
- name: "Continue functional chain (next: Replication)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
echo "Dispatching next functional suite: Replication"
gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-replication' \
-F 'client_payload[from_suite]=security'
- name: Notify on failure
if: failure()
run: |
echo "RustFS security test failed"
echo "Package source: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
echo "See the uploaded report and logs for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite security
--report "${SECURITY_ARTIFACTS_DIR}/suite-report.md"
--output "${RUNNER_TEMP}/chain-security-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/security.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-security-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-security-${{ github.run_id }}-${{ github.run_attempt }}/security.json
if-no-files-found: error
+413
View File
@@ -0,0 +1,413 @@
name: RustFS Storage Engine Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
topology:
description: 'Topology to run (all = SNSD, SNMD, MNMD)'
type: choice
options:
- all
- single-single
- single-multi
- multi-multi
default: all
repository_dispatch:
# Chain handoff: dispatched when the tier suite finishes.
types: [rustfs-chain-storage]
permissions:
contents: read
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
storage-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-storage-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: Run storage engine suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-storage-test.sh
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
TOPOLOGY='${{ inputs.topology }}'
ARGS=(-y --log-file "${LOG_FILE}")
if [ "${TOPOLOGY}" = "all" ] || [ -z "${TOPOLOGY}" ] || [ "${TOPOLOGY}" = "null" ]; then
ARGS+=(--all-topologies)
else
ARGS+=(--topology "${TOPOLOGY}")
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-storage-test.sh "${ARGS[@]}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
RUSTFS_VERSION_INFO="N/A"
if [ "${#NODES[@]}" -gt 0 ]; then
DETECTED_VERSION="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${NODES[0]}" 'rustfs --version' 2>/dev/null | tr -d '\r' | head -n 1 || true)"
if [ -n "${DETECTED_VERSION}" ]; then
RUSTFS_VERSION_INFO="${DETECTED_VERSION}"
fi
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS storage engine test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- RustFS Version: ${RUSTFS_VERSION_INFO}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: storage
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'storage'
SUITE_LABEL: 'Storage engine'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-storage-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
if-no-files-found: error
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs
'
done
- name: "Continue functional chain (next: Heal)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-heal' \
-F 'client_payload[from_suite]=storage'; then
echo "dispatched next suite Heal (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Heal after 3 attempts" >&2
TITLE="[functional][chain] stalled after storage (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **storage** to **Heal** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-heal'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-heal'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS storage engine suite failed"
echo "See the uploaded report and log artifacts for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite storage
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-storage-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/storage.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-storage-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-storage-${{ github.run_id }}-${{ github.run_attempt }}/storage.json
if-no-files-found: error
+360
View File
@@ -0,0 +1,360 @@
name: RustFS Table Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
rustfs_version:
description: 'RustFS release tag to test (leave empty to use the latest nightly deb)'
required: false
type: string
skip_smoke:
description: 'Skip the PyIceberg end-to-end case (TBL-101)'
required: false
type: boolean
default: false
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
permissions:
contents: read
env:
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES || 'vm000 vm001 vm002 vm003' }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER || 'azureuser' }}
RUSTFS_API_ENDPOINT: ${{ secrets.RUSTFS_API_ENDPOINT || vars.RUSTFS_API_ENDPOINT || 'http://rustfs-node1:9000' }}
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NIGHTLY_PACKAGE_URL: https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
table-test:
name: table-test
runs-on: smoke-testing
timeout-minutes: 60
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-table-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
timeout-minutes: 5
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
set -euo pipefail
echo "nodes: ${RUSTFS_NODES}"
echo "endpoint: ${RUSTFS_API_ENDPOINT}"
aws --version
python3 --version
- name: Cleanup environment (before)
timeout-minutes: 5
continue-on-error: true
run: |
set -uo pipefail
for node in ${RUSTFS_NODES}; do
ssh -o BatchMode=yes -o ConnectTimeout=10 "${RUSTFS_SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
' || echo "pre-cleanup failed on ${node} (continuing)"
done
- name: Stage verified candidate package
id: chain_package
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/prepare_functional_package.py prepare
- name: Run table suite
id: test
timeout-minutes: 45
# Standalone case failures are reported by the backlog manager.
# Chain evidence separately requires complete passing cases.
continue-on-error: true
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-table-test.sh
PACKAGE_URL='${{ steps.chain_package.outputs.package_url || inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
SMOKE_PATH="${GITHUB_WORKSPACE:-$PWD}/scripts/table-catalog/pyiceberg_smoke.py"
ARGS=(-y --smoke-script "${SMOKE_PATH}")
if [ '${{ inputs.skip_smoke }}' = 'true' ]; then
ARGS+=(--skip-smoke)
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
ARGS+=(--package-url "https://github.com/rustfs/rustfs/releases/download/${RUSTFS_VERSION}/rustfs_${RUSTFS_VERSION}_amd64.deb")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-table-test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
- name: Generate report
timeout-minutes: 2
id: chain_report
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS S3 Tables (Iceberg REST Catalog) test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
REPORT_FILE: ${{ env.REPORT_FILE }}
SUITE: table
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
echo "dashboard updated: ${REPORT_PATH}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found; skipping backlog issue management"
exit 0
fi
PACKAGE_URL='${{ inputs.package_url }}'
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
if [ -n "${PACKAGE_URL}" ]; then PACKAGE_SOURCE="${PACKAGE_URL}"; fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--outcome "${{ steps.test.outcome }}" \
--suite table --category table --suite-label "S3 Tables" \
--outcome '${{ steps.test.outcome }}' \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${GITHUB_EVENT_NAME}" \
--package-source "${PACKAGE_SOURCE}" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-table-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
if-no-files-found: error
retention-days: 30
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
continue-on-error: true
run: |
set -uo pipefail
for node in ${RUSTFS_NODES}; do
ssh -o BatchMode=yes -o ConnectTimeout=10 "${RUSTFS_SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -f /etc/default/rustfs
' || echo "cleanup failed on ${node} (continuing)"
done
- name: "Continue functional chain (next: Performance)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
echo "Dispatching next functional suite: Performance"
gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-performance' \
-F 'client_payload[from_suite]=table'
- name: Notify on failure
if: failure()
run: echo "RustFS table test workflow failed (harness/environment breakdown); see logs and report artifact."
- name: Remove verified candidate package
if: ${{ always() && inputs.chain_manifest != '' && steps.chain.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/prepare_functional_package.py cleanup
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite table
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-table-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/table.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-table-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-table-${{ github.run_id }}-${{ github.run_attempt }}/table.json
if-no-files-found: error
+741
View File
@@ -0,0 +1,741 @@
name: RustFS Tier Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
rustfs_version:
description: 'RustFS release tag to test. Leave empty for nightly.'
required: false
default: ''
package_url:
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
required: false
type: string
rc_archive_url:
description: 'Exact RustFS CLI Linux archive URL.'
required: false
default: 'https://github.com/rustfs/cli/releases/download/v0.1.32/rustfs-cli-linux-amd64-v0.1.32.tar.gz'
type: string
rc_archive_sha256:
description: 'Expected SHA-256 of the RustFS CLI archive.'
required: false
default: 'ab00d937079dcb6f1c7b41d34bbfaad0eb0bd4f7218672cbcb7c33652d1c46df'
type: string
rc_sha256:
description: 'Expected SHA-256 of the extracted RustFS CLI binary.'
required: false
default: '320bdd4223a4d1986c1a098165f2198e92c35c4042b9a4d5e6fa33e9152477df'
type: string
force_case_failure:
description: 'Diagnostic only: rewrite single-single/TIER-101 to FAIL after execution to verify artifact and final-gate behavior.'
required: false
default: false
type: boolean
repository_dispatch:
# Chain handoff: dispatched when the KMS suite finishes.
types: [rustfs-chain-tier]
permissions:
contents: read
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
RUSTFS_RC_ARCHIVE_URL: ${{ inputs.rc_archive_url || 'https://github.com/rustfs/cli/releases/download/v0.1.32/rustfs-cli-linux-amd64-v0.1.32.tar.gz' }}
RUSTFS_RC_ARCHIVE_SHA256: ${{ inputs.rc_archive_sha256 || 'ab00d937079dcb6f1c7b41d34bbfaad0eb0bd4f7218672cbcb7c33652d1c46df' }}
RUSTFS_EXPECTED_RC_SHA256: ${{ inputs.rc_sha256 || '320bdd4223a4d1986c1a098165f2198e92c35c4042b9a4d5e6fa33e9152477df' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
TIER_ARTIFACTS_DIR: /tmp/rustfs-tier-artifacts-${{ github.run_id }}-${{ github.run_attempt }}
jobs:
tier-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout chain tooling
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize run evidence directory
id: evidence
run: |
set -euo pipefail
umask 077
if ! mkdir -- "${TIER_ARTIFACTS_DIR}"; then
echo "refusing to reuse tier evidence path: ${TIER_ARTIFACTS_DIR}" >&2
exit 1
fi
test -d "${TIER_ARTIFACTS_DIR}"
test ! -L "${TIER_ARTIFACTS_DIR}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Prepare pinned RustFS CLI
timeout-minutes: 5
id: rc
run: |
set -euo pipefail
umask 077
case "${RUSTFS_RC_ARCHIVE_URL}" in
https://*) ;;
*)
echo "RustFS CLI archive URL must use HTTPS" >&2
exit 1
;;
esac
EXPECTED_ARCHIVE_SHA256="$(printf '%s' "${RUSTFS_RC_ARCHIVE_SHA256}" | tr '[:upper:]' '[:lower:]')"
EXPECTED_RC_SHA256="$(printf '%s' "${RUSTFS_EXPECTED_RC_SHA256}" | tr '[:upper:]' '[:lower:]')"
if ! [[ "${EXPECTED_ARCHIVE_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "invalid RustFS CLI archive SHA-256" >&2
exit 1
fi
if ! [[ "${EXPECTED_RC_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "invalid RustFS CLI binary SHA-256" >&2
exit 1
fi
RC_ROOT="${RUNNER_TEMP}/rustfs-tier-rc-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
RC_ARCHIVE="${RC_ROOT}/rustfs-cli.tar.gz"
RC_BIN="${RC_ROOT}/rc"
if ! mkdir -- "${RC_ROOT}"; then
echo "refusing to reuse RustFS CLI directory: ${RC_ROOT}" >&2
exit 1
fi
curl --fail --location --retry 3 --retry-all-errors \
--connect-timeout 15 --max-time 180 \
--proto '=https' --proto-redir '=https' \
--output "${RC_ARCHIVE}" "${RUSTFS_RC_ARCHIVE_URL}"
RC_ARCHIVE_SIZE="$(wc -c < "${RC_ARCHIVE}" | tr -d '[:space:]')"
if [ "${RC_ARCHIVE_SIZE}" -eq 0 ] || [ "${RC_ARCHIVE_SIZE}" -gt 33554432 ]; then
echo "RustFS CLI archive size is outside the accepted range: ${RC_ARCHIVE_SIZE}" >&2
exit 1
fi
if ! ACTUAL_ARCHIVE_SHA256="$(openssl dgst -sha256 -r "${RC_ARCHIVE}" | awk '{print $1}')"; then
echo "failed to calculate RustFS CLI archive SHA-256" >&2
exit 1
fi
if [ "${ACTUAL_ARCHIVE_SHA256}" != "${EXPECTED_ARCHIVE_SHA256}" ]; then
echo "RustFS CLI archive SHA-256 mismatch: expected ${EXPECTED_ARCHIVE_SHA256}, got ${ACTUAL_ARCHIVE_SHA256}" >&2
exit 1
fi
ARCHIVE_MEMBERS="$(tar -tzf "${RC_ARCHIVE}")"
if ! grep -Fxq 'rc' <<< "${ARCHIVE_MEMBERS}"; then
echo "RustFS CLI archive does not contain the rc entry" >&2
exit 1
fi
if ! tar -xOzf "${RC_ARCHIVE}" rc > "${RC_BIN}"; then
echo "failed to extract the RustFS CLI binary" >&2
exit 1
fi
chmod 0700 "${RC_BIN}"
if ! ACTUAL_RC_SHA256="$(openssl dgst -sha256 -r "${RC_BIN}" | awk '{print $1}')"; then
echo "failed to calculate RustFS CLI binary SHA-256" >&2
exit 1
fi
if [ "${ACTUAL_RC_SHA256}" != "${EXPECTED_RC_SHA256}" ]; then
echo "RustFS CLI binary SHA-256 mismatch: expected ${EXPECTED_RC_SHA256}, got ${ACTUAL_RC_SHA256}" >&2
exit 1
fi
if ! RC_VERSION_OUTPUT="$(timeout 30 "${RC_BIN}" --version 2>&1)"; then
echo "failed to execute the pinned RustFS CLI" >&2
exit 1
fi
RC_VERSION="${RC_VERSION_OUTPUT%%$'\n'*}"
if [ -z "${RC_VERSION}" ]; then
echo "pinned RustFS CLI returned an empty version" >&2
exit 1
fi
jq -n \
--arg schema_version '1' \
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg archive_url "${RUSTFS_RC_ARCHIVE_URL}" \
--arg archive_sha256 "${ACTUAL_ARCHIVE_SHA256}" \
--arg archive_size "${RC_ARCHIVE_SIZE}" \
--arg path "${RC_BIN}" \
--arg version "${RC_VERSION}" \
--arg sha256 "${ACTUAL_RC_SHA256}" \
'{
schema_version: ($schema_version | tonumber),
generated_at: $generated_at,
archive: {
url: $archive_url,
sha256: $archive_sha256,
size: ($archive_size | tonumber)
},
binary: {
path: $path,
version: $version,
sha256: $sha256
}
}' > "${TIER_ARTIFACTS_DIR}/rc-bootstrap.json"
printf 'path=%s\n' "${RC_BIN}" >> "${GITHUB_OUTPUT}"
echo "RustFS CLI ready: ${RC_VERSION} (${ACTUAL_RC_SHA256})"
- name: Show environment
run: |
uname -a
jq --version
openssl version
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
run: |
set -euo pipefail
sudo docker rm -f rustfs-test-mqtt >/dev/null 2>&1 || true
sudo rm -f /tmp/rustfs-mosquitto.conf
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms
'
done
- name: Ensure MQTT broker + clients
timeout-minutes: 5
run: |
set -euo pipefail
if ! command -v mosquitto_sub >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y mosquitto-clients
fi
command -v docker >/dev/null 2>&1 || { echo 'docker not found on runner'; exit 1; }
sudo docker rm -f rustfs-test-mqtt >/dev/null 2>&1 || true
cat <<'EOF' | sudo tee /tmp/rustfs-mosquitto.conf >/dev/null
listener 1883 0.0.0.0
allow_anonymous true
EOF
sudo docker run -d --name rustfs-test-mqtt -p 1883:1883 \
-v /tmp/rustfs-mosquitto.conf:/mosquitto/config/mosquitto.conf:ro \
eclipse-mosquitto:2 >/dev/null
for _ in {1..10}; do
if ss -tln 2>/dev/null | grep -q ':1883'; then
break
fi
sleep 1
done
ss -tln 2>/dev/null | grep -q ':1883' || {
echo 'mosquitto container is not listening on 1883'
sudo docker logs rustfs-test-mqtt || true
exit 1
}
- name: Run tier suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
env:
PACKAGE_URL_INPUT: ${{ inputs.package_url }}
RC_BIN: ${{ steps.rc.outputs.path }}
RUSTFS_VERSION_INPUT: ${{ inputs.rustfs_version }}
run: |
set -euo pipefail
LOG_FILE="${TIER_ARTIFACTS_DIR}/rustfs-tier.log"
chmod +x auto-testing/rustfs-tier-test.sh
PACKAGE_URL="${PACKAGE_URL_INPUT}"
RUSTFS_VERSION="${RUSTFS_VERSION_INPUT}"
ARGS=(
--all-topologies
-y
--log-file "${LOG_FILE}"
--rc-bin "${RC_BIN}"
--artifacts-dir "${TIER_ARTIFACTS_DIR}"
)
if [ -n "${RUSTFS_EXPECTED_RC_SHA256}" ]; then
ARGS+=(--expected-rc-sha256 "${RUSTFS_EXPECTED_RC_SHA256}")
fi
if [ -n "${PACKAGE_URL}" ]; then
ARGS+=(--package-url "${PACKAGE_URL}")
elif [ -n "${RUSTFS_VERSION}" ]; then
ARGS+=(--version "${RUSTFS_VERSION}")
else
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
./auto-testing/rustfs-tier-test.sh "${ARGS[@]}"
- name: Inject diagnostic case failure
if: ${{ always() && steps.evidence.outcome == 'success' && inputs.force_case_failure }}
run: |
set -euo pipefail
RESULT_FILE="${TIER_ARTIFACTS_DIR}/cases/single-single--TIER-101.json"
test -s "${RESULT_FILE}"
TMP_FILE="$(mktemp "${TIER_ARTIFACTS_DIR}/cases/.forced.XXXXXX")"
jq '.status = "FAIL" | .case_rc = 97' "${RESULT_FILE}" > "${TMP_FILE}"
mv "${TMP_FILE}" "${RESULT_FILE}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
env:
PACKAGE_URL_INPUT: ${{ inputs.package_url }}
RUSTFS_VERSION_INPUT: ${{ inputs.rustfs_version }}
TEST_OUTCOME: ${{ steps.test.outcome }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
TRIGGER_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
test -d "${TIER_ARTIFACTS_DIR}"
test ! -L "${TIER_ARTIFACTS_DIR}"
LOG_FILE="${TIER_ARTIFACTS_DIR}/rustfs-tier.log"
REPORT_FILE="${TIER_ARTIFACTS_DIR}/rustfs-tier-report.md"
CASE_TABLE="${TIER_ARTIFACTS_DIR}/rustfs-tier-cases.md"
GATE_RC_FILE="${TIER_ARTIFACTS_DIR}/rustfs-tier-gate.rc"
PACKAGE_URL="${PACKAGE_URL_INPUT}"
RUSTFS_VERSION="${RUSTFS_VERSION_INPUT}"
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
if RC_BOOTSTRAP_SUMMARY="$(jq -r '.binary | "\(.version) / \(.sha256)"' "${TIER_ARTIFACTS_DIR}/rc-bootstrap.json" 2>/dev/null)"; then
:
else
RC_BOOTSTRAP_SUMMARY="missing or invalid"
fi
set +e
python3 auto-testing/rustfs_tier_report.py \
--results-dir "${TIER_ARTIFACTS_DIR}/cases" \
--provenance "${TIER_ARTIFACTS_DIR}/provenance.json" \
--output "${CASE_TABLE}"
CASE_GATE_RC=$?
set -e
printf '%s\n' "${CASE_GATE_RC}" > "${GATE_RC_FILE}"
if [ ! -s "${CASE_TABLE}" ]; then
{
echo "## Case Summary"
echo ""
echo "Structured report generation failed before producing output (exit ${CASE_GATE_RC})."
} > "${CASE_TABLE}"
fi
# The tier case table carries a "## Case Summary" bullet list
# (Total/PASS/FAIL/...); parse the counts from there. Fall back to
# the case-ID table rows if the bullets are absent.
CASES_FAIL="$(grep -m1 -E '^- FAIL: [0-9]+' "${CASE_TABLE}" 2>/dev/null | grep -oE '[0-9]+' || true)"
if [ -n "${CASES_FAIL}" ]; then
CASES_PASS="$(grep -m1 -E '^- PASS: [0-9]+' "${CASE_TABLE}" 2>/dev/null | grep -oE '[0-9]+' || true)"
CASES_PASS=$(( ${CASES_PASS:-0} + 0 ))
else
CASES_TOTAL="$(grep -cE '^\| [A-Za-z0-9_.-]+ \| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Za-z0-9_.-]+ \| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
fi
CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
{
echo "# RustFS tier test report"
echo ""
echo "- Run: ${RUN_URL}"
echo "- Trigger: ${TRIGGER_NAME}"
echo "- Package: ${PACKAGE_SOURCE}"
echo "- Client bootstrap: ${RC_BOOTSTRAP_SUMMARY}"
echo "- Test Step Outcome: ${TEST_OUTCOME}"
echo "- Structured Gate Exit: ${CASE_GATE_RC}"
echo ""
cat "${CASE_TABLE}"
echo ""
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" || true
echo '```'
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
REPORT_FILE: ${{ env.TIER_ARTIFACTS_DIR }}/rustfs-tier-report.md
SUITE: tier
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Verify required tier evidence
timeout-minutes: 2
id: evidence_verify
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
failed=0
for name in \
rustfs-tier.log \
rustfs-tier-report.md \
rustfs-tier-cases.md \
rustfs-tier-gate.rc \
rc-bootstrap.json \
provenance.json; do
if [ ! -s "${TIER_ARTIFACTS_DIR}/${name}" ]; then
echo "required tier evidence is missing or empty: ${name}" >&2
failed=1
fi
done
for name in cases logs; do
if [ ! -d "${TIER_ARTIFACTS_DIR}/${name}" ]; then
echo "required tier evidence directory is missing: ${name}" >&2
failed=1
fi
done
if ! find "${TIER_ARTIFACTS_DIR}/cases" -maxdepth 1 -type f -name '*.json' -print -quit 2>/dev/null | grep -q .; then
echo "no atomic tier case result was produced" >&2
failed=1
fi
[ "${failed}" -eq 0 ]
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-tier-test-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ env.TIER_ARTIFACTS_DIR }}/
if-no-files-found: error
- name: Cleanup environment (after)
timeout-minutes: 5
if: always()
run: |
set -euo pipefail
sudo docker rm -f rustfs-test-mqtt >/dev/null 2>&1 || true
sudo rm -f /tmp/rustfs-mosquitto.conf
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms
'
done
- name: Cleanup pinned RustFS CLI
timeout-minutes: 2
if: always()
run: |
set -euo pipefail
RC_ROOT="${RUNNER_TEMP}/rustfs-tier-rc-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
rm -f -- "${RC_ROOT}/rustfs-cli.tar.gz" "${RC_ROOT}/rc"
if [ -d "${RC_ROOT}" ]; then
rmdir -- "${RC_ROOT}"
fi
- name: Enforce tier suite result
id: gate
if: always()
env:
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
TEST_OUTCOME: ${{ steps.test.outcome }}
GATE_RC_FILE: ${{ env.TIER_ARTIFACTS_DIR }}/rustfs-tier-gate.rc
run: |
set -euo pipefail
failed=0
if [ "${EVIDENCE_OUTCOME}" != "success" ]; then
echo "tier evidence directory initialization is ${EVIDENCE_OUTCOME}, expected success" >&2
failed=1
fi
if [ "${TEST_OUTCOME}" != "success" ]; then
echo "tier suite step outcome is ${TEST_OUTCOME}, expected success" >&2
failed=1
fi
if [ "${EVIDENCE_OUTCOME}" != "success" ]; then
echo "structured gate result is unavailable because evidence initialization failed" >&2
elif [ ! -s "${GATE_RC_FILE}" ]; then
echo "structured gate result is missing" >&2
failed=1
else
GATE_RC="$(tr -d '[:space:]' < "${GATE_RC_FILE}")"
if ! [[ "${GATE_RC}" =~ ^[0-9]+$ ]] || [ "${GATE_RC}" -ne 0 ]; then
echo "structured 56-case gate failed with exit ${GATE_RC:-invalid}" >&2
failed=1
fi
fi
[ "${failed}" -eq 0 ]
- name: File failure issue in rustfs/backlog
timeout-minutes: 2
if: ${{ always() && (steps.evidence.outcome != 'success' || steps.test.outcome == 'cancelled' || steps.evidence_verify.outcome == 'failure' || steps.evidence_verify.outcome == 'cancelled' || (steps.gate.outcome == 'failure' && steps.test.outcome != 'failure') || steps.gate.outcome == 'cancelled') }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'tier'
SUITE_LABEL: 'Tier'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVIDENCE_DIR: ${{ env.TIER_ARTIFACTS_DIR }}
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
VERIFY_OUTCOME: ${{ steps.evidence_verify.outcome }}
GATE_OUTCOME: ${{ steps.gate.outcome }}
REPORT_FILE: ${{ env.TIER_ARTIFACTS_DIR }}/rustfs-tier-report.md
LOG_FILE: ${{ env.TIER_ARTIFACTS_DIR }}/rustfs-tier.log
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue"
exit 0
fi
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID}, attempt ${GITHUB_RUN_ATTEMPT})"
EXISTING="$(gh issue list -R rustfs/backlog --state all \
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
--json number,title --jq ".[] | select(.title == \"${TITLE}\") | .number")"
if [ -n "${EXISTING}" ]; then
echo "backlog issue already exists for run ${GITHUB_RUN_ID}; skipping"
exit 0
fi
redact() {
sed -E \
-e 's/(RUSTFS_(ACCESS_KEY|SECRET_KEY)[=: ]+)[^[:space:]]+/\1[REDACTED]/Ig' \
-e 's/(Authorization:).*/\1 [REDACTED]/Ig' \
-e 's/(X-Amz-Signature=)[^&[:space:]]+/\1[REDACTED]/Ig' \
-e 's/^.*(password|secret|token)[=: ].*/[REDACTED SENSITIVE LINE]/Ig'
}
BODY_FILE="$(mktemp)"
{
echo "The **${SUITE_LABEL}** functional suite failed."
echo ""
echo "- Suite: \`${SUITE}\`"
echo "- Run: ${RUN_URL}"
echo "- Trigger: ${GITHUB_EVENT_NAME}"
echo "- Date: $(date -u +%Y-%m-%d)"
echo "- Evidence initialization: ${EVIDENCE_OUTCOME}"
echo "- Evidence verification: ${VERIFY_OUTCOME}"
echo "- Final gate: ${GATE_OUTCOME}"
echo ""
echo "## Report (errors and symptoms)"
echo ""
if [ "${EVIDENCE_OUTCOME}" != "success" ]; then
echo "(the run evidence directory was rejected; its contents were not read)"
elif [ ! -d "${EVIDENCE_DIR}" ] || [ -L "${EVIDENCE_DIR}" ]; then
echo "(the run evidence directory is missing or unsafe; its contents were not read)"
elif [ -s "${REPORT_FILE}" ]; then
redact < "${REPORT_FILE}"
elif [ -s "${LOG_FILE:-}" ]; then
echo "(report file missing; log tail below)"
echo ""
tail -n 200 "${LOG_FILE}" | redact
else
echo "(no report or log file was produced)"
fi
} | head -c 55000 > "${BODY_FILE}"
gh label create functional-test -R rustfs/backlog --color d73a4a 2>/dev/null || true
if ! gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test; then
gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}"
fi
echo "filed backlog issue for suite ${SUITE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Case-level issue management complements the infrastructure failure report:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
TEST_OUTCOME: ${{ steps.test.outcome }}
VERIFY_OUTCOME: ${{ steps.evidence_verify.outcome }}
GATE_OUTCOME: ${{ steps.gate.outcome }}
SUITE: 'tier'
SUITE_LABEL: 'Tier'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
if [ ! -d "${TIER_ARTIFACTS_DIR}" ] || [ -L "${TIER_ARTIFACTS_DIR}" ]; then
echo "tier evidence directory is missing or unsafe; skipping backlog issue management" >&2
exit 1
fi
OUTCOME="${TEST_OUTCOME}"
if [ "${OUTCOME}" != "cancelled" ] && { [ "${VERIFY_OUTCOME}" != "success" ] || [ "${GATE_OUTCOME}" != "success" ]; }; then
OUTCOME=failure
fi
PACKAGE_URL='${{ inputs.package_url }}'
RUSTFS_VERSION='${{ inputs.rustfs_version }}'
PACKAGE_SOURCE=""
if [ -n "${PACKAGE_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_URL}"
elif [ -n "${RUSTFS_VERSION}" ]; then
PACKAGE_SOURCE="version ${RUSTFS_VERSION}"
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${OUTCOME}" \
--report "${TIER_ARTIFACTS_DIR}/rustfs-tier-cases.md" \
--report-file "${TIER_ARTIFACTS_DIR}/rustfs-tier-report.md" \
--log "${TIER_ARTIFACTS_DIR}/rustfs-tier.log" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: "Continue functional chain (next: Storage engine)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-storage' \
-F 'client_payload[from_suite]=tier'; then
echo "dispatched next suite Storage engine (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch Storage engine after 3 attempts" >&2
TITLE="[functional][chain] stalled after tier (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **tier** to **Storage engine** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-storage'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-storage'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS tier suite failed"
echo "See the uploaded report and log artifacts for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite tier
--report "${TIER_ARTIFACTS_DIR}/rustfs-tier-cases.md"
--output "${RUNNER_TEMP}/chain-tier-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/tier.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-tier-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-tier-${{ github.run_id }}-${{ github.run_attempt }}/tier.json
if-no-files-found: error
+510
View File
@@ -0,0 +1,510 @@
# Copyright 2024 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.
name: RustFS Upgrade Test
on:
workflow_call:
inputs:
chain_manifest:
description: Verified candidate and chain attempt from the chain driver
type: string
required: true
workflow_dispatch:
inputs:
from_version:
description: 'OLD RustFS release tag, e.g. 1.0.0-rc.3 (its release must ship a .deb asset). Leave empty for the default.'
required: false
default: '1.0.0-rc.3'
from_url:
description: 'OLD .deb URL. Overrides from_version.'
required: false
type: string
to_version:
description: 'NEW RustFS release tag, e.g. 1.0.0-rc.5 (any version with a .deb asset). Leave empty for latest nightly.'
required: false
to_url:
description: 'NEW .deb URL. Overrides to_version / nightly default.'
required: false
type: string
topology:
description: 'Topology to run (all = SNSD, SNMD, MNMD)'
type: choice
options:
- all
- single-single
- single-multi
- multi-multi
default: all
backends:
description: 'KMS backends to run (local,vault-kv2)'
required: false
default: 'local,vault-kv2'
cleanup_before:
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
type: boolean
default: true
cleanup_after:
description: 'Reset the nodes after the test (DESTROYS test data/config)'
type: boolean
default: true
repository_dispatch:
# Functional-chain entry: dispatched by rustfs-functional-chain.yml.
types: [rustfs-chain-upgrade]
permissions:
contents: read
concurrency:
group: rustfs-shared-functional-tests-v2
cancel-in-progress: false
defaults:
run:
shell: bash
env:
RUSTFS_ACCESS_KEY: ${{ secrets.RUSTFS_ACCESS_KEY }}
RUSTFS_SECRET_KEY: ${{ secrets.RUSTFS_SECRET_KEY }}
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
upgrade-test:
runs-on: smoke-testing
timeout-minutes: 60
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
steps:
- name: Checkout repository (for report parser)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Initialize functional evidence
id: evidence
run: |
set -euo pipefail
umask 077
FUNCTIONAL_ARTIFACTS_DIR="${RUNNER_TEMP}/rustfs-upgrade-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -- "${FUNCTIONAL_ARTIFACTS_DIR}" "${FUNCTIONAL_ARTIFACTS_DIR}-scratch"
{
printf 'FUNCTIONAL_ARTIFACTS_DIR=%s\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'LOG_FILE=%s/suite.log\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'REPORT_FILE=%s/report.md\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
printf 'TMPDIR=%s-scratch\n' "${FUNCTIONAL_ARTIFACTS_DIR}"
} >> "${GITHUB_ENV}"
- name: Bind functional candidate
id: chain
if: ${{ inputs.chain_manifest != '' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
run: python3 scripts/functional_chain_evidence.py consume
- name: Checkout auto-testing scripts
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/auto-testing
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
path: auto-testing
persist-credentials: false
- name: Show environment
run: |
uname -a
jq --version
openssl version
aws --version || true
docker --version || true
df -h /data | tail -1
- name: Cleanup environment (before)
timeout-minutes: 5
if: ${{ inputs.cleanup_before != 'false' || github.event_name != 'workflow_dispatch' }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: Ensure docker (Vault container)
timeout-minutes: 5
run: |
if ! command -v docker >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y docker.io
fi
sudo systemctl enable --now docker
docker info >/dev/null 2>&1 || sudo docker info >/dev/null 2>&1
- name: Run upgrade compatibility suite
id: test
timeout-minutes: 45
# Case failures keep the run green: the report and the backlog
# issue manager carry the product signal.
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -euo pipefail
chmod +x auto-testing/rustfs-upgrade-test.sh
FROM_URL='${{ inputs.from_url }}'
FROM_VERSION='${{ inputs.from_version }}'
TO_URL='${{ inputs.to_url }}'
TO_VERSION='${{ inputs.to_version }}'
TOPOLOGY='${{ inputs.topology }}'
BACKENDS='${{ inputs.backends }}'
ARGS=(-y --log-file "${LOG_FILE}")
if [ "${TOPOLOGY}" = "all" ] || [ -z "${TOPOLOGY}" ] || [ "${TOPOLOGY}" = "null" ]; then
ARGS+=(--all-topologies)
else
ARGS+=(--topology "${TOPOLOGY}")
fi
if [ -n "${BACKENDS}" ] && [ "${BACKENDS}" != "null" ]; then
ARGS+=(--backends "${BACKENDS}")
fi
if [ -n "${FROM_URL}" ]; then
ARGS+=(--from-url "${FROM_URL}")
elif [ -n "${FROM_VERSION}" ] && [ "${FROM_VERSION}" != "null" ]; then
ARGS+=(--from-version "${FROM_VERSION}")
fi
if [ -n "${TO_URL}" ]; then
ARGS+=(--to-url "${TO_URL}")
elif [ -n "${TO_VERSION}" ] && [ "${TO_VERSION}" != "null" ]; then
ARGS+=(--to-version "${TO_VERSION}")
else
ARGS+=(--to-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
fi
# Fail fast with a clear message when a requested release tag has
# no .deb asset (e.g. 1.0.0-rc.4 ships only zips), instead of
# letting the suite die mid-run on a 404.
check_release_asset() {
local version="$1" tag asset url
[ -n "${version}" ] && [ "${version}" != "null" ] || return 0
tag="${version#v}"
asset="rustfs_${tag//-/.}_amd64.deb"
url="https://github.com/rustfs/rustfs/releases/download/${tag}/${asset}"
if ! gh api "repos/rustfs/rustfs/releases/tags/${tag}" --jq '.assets[].name' 2>/dev/null | grep -qxF "${asset}"; then
echo "ERROR: release ${tag} has no downloadable asset ${asset}:" >&2
echo " ${url}" >&2
echo "Pick a tag whose release ships a .deb (check its release assets)." >&2
exit 1
fi
echo "resolved ${tag} -> ${url}"
}
if [ -z "${FROM_URL}" ]; then
check_release_asset "${FROM_VERSION}"
fi
if [ -z "${TO_URL}" ]; then
check_release_asset "${TO_VERSION}"
fi
./auto-testing/rustfs-upgrade-test.sh "${ARGS[@]}"
- name: Generate report
id: chain_report
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
run: |
set -euo pipefail
FROM_URL='${{ inputs.from_url }}'
FROM_VERSION='${{ inputs.from_version }}'
TO_URL='${{ inputs.to_url }}'
TO_VERSION='${{ inputs.to_version }}'
if [ -n "${FROM_URL}" ]; then
FROM_SOURCE="${FROM_URL}"
elif [ -n "${FROM_VERSION}" ]; then
FROM_SOURCE="version ${FROM_VERSION}"
else
FROM_SOURCE="release (default)"
fi
if [ -n "${TO_URL}" ]; then
TO_SOURCE="${TO_URL}"
elif [ -n "${TO_VERSION}" ]; then
TO_SOURCE="version ${TO_VERSION}"
else
TO_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
CASE_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
MATRIX_TABLE="${FUNCTIONAL_ARTIFACTS_DIR}/matrix.md"
CASE_RESULT=success
python3 scripts/functional_case_report.py "${LOG_FILE}" "${CASE_TABLE}" "${MATRIX_TABLE}" || CASE_RESULT=failure
RESULT=failure
if [ '${{ steps.test.outcome }}' = 'success' ] && [ "${CASE_RESULT}" = 'success' ]; then
RESULT=success
fi
# Product gate: failing cases keep the run green — they are reported
# below and tracked in rustfs/backlog. Only harness/environment
# breakdowns (the suite never reached case level, or a wholesale
# failure with zero passes) turn the workflow red.
CASES_TOTAL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| (PASS|FAIL|UNSUPPORTED|RUNNING) \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_FAIL="$(grep -cE '^\| [A-Z][A-Z0-9]*-[0-9]+ .*\| FAIL \|' "${CASE_TABLE}" 2>/dev/null || true)"
CASES_TOTAL=$(( ${CASES_TOTAL:-0} + 0 )); CASES_FAIL=$(( ${CASES_FAIL:-0} + 0 ))
CASES_PASS=$(( CASES_TOTAL - CASES_FAIL ))
HARNESS_OK=0
if [ '${{ steps.test.outcome }}' = 'success' ]; then
HARNESS_OK=1
elif [ '${{ steps.test.outcome }}' = 'failure' ] && [ "${CASES_TOTAL}" -gt 0 ] && [ "${CASES_PASS}" -ge 1 ]; then
HARNESS_OK=1
fi
{
echo "# RustFS upgrade compatibility report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Attempt: ${GITHUB_RUN_ATTEMPT}"
echo "- Workflow Commit: ${GITHUB_SHA}"
echo "- Trigger: ${{ github.event_name }}"
echo "- From: ${FROM_SOURCE}"
echo "- To: ${TO_SOURCE}"
echo "- Test Step Outcome: ${RESULT}"
echo "- Suite Step Outcome: ${{ steps.test.outcome }}"
echo ""
if [ -s "${MATRIX_TABLE}" ]; then
cat "${MATRIX_TABLE}"
echo ""
fi
if [ -s "${CASE_TABLE}" ]; then
cat "${CASE_TABLE}"
echo ""
fi
echo "- Product result: ${CASES_PASS} passed, ${CASES_FAIL} failed (failing cases are tracked in rustfs/backlog)"
echo ""
if [ -s "${CASE_TABLE}" ]; then
echo "## Log tail"
echo '```text'
tail -n 200 "${LOG_FILE}" 2>/dev/null || true
echo '```'
else
echo "The suite or evidence validation failed. See this run's artifact for partial case results and suite.log."
fi
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
# Red only for harness/environment breakdowns; case failures stay green.
[ "${HARNESS_OK}" = "1" ]
- name: Upload functional report to dashboard
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
SUITE: upgrade
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
exit 0
fi
DATE="$(date -u +%Y-%m-%d)"
REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
# Base64-encode the report into a temp file and feed it to jq via
# --rawfile: large reports (e.g. pool) exceed the OS argv limit and
# make `jq --arg content "${CONTENT}"` fail with "Argument list too long".
B64_FILE="$(mktemp)"
python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}" > "${B64_FILE}"
SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
if [ -n "${SHA}" ]; then
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" --arg sha "${SHA}" \
'{message:$msg, content:($content|rtrimstr("\n")), sha:$sha}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
else
jq -n --arg msg "report(${SUITE}): ${DATE}" --rawfile content "${B64_FILE}" \
'{message:$msg, content:($content|rtrimstr("\n"))}' \
| gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
fi
rm -f "${B64_FILE}"
- name: Manage backlog issues (dedup / label / auto-close)
timeout-minutes: 2
# Replaces the old per-run failure filing. One entry point that:
# - dedups by signal: failing cases are matched against open backlog
# issues by label (category + case ID); covered cases become a
# comment on the existing issue, only uncovered cases file a new one
# - labels new issues (functional-test, category, case IDs, env)
# - closes fixed issues after a fully green run
# - never files or closes on cancelled runs
if: ${{ always() && steps.evidence.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
SUITE: 'upgrade'
SUITE_LABEL: 'Upgrade compatibility'
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; skipping backlog issue management"
exit 0
fi
if [ ! -f auto-testing/scripts/issue_manager.py ]; then
echo "issue_manager.py not found in auto-testing checkout; skipping"
exit 0
fi
FROM_URL='${{ inputs.from_url }}'
FROM_VERSION='${{ inputs.from_version }}'
TO_URL='${{ inputs.to_url }}'
TO_VERSION='${{ inputs.to_version }}'
PACKAGE_SOURCE=""
if [ -n "${TO_URL}" ]; then
PACKAGE_SOURCE="to ${TO_URL}"
elif [ -n "${TO_VERSION}" ]; then
PACKAGE_SOURCE="to version ${TO_VERSION}"
else
PACKAGE_SOURCE="to ${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
if [ -n "${FROM_URL}" ]; then
PACKAGE_SOURCE="${PACKAGE_SOURCE}; from ${FROM_URL}"
elif [ -n "${FROM_VERSION}" ]; then
PACKAGE_SOURCE="${PACKAGE_SOURCE}; from version ${FROM_VERSION}"
fi
python3 auto-testing/scripts/issue_manager.py handle \
--repo rustfs/backlog \
--suite "${SUITE}" --category "${SUITE}" --suite-label "${SUITE_LABEL}" \
--outcome "${{ steps.test.outcome }}" \
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md" \
--report-file "${REPORT_FILE}" \
--log "${LOG_FILE}" \
--run-url "${RUN_URL}/attempts/${GITHUB_RUN_ATTEMPT}#summary" \
--run-id "${GITHUB_RUN_ID}" \
--attempt "${GITHUB_RUN_ATTEMPT}" \
--commit "${GITHUB_SHA}" \
--trigger "${{ github.event_name }}" \
--package-source "${PACKAGE_SOURCE}" \
--date "$(date -u +%Y-%m-%d)"
- name: Upload report and logs
timeout-minutes: 2
if: ${{ always() && steps.evidence.outcome == 'success' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-upgrade-test-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/report.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/suite.log
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/cases.md
${{ env.FUNCTIONAL_ARTIFACTS_DIR }}/matrix.md
if-no-files-found: error
retention-days: 3
- name: Cleanup environment (after)
timeout-minutes: 5
if: ${{ always() && (inputs.cleanup_after != 'false' || github.event_name != 'workflow_dispatch') }}
run: |
set -euo pipefail
read -r -a NODES <<< "${RUSTFS_NODES:-vm000 vm001 vm002}"
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
for node in "${NODES[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${SSH_USER}@${node}" '
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} systemctl stop rustfs 2>/dev/null || true
if ${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
${SUDO} dpkg -P rustfs
fi
for i in 1 2 3 4; do ${SUDO} rm -rf /data/rustfs${i}/mnmd; done
${SUDO} rm -rf /var/log/rustfs /var/lib/rustfs/kms /var/lib/rustfs/kms-backup
'
done
- name: "Continue functional chain (next: S3 compatibility)"
# Only chain-triggered runs forward to the next suite; standalone
# workflow_dispatch runs stop after their own cleanup. A failed
# handoff must never pass silently: it retries, then files an alert
# issue in rustfs/backlog so a stalled chain is visible.
if: ${{ always() && github.event_name == 'repository_dispatch' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
run: |
set -uo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "PF_TESTING_GH_TOKEN is not configured; cannot dispatch the next suite" >&2
exit 1
fi
DISPATCHED=0
for attempt in 1 2 3; do
if gh api --method POST repos/rustfs/rustfs/dispatches \
-f event_type='rustfs-chain-s3' \
-F 'client_payload[from_suite]=upgrade'; then
echo "dispatched next suite S3 compatibility (attempt ${attempt})"
DISPATCHED=1
break
fi
echo "dispatch attempt ${attempt} failed; retrying in ${attempt}0s" >&2
sleep "${attempt}0"
done
if [ "${DISPATCHED:-0}" -ne 1 ]; then
echo "ERROR: functional chain stalled: could not dispatch S3 compatibility after 3 attempts" >&2
TITLE="[functional][chain] stalled after upgrade (run ${GITHUB_RUN_ID})"
BODY_FILE="$(mktemp)"
{
echo "The functional chain could not hand off from **upgrade** to **S3 compatibility** after 3 attempts."
echo ""
echo "- Failed suite job: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "- Expected next event: 'rustfs-chain-s3'"
echo "- Likely cause: PF_TESTING_GH_TOKEN lacks contents:write on rustfs/rustfs, or the GitHub API was unavailable."
echo "- Recovery: re-dispatch manually with"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
echo " gh api --method POST repos/rustfs/rustfs/dispatches -f event_type='rustfs-chain-s3'"
FENCE="$(printf "\x60\x60\x60")"; echo " ${FENCE}"
} > "${BODY_FILE}"
gh issue create -R rustfs/backlog --title "${TITLE}" \
--body-file "${BODY_FILE}" --label functional-test \
|| gh issue create -R rustfs/backlog --title "${TITLE}" --body-file "${BODY_FILE}" \
|| echo "could not file the stall alert issue either; check the token" >&2
exit 1
fi
- name: Notify on failure
if: failure()
run: |
echo "RustFS upgrade compatibility test failed"
echo "From: ${{ inputs.from_url || inputs.from_version || 'release (default)' }}"
echo "To: ${{ inputs.to_url || inputs.to_version || 'nightly (R2 latest)' }}"
echo "See the uploaded report and logs for details."
- name: Record chain evidence
id: chain_record
if: ${{ always() && inputs.chain_manifest != '' && steps.evidence.outcome == 'success' }}
env:
CHAIN_MANIFEST: ${{ inputs.chain_manifest }}
CHAIN_JOB_STATUS: ${{ job.status }}
CHAIN_TEST_OUTCOME: ${{ steps.test.outcome }}
CHAIN_REPORT_OUTCOME: ${{ steps.chain_report.outcome }}
run: >-
python3 scripts/functional_chain_evidence.py record --suite upgrade
--report "${FUNCTIONAL_ARTIFACTS_DIR}/cases.md"
--output "${RUNNER_TEMP}/chain-upgrade-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/upgrade.json"
- name: Upload chain evidence
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: functional-chain-upgrade-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/chain-upgrade-${{ github.run_id }}-${{ github.run_attempt }}/upgrade.json
if-no-files-found: error
@@ -19,7 +19,9 @@
# 'failure')` wiring used by the real consumers (e2e-s3tests, mint, fuzz,
# performance-ab). Dispatch it twice to verify both the issue-creation and
# the dedupe-comment paths, then close the resulting
# "[scheduled-failure] Schedule Failure Alert Drill" issue.
# "[scheduled-failure] Schedule Failure Alert Drill" issue in rustfs/backlog
# (the default target repository). Requires the BACKLOG_ISSUE_TOKEN secret
# (a token with issues:write on rustfs/backlog).
#
# The run itself is expected to end red (the forced failure); only the
# alert-on-failure job result matters.
@@ -68,4 +70,4 @@ jobs:
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}

Some files were not shown because too many files have changed in this diff Show More