mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 20:46:11 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cbe4a8465 |
@@ -1,33 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: adversarial-validation
|
name: adversarial-validation
|
||||||
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.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# RustFS Adversarial Validation
|
# RustFS Adversarial Validation
|
||||||
|
|
||||||
Use the [repository risk tiers and review shape](../../references/adversarial-validation.md). This skill
|
Use the risk tier and review shape defined in the root `AGENTS.md`. This skill
|
||||||
routes a review to RustFS-specific probes without loading unrelated domains.
|
routes a review to RustFS-specific probes without loading unrelated domains.
|
||||||
|
|
||||||
## Select Lenses
|
## Select Lenses
|
||||||
@@ -31,18 +31,15 @@ adversarial review.
|
|||||||
|
|
||||||
## Review Protocol
|
## Review Protocol
|
||||||
|
|
||||||
1. Freeze the exact final diff/head (or the design under review) and list the
|
1. Freeze the exact final diff/head and list the selected lenses.
|
||||||
selected lenses.
|
2. Run the review shape required by root `AGENTS.md`.
|
||||||
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
|
3. For each selected lens, either report a concrete finding or a null verdict
|
||||||
naming the attacks performed.
|
naming the attacks performed.
|
||||||
4. Apply root `AGENTS.md`'s finding standard. Test each candidate against callers,
|
4. A finding needs `file:line`, a triggering input/state/interleaving, the wrong
|
||||||
existing coverage, and invariants before accepting it; an adversarial role
|
outcome, and a focused fix or missing regression check.
|
||||||
does not have to produce a defect.
|
5. Fix or rebut every finding with code-path, test, or invariant evidence.
|
||||||
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
|
6. After a non-trivial edit, rerun only lenses affected by that edit against the
|
||||||
new exact diff.
|
new exact diff.
|
||||||
|
|
||||||
Do not turn a null verdict into a long checklist. Record concise evidence that
|
Do not turn a null verdict into a long checklist. Record concise evidence that
|
||||||
the relevant failure classes were attacked, then stop under the root completion
|
the relevant failure classes were attacked.
|
||||||
rule. Keep the required per-lens verdicts for high-risk PRs.
|
|
||||||
|
|||||||
@@ -22,17 +22,3 @@
|
|||||||
fixtures and encrypted migration data.
|
fixtures and encrypted migration data.
|
||||||
- Compatibility shims use `RUSTFS_COMPAT_TODO(<task-id>)`, have a removal
|
- Compatibility shims use `RUSTFS_COMPAT_TODO(<task-id>)`, have a removal
|
||||||
condition, and default toward reading old data safely.
|
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,10 +3,9 @@
|
|||||||
- For every behavior claim, name the focused test/check that fails if the
|
- 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
|
changed hunk is reverted. If none is practical, require the reason and
|
||||||
residual risk.
|
residual risk.
|
||||||
- Confirm tests exercise the real production path and distinguish the intended
|
- Confirm tests exercise the real production path and assert returned values,
|
||||||
behavior from the named regression. A success, `is_err()`, or no-panic check
|
exact bytes, stored state, or the specific error variant—not only success,
|
||||||
can be sufficient when that is the actual contract; require exact values,
|
`is_err()`, or no panic.
|
||||||
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
|
- For new flags/modes, verify each branch and ask which test fails if the branch
|
||||||
is inverted.
|
is inverted.
|
||||||
- For new error propagation, inject the failure and assert the caller observes
|
- For new error propagation, inject the failure and assert the caller observes
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: arch-checks
|
name: arch-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.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Architecture Guard Checks
|
# Architecture Guard Checks
|
||||||
|
|
||||||
Read only the section for the failing guard. Use `.config/make/` and the current
|
All five run in `make pre-commit` / `make pre-pr` and in CI. Fix the cause;
|
||||||
workflow to verify its wiring; not every guard is part of every gate. Fix the
|
never weaken a check to get green.
|
||||||
cause and rerun the failed guard; never weaken a check to get green.
|
|
||||||
|
|
||||||
## `check_layer_dependencies.sh` — layer DAG in `rustfs/src`
|
## `check_layer_dependencies.sh` — layer DAG in `rustfs/src`
|
||||||
|
|
||||||
@@ -51,12 +50,10 @@ consider adding it to the script's `checked_files` list.
|
|||||||
|
|
||||||
## `check_doc_paths.sh`
|
## `check_doc_paths.sh`
|
||||||
|
|
||||||
Instruction docs (`AGENTS.md`, `CLAUDE.md`, `ARCHITECTURE.md`) and every
|
Instruction/architecture docs (`AGENTS.md`, `CLAUDE.md`, `ARCHITECTURE.md`,
|
||||||
Markdown file under `docs/` (architecture, operations, testing, index) must not
|
`docs/architecture/*.md`) must not reference repo file paths that no longer
|
||||||
reference repo file paths that no longer exist. If your refactor moved code,
|
exist. If your refactor moved code, update the docs that point at it — the
|
||||||
update the docs that point at it — the error message lists `doc -> stale-path`
|
error message lists `doc -> stale-path` pairs.
|
||||||
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`
|
## `check_no_planning_docs.sh`
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ 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
|
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.
|
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
|
## Core Workflow
|
||||||
|
|
||||||
### 1) Scope and assumptions
|
### 1) Scope and assumptions
|
||||||
- Derive the change source, target branch, and relevant runtime/version from the
|
- Confirm change source (diff, commit, PR, files), target branch, language/runtime, and version.
|
||||||
supplied diff and metadata. Ask only when missing context could change the verdict.
|
- If context is missing, state assumptions before deeper analysis.
|
||||||
- Focus only on requested scope; avoid reviewing unrelated files.
|
- Focus only on requested scope; avoid reviewing unrelated files.
|
||||||
|
|
||||||
### 2) Risk map
|
### 2) Risk map
|
||||||
@@ -32,20 +40,43 @@ for adversarial validation, use `adversarial-validation` instead of running both
|
|||||||
- unchecked assumptions and null/empty/error-path handling
|
- unchecked assumptions and null/empty/error-path handling
|
||||||
- stale tests, fixtures, and configs
|
- stale tests, fixtures, and configs
|
||||||
- hidden coupling to shared helpers/constants/features
|
- hidden coupling to shared helpers/constants/features
|
||||||
- Apply root `AGENTS.md`'s finding standard: try to disprove a candidate before
|
- If a point is uncertain, mark it as an open question instead of guessing.
|
||||||
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
|
#### Rust-specific checks (apply to all Rust changes)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### 4) Findings-first output
|
### 4) Findings-first output
|
||||||
- Order supported findings by P0–P3 severity; preserve the Rust ratings above.
|
- Order findings by severity:
|
||||||
Include `path:line`, the failure and impact, a focused fix, and its validation.
|
- P0: critical failure, security breach, or data loss risk
|
||||||
- If no supported issues remain, state `No findings` with the reviewed scope and
|
- P1: high-impact regression
|
||||||
any material verification limitation. Do not append optional improvements to
|
- P2: medium risk correctness gap
|
||||||
make a clean review look productive.
|
- 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.
|
||||||
|
|
||||||
Close after the required review. Recommend additional verification only for an
|
### 5) Close
|
||||||
identified unresolved risk or required gate; reuse evidence for unchanged code.
|
- 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: ...
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
name: plugin-contract-guard
|
||||||
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.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Plugin & Extension Contract Guard
|
# Plugin & Extension Contract Guard
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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."
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: rust-code-quality
|
name: rust-code-quality
|
||||||
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.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Rust Code Quality Gate
|
# Rust Code Quality Gate
|
||||||
@@ -8,18 +8,12 @@ description: Run a focused Rust quality review when the user requests one or a s
|
|||||||
Use this skill for a dedicated Rust review to cover rules that `cargo clippy`
|
Use this skill for a dedicated Rust review to cover rules that `cargo clippy`
|
||||||
does not catch.
|
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
|
## Quick Start
|
||||||
|
|
||||||
1. Identify changed `.rs` files.
|
1. Identify changed `.rs` files.
|
||||||
2. Run the matching candidate searches on changed files.
|
2. Run automated checks on changed files.
|
||||||
3. Apply the manual checklist sections whose behavior the diff touches.
|
3. Run manual review checklist on the diff.
|
||||||
4. Report or rebut every finding with evidence; P0/P1 findings block approval.
|
4. Resolve or rebut every finding with evidence; P0/P1 findings cannot be deferred.
|
||||||
Fix them when implementation is authorized; a read-only review reports them.
|
|
||||||
|
|
||||||
## Automated Checks
|
## Automated Checks
|
||||||
|
|
||||||
@@ -41,7 +35,7 @@ rg -n 'Result<.*String>' <changed-files>
|
|||||||
rg -n 'Box<dyn.*Error' <changed-files>
|
rg -n 'Box<dyn.*Error' <changed-files>
|
||||||
|
|
||||||
# 5. println/eprintln in production
|
# 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)
|
# 6. Ordering::Relaxed usage (verify each is intentional)
|
||||||
rg -n 'Ordering::Relaxed' <changed-files>
|
rg -n 'Ordering::Relaxed' <changed-files>
|
||||||
@@ -86,7 +80,7 @@ For the Rust diff under review, verify:
|
|||||||
- [ ] Test volume and line count are never treated as production-code growth
|
- [ ] Test volume and line count are never treated as production-code growth
|
||||||
|
|
||||||
### Serde
|
### Serde
|
||||||
- [ ] Structs from untrusted input reject unknown fields where the compatibility contract permits; otherwise validate security-critical fields explicitly and test the supported input shape
|
- [ ] Structs from untrusted input have `#[serde(deny_unknown_fields)]`
|
||||||
- [ ] `#[serde(default)]` not used on security-critical fields without validation
|
- [ ] `#[serde(default)]` not used on security-critical fields without validation
|
||||||
|
|
||||||
### Code Hygiene
|
### Code Hygiene
|
||||||
@@ -110,7 +104,20 @@ For the Rust diff under review, verify:
|
|||||||
|
|
||||||
## Output Template
|
## Output Template
|
||||||
|
|
||||||
Use the calling review's output format. For a standalone review, report supported
|
```
|
||||||
findings with severity, location, impact, fix, and validation, or `No findings`.
|
## Rust Code Quality Report
|
||||||
Include only material unverified checks. Candidate counts are not a quality
|
|
||||||
metric and do not need a separate scan 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)
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: rustfs-release-publish
|
name: rustfs-release-publish
|
||||||
description: "Run the end-to-end RustFS console gate, version bump, preview validation, human confirmation, and final-tag publication pipeline. Use only when the user explicitly asks to release or publish a RustFS version (发版/发布)."
|
description: "Run the end-to-end RustFS console gate, version bump, preview validation, and final-tag publication pipeline. Use only when the user explicitly asks to release or publish a RustFS version (发版/发布)."
|
||||||
---
|
---
|
||||||
# RustFS Release Publish (preview-validated pipeline)
|
# RustFS Release Publish (preview-validated pipeline)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
The binary reports its build tag (`build::TAG` via shadow_rs; `SHORT_VERSION` in
|
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. 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.
|
||||||
`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:
|
Pipeline shape:
|
||||||
|
|
||||||
@@ -22,9 +17,7 @@ check console main against its latest Release
|
|||||||
-> tag <preview-tag> at that commit -> CI green
|
-> tag <preview-tag> at that commit -> CI green
|
||||||
-> verify preview Release assets -> run binary locally + console checks
|
-> verify preview Release assets -> run binary locally + console checks
|
||||||
-> validate with latest rc client
|
-> validate with latest rc client
|
||||||
-> report preview acceptance results -> STOP for explicit human confirmation
|
|
||||||
-> tag <target> at the SAME commit (zero delta) -> re-verify CI/release
|
-> tag <target> at the SAME commit (zero delta) -> re-verify CI/release
|
||||||
-> CI deletes the <target>-preview.N Releases (tags kept)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
On validation failure: fix lands on main via normal PR (version files are already at `<target>`, no new bump PR), then tag `<preview-tag N+1>` at the new main commit and restart from Phase 2.
|
On validation failure: fix lands on main via normal PR (version files are already at `<target>`, no new bump PR), then tag `<preview-tag N+1>` at the new main commit and restart from Phase 2.
|
||||||
@@ -34,9 +27,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`.
|
- 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`).
|
- 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, 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).
|
If the target version is missing or ambiguous, stop and ask before doing anything (see the semver gate below).
|
||||||
|
|
||||||
## Semver gate — resolve the target before version edits or publication
|
## Semver gate — confirm the target version before touching anything
|
||||||
|
|
||||||
Versions follow [SemVer 2.0.0](https://semver.org/). Precedence reminder:
|
Versions follow [SemVer 2.0.0](https://semver.org/). Precedence reminder:
|
||||||
|
|
||||||
@@ -48,7 +41,7 @@ Numeric prerelease identifiers compare numerically (`beta.9 < beta.10`), not lex
|
|||||||
|
|
||||||
Rules:
|
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 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 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.
|
||||||
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
@@ -57,20 +50,16 @@ Rules:
|
|||||||
- Use `<target>-preview.N` for every target, e.g. `1.0.0-beta.10-preview.3` or `1.1.0-preview.1`.
|
- Use `<target>-preview.N` for every target, e.g. `1.0.0-beta.10-preview.3` or `1.1.0-preview.1`.
|
||||||
- The canonical suffix is exactly `-preview.<digits>`. `build.yml` recognizes it before alpha/beta/rc classification and routes it to the preview-only path; any other tag containing `-preview` fails closed instead of being treated as a release.
|
- The canonical suffix is exactly `-preview.<digits>`. `build.yml` recognizes it before alpha/beta/rc classification and routes it to the preview-only path; any other tag containing `-preview` fails closed instead of being treated as a release.
|
||||||
- A preview Release MUST be published with `isPrerelease=true` and `isLatest=false`. Any `*-latest` preview asset or preview-triggered `latest.json`, R2, Docker, or Helm publication is a pipeline failure.
|
- A preview Release MUST be published with `isPrerelease=true` and `isLatest=false`. Any `*-latest` preview asset or preview-triggered `latest.json`, R2, Docker, or Helm publication is a pipeline failure.
|
||||||
- Preview Releases are cleaned up by the `cleanup-preview-releases` job after `publish-release` succeeds for the deliverable tag. It deletes every Release whose tag is exactly `<target>-preview.<digits>` and never passes `--cleanup-tag`, so the tags survive.
|
|
||||||
|
|
||||||
## Hard rules
|
## Hard rules
|
||||||
|
|
||||||
- Version files (Cargo.toml, Cargo.lock, README, flake.nix, Chart.yaml, rustfs.spec) are bumped ONCE, directly to `<target>`. Never write a `-preview.N` suffix into any version file. If `rustfs-release-version-bump` is ever asked for a `-preview` version, that is a pipeline bug — stop.
|
- Version files (Cargo.toml, Cargo.lock, README, flake.nix, Chart.yaml, rustfs.spec) are bumped ONCE, directly to `<target>`. Never write a `-preview.N` suffix into any version file. If `rustfs-release-version-bump` is ever asked for a `-preview` version, that is a pipeline bug — stop.
|
||||||
- Preview Release assets are versioned and intentionally visible on the Releases page for the duration of validation. Do not label them Latest or use them to update any latest distribution channel.
|
- Preview Release assets are versioned and intentionally visible on the Releases page. Do not label them Latest or use them to update any latest distribution channel.
|
||||||
- Never delete a preview Release by hand before Phase 6 finishes — Phase 4 downloads its assets and the final Release notes are generated while it still exists. Cleanup is CI's job; only step in manually (`gh release delete "<preview-tag>" --yes`, never `--cleanup-tag`) if `cleanup-preview-releases` failed.
|
|
||||||
- Tags have no `v` prefix. Always annotated: `git tag -a <tag> -m "Release <tag>"`.
|
- Tags have no `v` prefix. Always annotated: `git tag -a <tag> -m "Release <tag>"`.
|
||||||
- The final tag MUST point at exactly `PREVIEW_HASH` — the commit the validated preview tag points at. Never tag current `main` HEAD (commits merged after validation are unvalidated), and never create an extra version-bump commit between preview and final.
|
- The final tag MUST point at exactly `PREVIEW_HASH` — the commit the validated preview tag points at. Never tag current `main` HEAD (commits merged after validation are unvalidated), and never create an extra version-bump commit between preview and final.
|
||||||
- When a previous deliverable exists, GitHub Release notes for the preview and final tags MUST use it as their shared comparison baseline: the most recently published non-preview Release before the target. Internal `-preview.N` Releases are explicitly excluded from that selection, even when they point at the same commit as the final tag — cleanup runs after the notes are generated, so the preview Release is still present and would otherwise be picked as the baseline. If no previous deliverable exists, omit `previous_tag_name` and record that GitHub's default baseline fallback was used.
|
- When a previous deliverable exists, GitHub Release notes for the preview and final tags MUST use it as their shared comparison baseline: the most recently published non-preview Release before the target. Internal `-preview.N` Releases are explicitly excluded from that selection, even when they point at the same commit as the final tag. If no previous deliverable exists, omit `previous_tag_name` and record that GitHub's default baseline fallback was used.
|
||||||
- Generated Release notes carry a workflow-management marker so retries can repair them. Before manually curating a generated body, remove that marker; unmarked non-placeholder notes are preserved by later workflow runs.
|
- Generated Release notes carry a workflow-management marker so retries can repair them. Before manually curating a generated body, remove that marker; unmarked non-placeholder notes are preserved by later workflow runs.
|
||||||
- Phases run in order; a failure in any phase blocks everything after it. After the fix lands on main, restart from Phase 2 with the next preview iteration against the new `origin/main` hash — do not resume mid-pipeline against a stale hash.
|
- Phases run in order; a failure in any phase blocks everything after it. After the fix lands on main, restart from Phase 2 with the next preview iteration against the new `origin/main` hash — do not resume mid-pipeline against a stale hash.
|
||||||
- Completing preview acceptance does not authorize the final tag. After Phases 3–5 pass, report the acceptance evidence and stop until the user explicitly confirms continuation. The original release request, an earlier confirmation, silence, or an automated follow-up does not satisfy this gate.
|
|
||||||
- Confirmation is scoped to the reported `<target>`, `<preview-tag>`, and `PREVIEW_HASH`. A failed or repeated acceptance cycle, including any new preview iteration, invalidates prior confirmation and requires a new one.
|
|
||||||
- If the release is abandoned after Phase 1 merged, main's version files claim a version that was never tagged. Either revert the bump PR or leave it to be overwritten by the next release — but tell the user explicitly and record the decision.
|
- If the release is abandoned after Phase 1 merged, main's version files claim a version that was never tagged. Either revert the bump PR or leave it to be overwritten by the next release — but tell the user explicitly and record the decision.
|
||||||
- User-facing status updates in Chinese; commits, PR titles/bodies, and tag messages in English. No hard-wrapping in commit messages, PR bodies, or documentation prose — one logical line per sentence/paragraph, let soft wrap handle display.
|
- User-facing status updates in Chinese; commits, PR titles/bodies, and tag messages in English. No hard-wrapping in commit messages, PR bodies, or documentation prose — one logical line per sentence/paragraph, let soft wrap handle display.
|
||||||
|
|
||||||
@@ -82,7 +71,58 @@ Rules:
|
|||||||
|
|
||||||
### Console release gate
|
### Console release gate
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## Phase 1 — Version bump to the final target (once)
|
## Phase 1 — Version bump to the final target (once)
|
||||||
|
|
||||||
@@ -118,15 +158,54 @@ 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
## Phases 4–5 — Local artifact, Console, and rc acceptance
|
## Phase 4 — Run the artifact locally, verify the console
|
||||||
|
|
||||||
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.
|
Work inside the session scratchpad directory; never leave stray data dirs.
|
||||||
|
|
||||||
### Manual confirmation gate
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
After every Phase 3–5 check passes, report the target, preview tag, `PREVIEW_HASH`, preview Release URL, console result, and rc matrix, then explicitly ask the user whether to publish the final tag. End the turn without creating or pushing `<target>`.
|
Defaults: S3 endpoint `:9000`, embedded console `:9001`.
|
||||||
|
|
||||||
Continue to Phase 6 only after a new user reply explicitly confirms the reported target, preview tag, and commit. A clear affirmative reply to that exact report, such as `确认继续`, is sufficient; if the reply is ambiguous or any reported value changed, ask again.
|
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.
|
||||||
|
|
||||||
## Phase 6 — Publish the final tag on the validated commit
|
## Phase 6 — Publish the final tag on the validated commit
|
||||||
|
|
||||||
@@ -142,7 +221,6 @@ git push origin "<target>"
|
|||||||
- CI rebuilds from the same source; the only changed input is the tag name, so the binary now self-reports `<target>`.
|
- CI rebuilds from the same source; the only changed input is the tag name, so the binary now self-reports `<target>`.
|
||||||
- Verify the final tag's complete publication path: all matrix and release jobs green; `gh release view "<target>"` shows the full versioned and `-latest` asset set plus checksums, SBOM, and provenance; Docker and Helm workflows succeed; `latest.json` points to `<target>`. A stable target must have `isPrerelease=false` and `isLatest=true`. An alpha/beta/rc target must have `isPrerelease=true`; GitHub does not permit prereleases to be Latest, but the project `latest.json` still advances to the final non-preview target.
|
- Verify the final tag's complete publication path: all matrix and release jobs green; `gh release view "<target>"` shows the full versioned and `-latest` asset set plus checksums, SBOM, and provenance; Docker and Helm workflows succeed; `latest.json` points to `<target>`. A stable target must have `isPrerelease=false` and `isLatest=true`. An alpha/beta/rc target must have `isPrerelease=true`; GitHub does not permit prereleases to be Latest, but the project `latest.json` still advances to the final non-preview target.
|
||||||
- Verify the final Release body contains `## What's Changed` and a Full Changelog link. When `PREVIOUS_DELIVERABLE` exists, the link MUST be `https://github.com/rustfs/rustfs/compare/<PREVIOUS_DELIVERABLE>...<target>` and the baseline MUST equal the preview Release baseline; for example, both `1.0.0-beta.12-preview.1` and `1.0.0-beta.12` compare from `1.0.0-beta.11`.
|
- Verify the final Release body contains `## What's Changed` and a Full Changelog link. When `PREVIOUS_DELIVERABLE` exists, the link MUST be `https://github.com/rustfs/rustfs/compare/<PREVIOUS_DELIVERABLE>...<target>` and the baseline MUST equal the preview Release baseline; for example, both `1.0.0-beta.12-preview.1` and `1.0.0-beta.12` compare from `1.0.0-beta.11`.
|
||||||
- Verify the preview cleanup: `cleanup-preview-releases` must succeed, `gh release view "<preview-tag>"` must then report `release not found` for every preview iteration of this target, and `git rev-parse "<preview-tag>^{commit}"` must still resolve to `PREVIEW_HASH` (the tag is kept). If the job failed, delete the leftover Releases manually with `gh release delete "<preview-tag>" --yes` and report it.
|
|
||||||
- Optionally spot-check `./rustfs --version` from a final-tag artifact — it must report `<target>`.
|
- Optionally spot-check `./rustfs --version` from a final-tag artifact — it must report `<target>`.
|
||||||
|
|
||||||
## Output contract
|
## Output contract
|
||||||
@@ -151,6 +229,5 @@ Always report:
|
|||||||
|
|
||||||
- Console gate result: previous/latest Console tags, whether merged changes required a release, `CONSOLE_HASH`, and Console run/Release URLs when a release was published.
|
- Console gate result: previous/latest Console tags, whether merged changes required a release, `CONSOLE_HASH`, and Console run/Release URLs when a release was published.
|
||||||
- Target version, preview tag(s) used, `PREVIEW_HASH` (which both tags point at).
|
- Target version, preview tag(s) used, `PREVIEW_HASH` (which both tags point at).
|
||||||
- Manual confirmation gate status (`WAITING_FOR_CONFIRMATION` or `CONFIRMED`) and its exact target, preview tag, and `PREVIEW_HASH`.
|
- Per-phase result (PASS/FAIL/BLOCKED) with key evidence: preview and final Release URLs, preview `isPrerelease`/`isLatest` state, final latest-channel state, console check results, and the rc command matrix.
|
||||||
- Per-phase result (PASS/FAIL/BLOCKED) with key evidence: preview and final Release URLs, preview `isPrerelease`/`isLatest` state, final latest-channel state, console check results, the rc command matrix, and the preview-Release cleanup result (deleted Releases plus surviving tags).
|
|
||||||
- Any deviation from this pipeline and why the user approved it.
|
- Any deviation from this pipeline and why the user approved it.
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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,16 +4,17 @@ description: "Prepare the version-file and release-asset bump for an exact RustF
|
|||||||
---
|
---
|
||||||
# RustFS Release Version Bump
|
# RustFS Release Version Bump
|
||||||
|
|
||||||
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`.
|
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`).
|
||||||
|
|
||||||
Validated baseline: release pattern used in PR `#2957`.
|
Validated baseline: release pattern used in PR `#2957`.
|
||||||
|
|
||||||
## Required inputs
|
## Required inputs
|
||||||
|
|
||||||
- Exact target version, for example `1.0.0-beta.4`.
|
- Exact target version, for example `1.0.0-beta.4`.
|
||||||
- Delivery scope: local (`edit/verify`), git (`commit/push`), or GitHub
|
- Delivery scope:
|
||||||
(`commit/push/PR`). Derive it from the conversation; when unspecified, prepare
|
- Local only (`edit/verify`).
|
||||||
and verify locally without blocking on a delivery question.
|
- Local + git (`commit/push`).
|
||||||
|
- Full GitHub flow (`commit/push/PR`).
|
||||||
|
|
||||||
If target version is missing or ambiguous, stop and ask before editing.
|
If target version is missing or ambiguous, stop and ask before editing.
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ Reject any target version containing `-preview`: preview identifiers are tag-onl
|
|||||||
## Read before editing
|
## Read before editing
|
||||||
|
|
||||||
- `AGENTS.md` (root and nearest path-specific files).
|
- `AGENTS.md` (root and nearest path-specific files).
|
||||||
- `.github/pull_request_template.md` only when preparing a PR.
|
- `.github/pull_request_template.md`.
|
||||||
- Current branch status and diff against `origin/main`.
|
- Current branch status and diff against `origin/main`.
|
||||||
|
|
||||||
## Default release file scope
|
## Default release file scope
|
||||||
@@ -49,7 +50,8 @@ Only drop a file when the current repository release process clearly no longer r
|
|||||||
## Step-by-step workflow
|
## Step-by-step workflow
|
||||||
|
|
||||||
1. Confirm intent and isolate scope
|
1. Confirm intent and isolate scope
|
||||||
- Use the exact target and delivery scope already supplied; ask only for a missing or ambiguous target or a material release-policy choice.
|
- Confirm target version string exactly.
|
||||||
|
- Confirm whether user requested local-only or full GitHub flow.
|
||||||
- Inspect current branch and ensure only release-related files are touched for this task.
|
- Inspect current branch and ensure only release-related files are touched for this task.
|
||||||
|
|
||||||
2. Update workspace versions
|
2. Update workspace versions
|
||||||
@@ -80,18 +82,18 @@ Only drop a file when the current repository release process clearly no longer r
|
|||||||
4. Verify before shipping
|
4. Verify before shipping
|
||||||
- Run:
|
- Run:
|
||||||
- `make pre-commit`
|
- `make pre-commit`
|
||||||
- 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.
|
- If `make pre-commit` fails, return `BLOCKED` with root cause and do not silently widen scope to fix unrelated issues unless user asks.
|
||||||
|
|
||||||
5. Commit strategy (only when committing is authorized)
|
5. Commit strategy
|
||||||
- Preferred split when both parts changed:
|
- Preferred split when both parts changed:
|
||||||
- `chore(release): prepare <version>` for `Cargo.toml` and `Cargo.lock`.
|
- `chore(release): prepare <version>` for `Cargo.toml` and `Cargo.lock`.
|
||||||
- `chore(release): align release assets for <version>` for docs and packaging files.
|
- `chore(release): align release assets for <version>` for docs and packaging files.
|
||||||
- If user asks for one commit, use one commit.
|
- If user asks for one commit, use one commit.
|
||||||
- Stage only intended release files; do not include unrelated working tree changes.
|
- Stage only intended release files; do not include unrelated working tree changes.
|
||||||
|
|
||||||
6. Push and PR (only for the authorized delivery scope)
|
6. Push and PR
|
||||||
- Push branch:
|
- Push branch:
|
||||||
- Use the user-requested or configured push remote: `git push -u <push-remote> <branch>` (first push), or `git push` when tracking is already configured.
|
- `git push -u origin <branch>` (first push), or `git push` (tracking already exists).
|
||||||
- Create PR with template headings unchanged:
|
- Create PR with template headings unchanged:
|
||||||
- `gh pr create --base main --head <branch> --title ... --body-file ...`
|
- `gh pr create --base main --head <branch> --title ... --body-file ...`
|
||||||
- PR title/body must be English.
|
- PR title/body must be English.
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ matched security surface, the concise security reference under
|
|||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Freeze the exact diff/head and identify the changed trust boundaries.
|
1. Freeze the exact diff/head and identify the changed trust boundaries.
|
||||||
2. Inspect the headings in [advisory-patterns.md](references/advisory-patterns.md),
|
2. Read [advisory-patterns.md](references/advisory-patterns.md), then apply only
|
||||||
then read the matching sections. Read the full map only for a broad security
|
the matching sections. Useful headings are
|
||||||
audit. Useful headings are
|
|
||||||
auth/admin, IAM/STS/OIDC, policy/plugins, S3/copy/multipart, protocols, paths,
|
auth/admin, IAM/STS/OIDC, policy/plugins, S3/copy/multipart, protocols, paths,
|
||||||
secrets/logging/RPC, browser/CORS/proxy, SSE, Object Lock, and serde.
|
secrets/logging/RPC, browser/CORS/proxy, SSE, Object Lock, and serde.
|
||||||
3. Trace unauthenticated, low-privilege, wrong-action/owner/bucket, malformed,
|
3. Trace unauthenticated, low-privilege, wrong-action/owner/bucket, malformed,
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ Update this file only when an advisory adds or changes a reusable lesson, affect
|
|||||||
|
|
||||||
### S3 object actions, copy, multipart, and upload policy validation
|
### S3 object actions, copy, multipart, and upload policy validation
|
||||||
|
|
||||||
- `GHSA-g8w9-qw9q-fghr`: a valid presigned `PutObject` accepted extra `x-amz-tagging`, website redirect, and storage-class headers omitted from `SignedHeaders`. Lesson: a presigned URL is a bounded capability; reject `x-amz-*` headers that are not cryptographically bound by the signature so unsigned metadata cannot change authorization, lifecycle, redirect, cost, or durability semantics.
|
|
||||||
- `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-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-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`.
|
- `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`.
|
||||||
@@ -108,9 +107,9 @@ Update this file only when an advisory adds or changes a reusable lesson, affect
|
|||||||
|
|
||||||
### Serde deserialization and input validation
|
### Serde deserialization and input validation
|
||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
- `#[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.
|
- `#[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()` with a typed error, or clamp only when the domain explicitly requires saturation.
|
- 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.
|
||||||
- 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.
|
- 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
|
## Useful Search Seeds
|
||||||
@@ -120,7 +119,7 @@ Use these targeted searches when a diff touches security-sensitive code:
|
|||||||
```bash
|
```bash
|
||||||
rg -n "validate_admin_request|check_permissions|AdminAction::|deny_only|is_allowed" rustfs crates
|
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 "authorize_operation|FtpsDriver|SftpDriver|RETR|MKD|SIZE|MDTM|CreateBucket|GetObject|HeadObject" crates/protocols rustfs
|
||||||
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|presign|SignedHeaders|content-length-range|starts-with" rustfs crates
|
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|content-length-range|starts-with" rustfs crates
|
||||||
rg -n "ListBucketVersions|GetObjectVersion|versionId|VersionId|ExistingObjectTag|ForAllValues|ForAnyValue|POLICY_PLUGIN|opa" 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 "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
|
rg -n "DEFAULT_SECRET|DEFAULT_ACCESS|TEST_PRIVATE_KEY|rustfs rpc|RUSTFS_RPC_SECRET" rustfs crates
|
||||||
@@ -137,7 +136,6 @@ 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.
|
- 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.
|
- 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.
|
- 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, and storage-class headers; require rejection before 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: test-coverage-improver
|
name: test-coverage-improver
|
||||||
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.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Test Coverage Improver
|
# Test Coverage Improver
|
||||||
@@ -9,49 +9,58 @@ Use this skill when you need a prioritized, risk-aware plan to improve tests fro
|
|||||||
|
|
||||||
## Usage assumptions
|
## Usage assumptions
|
||||||
- Focus scope is either changed lines/files, a module, or the whole repository.
|
- Focus scope is either changed lines/files, a module, or the whole repository.
|
||||||
- Reuse a supplied coverage artifact when its revision, scope, and format match.
|
- Coverage artifact must be generated or provided in a supported format.
|
||||||
- If required context is missing, call out assumptions explicitly before proposing work.
|
- If required context is missing, call out assumptions explicitly before proposing work.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Define scope and baseline
|
1. Define scope and baseline
|
||||||
- Derive the revision and scope from the request, diff, or supplied report.
|
- Confirm target language, framework, and branch.
|
||||||
- Default to the affected files/module; whole-workspace coverage requires that
|
- Confirm whether the scope is changed files only or full-repo.
|
||||||
scope in the request. Ask only if a wrong scope would change the result.
|
|
||||||
|
|
||||||
2. Obtain coverage evidence
|
2. Produce coverage snapshot
|
||||||
- First inspect a matching existing artifact; do not regenerate it merely
|
- Rust: `cargo llvm-cov` (or `cargo tarpaulin`) with existing repo config.
|
||||||
because this skill was selected.
|
- JavaScript/TypeScript: `npm test -- --coverage` and read `coverage/coverage-final.json`.
|
||||||
- If measurement is needed, read the Coverage section of
|
- Python: `pytest --cov=<pkg> --cov-report=json` and read `coverage.json`.
|
||||||
[the testing guide](../../../docs/testing/README.md#coverage), check disk
|
- Collect total, per-file, and changed-line coverage.
|
||||||
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
|
3. Rank highest-risk gaps
|
||||||
- Prioritize changed code, branch coverage gaps, and low-confidence boundaries.
|
- Prioritize changed code, branch coverage gaps, and low-confidence boundaries.
|
||||||
- Apply the risk rubric in [coverage-prioritization.md](references/coverage-prioritization.md).
|
- Apply the risk rubric in [coverage-prioritization.md](references/coverage-prioritization.md).
|
||||||
- Report up to 5–8 evidenced gaps; do not pad a small scope.
|
- Keep shortlist to 5–8 gaps.
|
||||||
- For each gap, capture: file, lines, uncovered branches, and estimated risk score.
|
- For each gap, capture: file, lines, uncovered branches, and estimated risk score.
|
||||||
|
|
||||||
4. Propose high-impact tests
|
4. Propose high-impact tests
|
||||||
- For each gap, name the behavior and regression, distinguishing assertions,
|
- For each shortlisted gap, output:
|
||||||
relevant normal/edge/failure cases, necessary setup, and estimated effort.
|
- Intent and expected behavior.
|
||||||
- Include only scenarios and setup that apply; reuse shared fixture details.
|
- Normal, edge, and failure scenarios.
|
||||||
|
- Assertions and side effects to verify.
|
||||||
|
- Setup needs (fixtures, mocks, integration dependencies).
|
||||||
|
- Estimated effort (`S/M/L`).
|
||||||
|
|
||||||
5. Close with validation plan
|
5. Close with validation plan
|
||||||
- State which gaps remain after proposals.
|
- State which gaps remain after proposals.
|
||||||
- Give a scoped verification command and behavior-based acceptance criterion;
|
- Provide concrete verification command and acceptance threshold.
|
||||||
use a coverage threshold only when the task or repository requires one.
|
|
||||||
- List assumptions or blockers (environment, fixtures, flaky dependencies).
|
- List assumptions or blockers (environment, fixtures, flaky dependencies).
|
||||||
|
|
||||||
## Report
|
## Output template
|
||||||
|
|
||||||
Summarize the supported metrics, then combine each ranked gap with its proposed
|
### Coverage Snapshot
|
||||||
test and validation. Include source lines only when supplied or inspected;
|
- total / branch coverage
|
||||||
mark missing metrics or locations as unknown. Do not duplicate gaps and tests
|
- changed-file coverage
|
||||||
in separate templates or fill empty categories for an otherwise small report.
|
- 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "Test Coverage Improver"
|
display_name: "Test Coverage Improver"
|
||||||
short_description: "Find top uncovered risk areas and propose high-impact tests."
|
short_description: "Find top uncovered risk areas and propose high-impact tests."
|
||||||
default_prompt: "Use $test-coverage-improver to analyze coverage for the requested scope, reuse matching reports, and propose tests for evidenced risks."
|
default_prompt: "Run coverage checks, identify largest gaps, and recommend highest-impact test cases to improve risk coverage."
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ description: Debug ILM tiering / lifecycle transition issues — NoSuchVersion o
|
|||||||
|
|
||||||
# Tier / ILM Debugging
|
# Tier / ILM Debugging
|
||||||
|
|
||||||
Playbook: [docs/operations/tier-ilm-debugging.md](../../../docs/operations/tier-ilm-debugging.md).
|
Full playbook: [docs/operations/tier-ilm-debugging.md](../../../docs/operations/tier-ilm-debugging.md)
|
||||||
Read the section matching the symptom: metadata/`xl.meta`, runtime versionId,
|
— read it before changing tier code.
|
||||||
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:
|
Quick moves:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
sha256-linux=4696a43b167ac608b3b8677027c9fe9fdac3396d37c8cca11dce531c720ac6d2
|
|
||||||
sha256-darwin=9785867929047dfd8c6f768e0d2b1e0a8fdba85216f4a4139093b1619d03ff07
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
sha256-darwin=53b05ac745905809d3828c6994bdd8ecf9d20b2b61a8a9d80fe15eb62f932193
|
sha256-darwin=88ee9684ece0e27294f2b3f0c9c8fe62890feff76aa47279d42dab0af3196fe2
|
||||||
sha256-linux=7c892afa4b9d1591b46bd79c976b647109a277284fddb3b98edced4b0297eda2
|
sha256-linux=d13337936af6778b1d2b2b255ae7fd350fdec94034be46daf738bd577653f799
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
sha256-darwin=a5665318c9bdc0947514fb7008ba1b83b114b739fac775c3c446f207058b7c7a
|
sha256=9b9bc336b43b70d0e06e0adb5455bf035bb18945d85d60936eb6fe4d48e0e680
|
||||||
sha256-linux=45d80e1723de5d25bb5b81f3ef5c82f583efc3e4f036a8cd2bb99e4f1eca9e51
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
sha256=87c05c46d611ea7ed3feb5f7276bda8e5a0f70d72165d305d73a457907e7ba79
|
|
||||||
@@ -1 +1 @@
|
|||||||
sha256=95c8adc016bbc0df9fb2afa24a108bcdf6567ec4d0518725a6cae301593ab556
|
sha256=655a3f3c1d042e694339d15caba7580518320322d1bac0f09450b37e6c09e2e7
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
sha256=5db88c6fec94d4f269c7d9cfc128bd2adc27b3d7021127e2fa0b1daccc5f900f
|
sha256=ec27cde6ce6400723c4b372bfbd2ac61709c744294e4810af765e8a808d8e31d
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -23,4 +23,4 @@ coverage: core-deps ## Workspace line coverage (cargo-llvm-cov + nextest; slow,
|
|||||||
@mkdir -p target/llvm-cov
|
@mkdir -p target/llvm-cov
|
||||||
cargo llvm-cov report --lcov --output-path target/llvm-cov/lcov.info
|
cargo llvm-cov report --lcov --output-path target/llvm-cov/lcov.info
|
||||||
cargo llvm-cov report --json --output-path target/llvm-cov/coverage.json
|
cargo llvm-cov report --json --output-path target/llvm-cov/coverage.json
|
||||||
$(RUSTFS_PYTHON_BIN) scripts/coverage_per_crate.py target/llvm-cov/coverage.json
|
python3 scripts/coverage_per_crate.py target/llvm-cov/coverage.json
|
||||||
|
|||||||
@@ -45,11 +45,6 @@ logging-guardrails-check: ## Check logging guardrails for redaction and noise re
|
|||||||
@echo "🪵 Checking logging guardrails..."
|
@echo "🪵 Checking logging guardrails..."
|
||||||
./scripts/check_logging_guardrails.sh
|
./scripts/check_logging_guardrails.sh
|
||||||
|
|
||||||
.PHONY: error-other-ratchet-check
|
|
||||||
error-other-ratchet-check: ## Check the ecstore ::other(format!) quorum-bucketing ratchet stays shrink-only
|
|
||||||
@echo "🪣 Checking error other(format!) ratchet..."
|
|
||||||
./scripts/check_error_other_format_ratchet.sh
|
|
||||||
|
|
||||||
.PHONY: tokio-io-uring-check
|
.PHONY: tokio-io-uring-check
|
||||||
tokio-io-uring-check: ## Check tokio io-uring runtime feature stays removed
|
tokio-io-uring-check: ## Check tokio io-uring runtime feature stays removed
|
||||||
@echo "🚫 Checking tokio io-uring feature guard..."
|
@echo "🚫 Checking tokio io-uring feature guard..."
|
||||||
@@ -80,15 +75,10 @@ embedded-secrets-check: ## Check no private key material or credential literal i
|
|||||||
@echo "🔑 Checking embedded secret material guard..."
|
@echo "🔑 Checking embedded secret material guard..."
|
||||||
./scripts/check_embedded_secrets.sh
|
./scripts/check_embedded_secrets.sh
|
||||||
|
|
||||||
.PHONY: offline-enrollment-e2e-check
|
|
||||||
offline-enrollment-e2e-check: core-deps ## Build and exercise the dedicated offline enrollment E2E root
|
|
||||||
@echo "🔐 Checking the offline enrollment E2E root boundary..."
|
|
||||||
./scripts/check_offline_enrollment_e2e.sh
|
|
||||||
|
|
||||||
.PHONY: test-wiring-check
|
.PHONY: test-wiring-check
|
||||||
test-wiring-check: ## Check tests stay registered and selected by their intended runners
|
test-wiring-check: ## Check tests stay registered and selected by their intended runners
|
||||||
@echo "🧪 Checking test wiring..."
|
@echo "🧪 Checking test wiring..."
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_test_wiring.py
|
python3 ./scripts/check_test_wiring.py
|
||||||
|
|
||||||
.PHONY: log-analyzer-rules-check
|
.PHONY: log-analyzer-rules-check
|
||||||
log-analyzer-rules-check: core-deps ## Check log-analyzer rule anchors still exist verbatim in source
|
log-analyzer-rules-check: core-deps ## Check log-analyzer rule anchors still exist verbatim in source
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
.NOTPARALLEL: pre-commit pre-pr dev-check
|
.NOTPARALLEL: pre-commit pre-pr dev-check
|
||||||
|
|
||||||
.PHONY: setup-hooks
|
.PHONY: setup-hooks
|
||||||
setup-hooks: ## Install the configured pre-commit hooks
|
setup-hooks: ## Set up git hooks
|
||||||
@echo "🔧 Setting up git hooks..."
|
@echo "🔧 Setting up git hooks..."
|
||||||
pre-commit validate-config
|
chmod +x .git/hooks/pre-commit
|
||||||
pre-commit install
|
|
||||||
@echo "✅ Git hooks setup complete!"
|
@echo "✅ Git hooks setup complete!"
|
||||||
|
|
||||||
.PHONY: doc-paths-check
|
.PHONY: doc-paths-check
|
||||||
@@ -20,13 +19,13 @@ planning-docs-check: ## Check that no planning-type documents are committed
|
|||||||
./scripts/check_no_planning_docs.sh
|
./scripts/check_no_planning_docs.sh
|
||||||
|
|
||||||
.PHONY: pre-commit
|
.PHONY: pre-commit
|
||||||
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
|
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
|
||||||
@echo "✅ All pre-commit checks passed!"
|
@echo "✅ All pre-commit checks passed!"
|
||||||
|
|
||||||
.PHONY: pre-pr
|
.PHONY: pre-pr
|
||||||
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check offline-enrollment-e2e-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
||||||
@echo "✅ All pre-PR checks passed!"
|
@echo "✅ All pre-PR checks passed!"
|
||||||
|
|
||||||
.PHONY: dev-check
|
.PHONY: dev-check
|
||||||
dev-check: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast local development checks
|
dev-check: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast local development checks
|
||||||
@echo "✅ Fast development checks passed!"
|
@echo "✅ Fast development checks passed!"
|
||||||
|
|||||||
+5
-11
@@ -31,23 +31,17 @@ script-tests: ## Run shell script tests
|
|||||||
./scripts/test_object_batch_bench_enhanced.sh
|
./scripts/test_object_batch_bench_enhanced.sh
|
||||||
./scripts/test_hotpath_warp_ab_gate.sh
|
./scripts/test_hotpath_warp_ab_gate.sh
|
||||||
./scripts/test_hotpath_warp_abba.sh
|
./scripts/test_hotpath_warp_abba.sh
|
||||||
./scripts/test_scanner_validation_harness.sh
|
|
||||||
./scripts/test_exact_1mib_handoff_abba.sh
|
./scripts/test_exact_1mib_handoff_abba.sh
|
||||||
./scripts/test_pinned_paired_abba_bench.sh
|
./scripts/test_pinned_paired_abba_bench.sh
|
||||||
./scripts/test_manual_transition_runbooks.sh
|
./scripts/test_manual_transition_runbooks.sh
|
||||||
./scripts/test_fuzz_runner.sh
|
|
||||||
./scripts/test_python_bin.sh
|
|
||||||
./scripts/check_embedded_secrets.sh --self-test
|
./scripts/check_embedded_secrets.sh --self-test
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_test_wiring.py --self-test
|
python3 ./scripts/check_test_wiring.py --self-test
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_security_coverage.py --self-test
|
python3 ./scripts/check_security_coverage.py --self-test
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_scheduled_validation_freshness.py --self-test
|
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_security_workflow.py
|
python3 ./scripts/s3-tests/test_report_compat.py
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_nightly_candidate.py
|
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/s3-tests/test_report_compat.py
|
|
||||||
bash -n ./scripts/validate_object_data_cache_cold_stampede.sh
|
bash -n ./scripts/validate_object_data_cache_cold_stampede.sh
|
||||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_object_data_cache_follower_samples.py --self-test
|
python3 ./scripts/check_object_data_cache_follower_samples.py --self-test
|
||||||
./scripts/validate_object_data_cache_cold_stampede.sh --self-test
|
./scripts/validate_object_data_cache_cold_stampede.sh --self-test
|
||||||
./scripts/run_scanner_heal_evidence_case.sh --self-test
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: core-deps script-tests ## Run all tests (needs cargo-nextest; RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to override)
|
test: core-deps script-tests ## Run all tests (needs cargo-nextest; RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to override)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# Committed floor for the number of tests selected by the migration-critical
|
# 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).
|
# CI gate (see scripts/check_migration_gate_count.sh, backlog#1153 infra-12).
|
||||||
#
|
#
|
||||||
# The floor equals the exact count of rustfs-ecstore --lib tests, with the
|
# The floor equals the exact count of rustfs-ecstore --lib tests matching the
|
||||||
# test-util feature enabled, matching the gate filter (name substrings:
|
# gate filter (name substrings: data_movement, rebalance, decommission,
|
||||||
# data_movement, rebalance, decommission, source_cleanup, delete_marker) at
|
# source_cleanup, delete_marker) at the time this file was last updated.
|
||||||
# the time this file was last updated.
|
|
||||||
# CI fails if the selected count drops below this number, so renames or
|
# 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.
|
# 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.
|
# Adding tests does not require a bump, but bumping keeps the guard tight.
|
||||||
946
|
571
|
||||||
|
|||||||
+25
-234
@@ -1,7 +1,5 @@
|
|||||||
# nextest configuration for RustFS.
|
# nextest configuration for RustFS.
|
||||||
#
|
#
|
||||||
experimental = ["setup-scripts"]
|
|
||||||
|
|
||||||
# Serialize the ecstore tests that share the process-wide disk registry or
|
# Serialize the ecstore tests that share the process-wide disk registry or
|
||||||
# exercise a multi-disk commit handoff across nextest process boundaries.
|
# exercise a multi-disk commit handoff across nextest process boundaries.
|
||||||
#
|
#
|
||||||
@@ -46,36 +44,7 @@ e2e-reliability = { max-threads = 1 }
|
|||||||
e2e-inline-boundaries = { max-threads = 1 }
|
e2e-inline-boundaries = { max-threads = 1 }
|
||||||
e2e-cluster-nightly = { 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.
|
|
||||||
[scripts.setup.ecstore-large-stack]
|
|
||||||
command = ['sh', '-c', 'echo RUST_MIN_STACK=33554432 >> "$NEXTEST_ENV"']
|
|
||||||
|
|
||||||
# The serial ILM selection builds the same deep storage futures in both the
|
|
||||||
# lifecycle transition module and scanner integration binary. Different tests
|
|
||||||
# in each have overflowed first across otherwise unrelated CI runs.
|
|
||||||
[scripts.setup.lifecycle-large-stack]
|
|
||||||
command = ['sh', '-c', 'echo RUST_MIN_STACK=33554432 >> "$NEXTEST_ENV"']
|
|
||||||
|
|
||||||
# --- default profile (local): serialize the flaky groups, never retry --------
|
# --- 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::(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'
|
|
||||||
|
|
||||||
[[profile.default.overrides]]
|
[[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)/))'
|
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'
|
test-group = 'ecstore-serial-flaky'
|
||||||
@@ -89,29 +58,6 @@ test-group = 'ecstore-serial-flaky'
|
|||||||
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
|
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
|
||||||
test-group = 'ecstore-serial-flaky'
|
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,
|
# 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
|
# 2-pool store and commits a 72 MiB multipart object. Keep that cross-disk IO
|
||||||
# from overlapping the ecstore commit fixtures above.
|
# from overlapping the ecstore commit fixtures above.
|
||||||
@@ -132,29 +78,12 @@ test-group = 'embedded-test-ports'
|
|||||||
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
|
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
|
||||||
test-group = 'ecstore-serial-flaky'
|
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
|
# The durable ILM decommission regressions build isolated multi-pool stores and
|
||||||
# deliberately take source or target disks offline while checking fencing.
|
# deliberately take source or target disks offline while checking fencing.
|
||||||
[[profile.default.overrides]]
|
[[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))'
|
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'
|
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'
|
|
||||||
|
|
||||||
# Serialize the bucket-incarnation / lifecycle-fence tests. They drive
|
# Serialize the bucket-incarnation / lifecycle-fence tests. They drive
|
||||||
# init_bucket_metadata_sys and bucket_metadata_sys_of, i.e. process-global
|
# 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
|
# OnceLock state that serial_test's #[serial] cannot protect across nextest's
|
||||||
@@ -183,18 +112,11 @@ test-group = 'e2e-reliability'
|
|||||||
filter = 'package(e2e_test) & test(/^inline_fast_path_cluster_test::/)'
|
filter = 'package(e2e_test) & test(/^inline_fast_path_cluster_test::/)'
|
||||||
test-group = 'e2e-inline-boundaries'
|
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]
|
# 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
|
# does not cross nextest process boundaries, so keep every Vault-backed test in
|
||||||
# one group.
|
# one group.
|
||||||
[[profile.default.overrides]]
|
[[profile.default.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$/))'
|
filter = 'package(e2e_test) & (test(/^kms::kms_vault_test::/) | test(/^kms::configured_roundtrip_test::test_configured_vault_kms_admin_and_versioned_cleanup$/))'
|
||||||
test-group = 'e2e-vault'
|
test-group = 'e2e-vault'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -212,18 +134,6 @@ fail-fast = false
|
|||||||
# marker is the observable signal the flake policy is built around.
|
# marker is the observable signal the flake policy is built around.
|
||||||
path = "junit.xml"
|
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::(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'
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# QUARANTINE — flaky tests granted retries = 2 under the ci profile ONLY.
|
# QUARANTINE — flaky tests granted retries = 2 under the ci profile ONLY.
|
||||||
#
|
#
|
||||||
@@ -256,15 +166,6 @@ test-group = 'ecstore-serial-flaky'
|
|||||||
filter = 'package(rustfs-ecstore) & test(walk_dir_does_not_charge_consumer_backpressure_to_the_stall_budget)'
|
filter = 'package(rustfs-ecstore) & test(walk_dir_does_not_charge_consumer_backpressure_to_the_stall_budget)'
|
||||||
retries = 2
|
retries = 2
|
||||||
|
|
||||||
# Serialize the relocated-pool GET resume regression under the ci profile too
|
|
||||||
# (see the matching default-profile override near the top). No longer a
|
|
||||||
# quarantine: the fixture race (rustfs#6701/rustfs#6703) was fixed by #6707,
|
|
||||||
# which made the staging tolerate quorum-tolerated disk gaps; only the 8-disk
|
|
||||||
# cross-disk-IO serialization remains.
|
|
||||||
[[profile.ci.overrides]]
|
|
||||||
filter = 'package(rustfs) & test(execute_get_object_resumes_from_relocated_pool_without_splicing_body)'
|
|
||||||
test-group = 'ecstore-serial-flaky'
|
|
||||||
|
|
||||||
# Serialize the 4-disk reliability / degraded-read e2e tests under the ci
|
# Serialize the 4-disk reliability / degraded-read e2e tests under the ci
|
||||||
# profile too (see the e2e-reliability test-group note near the top). Not a
|
# profile too (see the e2e-reliability test-group note near the top). Not a
|
||||||
# quarantine: no retries, just single-threaded so several 4-disk servers never
|
# quarantine: no retries, just single-threaded so several 4-disk servers never
|
||||||
@@ -280,18 +181,8 @@ test-group = 'e2e-reliability'
|
|||||||
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
|
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
|
||||||
test-group = 'ecstore-serial-flaky'
|
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]]
|
[[profile.ci.overrides]]
|
||||||
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::heal::heal_result_report_tests::/)'
|
filter = 'package(rustfs) & test(execute_get_object_resumes_from_relocated_pool_without_splicing_body)'
|
||||||
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'
|
test-group = 'ecstore-serial-flaky'
|
||||||
|
|
||||||
# Match the default-profile embedded test isolation without quarantining or
|
# Match the default-profile embedded test isolation without quarantining or
|
||||||
@@ -306,20 +197,10 @@ test-group = 'embedded-test-ports'
|
|||||||
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
|
filter = 'package(rustfs-ecstore) & test(manual_transition_page_checkpoint_persists_durable_job_progress)'
|
||||||
test-group = 'ecstore-serial-flaky'
|
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]]
|
[[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))'
|
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'
|
test-group = 'ecstore-serial-flaky'
|
||||||
|
|
||||||
[[profile.ci.overrides]]
|
|
||||||
filter = 'package(rustfs-ecstore) & test(/^store::init::tests::(decommission_|suspended_.*decommission)$/)'
|
|
||||||
test-group = 'ecstore-serial-flaky'
|
|
||||||
|
|
||||||
# Serialize the bucket-incarnation / lifecycle-fence tests under the ci profile
|
# Serialize the bucket-incarnation / lifecycle-fence tests under the ci profile
|
||||||
# too (see the matching default-profile override near the top). No retries.
|
# too (see the matching default-profile override near the top). No retries.
|
||||||
[[profile.ci.overrides]]
|
[[profile.ci.overrides]]
|
||||||
@@ -362,8 +243,7 @@ test-group = 'ecstore-serial-flaky'
|
|||||||
# allowlist", so any new replication test lands in nightly by default (never
|
# 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
|
# silently unrun) until it is explicitly blessed as fast here. Keep the two
|
||||||
# regexes byte-identical. The committed profile selection digests make changes
|
# regexes byte-identical. The committed profile selection digests make changes
|
||||||
# visible in CI; list current membership with `cargo nextest list -p e2e_test
|
# visible in CI; current counts live in docs/testing/e2e-suite-inventory.md.
|
||||||
# --profile <profile>` (platform-dependent; see docs/testing/README.md).
|
|
||||||
# HISTORY (2026-07-11): the 20 fast tests were briefly pulled out of this lane
|
# 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
|
# (#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 —
|
# SSRF egress guard rejected on every PR after repl-1 (#4712). That is fixed —
|
||||||
@@ -401,32 +281,13 @@ test-group = 'ecstore-serial-flaky'
|
|||||||
# rustfs/rustfs#5169 disabled them) have PR-lane signal, not just merge-gate.
|
# 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
|
# Single-node servers on random ports with isolated temp dirs — meets the
|
||||||
# admission criteria unchanged.
|
# 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]
|
[profile.e2e-smoke]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test) & (
|
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|data_usage)_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)_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(/^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::lifecycle::/)
|
||||||
| test(/^reliant::tiering::/)
|
| 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
|
fail-fast = false
|
||||||
@@ -466,10 +327,6 @@ slow-timeout = { period = "60s", terminate-after = 2, grace-period = "10s" }
|
|||||||
# until it is explicitly promoted to the fast PR subset — no replication test
|
# until it is explicitly promoted to the fast PR subset — no replication test
|
||||||
# is ever silently left out of CI.
|
# 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
|
# #[serial] does NOT serialize under nextest (process-per-test; see the file
|
||||||
# header). These tests need no cross-test serialization: each spawns its own
|
# 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
|
# server(s) on random ports with isolated temp dirs, so they are parallel-safe
|
||||||
@@ -487,7 +344,7 @@ slow-timeout = { period = "60s", terminate-after = 2, grace-period = "10s" }
|
|||||||
[profile.e2e-repl-nightly]
|
[profile.e2e-repl-nightly]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test)
|
package(e2e_test)
|
||||||
& (test(/^replication_extension_test::/) | test(/^replication_target_matrix_test::/))
|
& test(/^replication_extension_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)$/)
|
& !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
|
fail-fast = false
|
||||||
@@ -500,29 +357,15 @@ path = "junit.xml"
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# e2e-nightly profile — destructive multi-process cluster fault domains
|
# e2e-nightly profile — destructive multi-process cluster fault domains
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# These eight modules are deliberately outside e2e-full's merge budget. Each
|
# These seven modules are deliberately outside e2e-full's merge budget. Each
|
||||||
# starts a real multi-process or multi-disk topology and exercises node/disk
|
# starts a real multi-process or multi-disk topology and exercises node/disk
|
||||||
# loss, quorum, cleanup, notification fan-in, or admin-timeout behavior. The
|
# loss, quorum, cleanup, notification fan-in, or admin-timeout behavior. The
|
||||||
# consolidated nightly workflow runs them serially to avoid resource
|
# consolidated nightly workflow runs them serially to avoid resource
|
||||||
# starvation; failures are never retried.
|
# 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]
|
[profile.e2e-nightly]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test)
|
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
|
fail-fast = false
|
||||||
|
|
||||||
@@ -533,55 +376,6 @@ path = "junit.xml"
|
|||||||
filter = 'package(e2e_test)'
|
filter = 'package(e2e_test)'
|
||||||
test-group = 'e2e-cluster-nightly'
|
test-group = 'e2e-cluster-nightly'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 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
|
# e2e-protocols profile — serial protocol lane
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -602,27 +396,16 @@ path = "junit.xml"
|
|||||||
# quota, checksum, encryption,
|
# quota, checksum, encryption,
|
||||||
# security-boundary, ... — that the fast PR `e2e-smoke` subset deliberately
|
# security-boundary, ... — that the fast PR `e2e-smoke` subset deliberately
|
||||||
# skips. Budget <= 45 min; authority for the suite count is `cargo nextest list
|
# skips. Budget <= 45 min; authority for the suite count is `cargo nextest list
|
||||||
# --profile e2e-full -p e2e_test` (platform-dependent; see docs/testing/README.md).
|
# --profile e2e-full` (see docs/testing/e2e-suite-inventory.md).
|
||||||
#
|
#
|
||||||
# The filter is "the whole e2e_test crate MINUS the sets owned by other lanes":
|
# 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
|
# * protocols:: — FTPS/SFTP/WebDAV, run from the dedicated protocol profile
|
||||||
# with one worker because the suite owns fixed ports.
|
# with one worker because the suite owns fixed ports.
|
||||||
# * cluster suites that spin up a RustFSTestClusterEnvironment
|
# * the 7 cluster suites that spin up a RustFSTestClusterEnvironment
|
||||||
# (cluster_concurrency, cluster_multidrive_pool, stale_multipart_cleanup_cluster,
|
# (cluster_concurrency, cluster_multidrive_pool, stale_multipart_cleanup_cluster,
|
||||||
# namespace_lock_quorum, admin_timeout_regression, object_lambda) — too
|
# namespace_lock_quorum, heal_erasure_disk_rebuild, admin_timeout_regression,
|
||||||
# heavy for the merge budget; they run in the e2e-nightly serial
|
# object_lambda) — too heavy for the merge budget; they run in the
|
||||||
# cluster-fault lane. heal_erasure_disk_rebuild is intentionally not
|
# e2e-nightly serial cluster-fault lane.
|
||||||
# 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
|
# * replication_extension_test — repl-1 already splits it into the PR
|
||||||
# `e2e-smoke` (20 fast) and `e2e-repl-nightly` (56 slow) lanes and reserves
|
# `e2e-smoke` (20 fast) and `e2e-repl-nightly` (56 slow) lanes and reserves
|
||||||
# it for those, so e2e-full does not double-run it.
|
# it for those, so e2e-full does not double-run it.
|
||||||
@@ -634,15 +417,23 @@ path = "junit.xml"
|
|||||||
# parallel-safe — the same property e2e-smoke relies on. The exceptions are the
|
# 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
|
# 4-disk reliability / degraded-read fault-injection tests and the fixed-port
|
||||||
# Vault tests, both serialized below.
|
# 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.
|
||||||
[profile.e2e-full]
|
[profile.e2e-full]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test)
|
package(e2e_test)
|
||||||
& !test(/^protocols::/)
|
& !test(/^protocols::/)
|
||||||
& !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(/^(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(/^distributed::/)
|
|
||||||
& !test(/^replication_extension_test::/)
|
& !test(/^replication_extension_test::/)
|
||||||
& !test(/^replication_target_matrix_test::/)
|
& !test(/^multipart_auth_test::test_signed_put_object_extract_skips_invalid_entry_when_ignore_errors_enabled$/)
|
||||||
& !test(/^on_demand_migration::(concurrency_test|fault_test|interop_test|real_source_test)::/)
|
& !test(/^snowball_auto_extract_test::tests::snowball_auto_extract_(ignores_invalid_entries_when_requested|supports_standard_headers_with_combined_extract_options)$/)
|
||||||
"""
|
"""
|
||||||
fail-fast = false
|
fail-fast = false
|
||||||
|
|
||||||
@@ -663,5 +454,5 @@ filter = 'package(e2e_test) & test(/^inline_fast_path_cluster_test::/)'
|
|||||||
test-group = 'e2e-inline-boundaries'
|
test-group = 'e2e-inline-boundaries'
|
||||||
|
|
||||||
[[profile.e2e-full.overrides]]
|
[[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$/))'
|
filter = 'package(e2e_test) & (test(/^kms::kms_vault_test::/) | test(/^kms::configured_roundtrip_test::test_configured_vault_kms_admin_and_versioned_cleanup$/))'
|
||||||
test-group = 'e2e-vault'
|
test-group = 'e2e-vault'
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": 1,
|
|
||||||
"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},
|
|
||||||
"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},
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"release_pending": {
|
|
||||||
"G01": "W02/W04 complete root and quota authority coverage",
|
|
||||||
"G02": "W03 bounded checkpoint progress and independent version inventory",
|
|
||||||
"G03": "W17/W18 exact scoped ACK with durable publication and mixed peers",
|
|
||||||
"G04": "W03/W15/W16 crash at every cache/root/floor/intent boundary",
|
|
||||||
"G05": "W06/W07 per-object outcomes and bounded terminal retention",
|
|
||||||
"G06": "W06/W08/W23 concurrent status, legacy clients and truncation",
|
|
||||||
"G07": "W12/W13/W14 durable MRF responsibility at every commit boundary",
|
|
||||||
"G08": "W12/W13/W14 MRF capacity, disk-full and replica-loss matrix",
|
|
||||||
"G09": "W13/W18/W23 actual mixed-version reader/writer and rollback payloads",
|
|
||||||
"G10": "W05/W09/W10/W11 bounded scheduling and pressure recovery",
|
|
||||||
"G11": "W04/W19/W24 maintenance and complete producer coverage",
|
|
||||||
"G12": "W02/W15/W16 both quota paths during reset and settlement",
|
|
||||||
"G13": "W07/W14 quorum-minus-one, unknown disks, remount, Object Lock, dry-run, grace and commit tail",
|
|
||||||
"G14": "W20/W21 same-window field evidence; 3x4 EC8+4 and multi-set/pool coverage",
|
|
||||||
"P1": "W20 measured cold-walk share and foreground latency/throughput",
|
|
||||||
"P2": "W20/W24 measured post-stop convergence and cold segment reuse",
|
|
||||||
"P3": "W20 measured two-hour pressure/heal capacity and recovery window",
|
|
||||||
"P4": "W20 measured MRF scale and replay cost with retained responsibility",
|
|
||||||
"R-E": "W03/W05 fixed-budget real process restart through enumeration and classification",
|
|
||||||
"R-D": "W07/W14 manager-to-event-to-ledger exact disposition, including grace",
|
|
||||||
"R-L": "W13/W14 legacy source conflicts, migration gaps and crash-safe source retirement"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,4 +9,4 @@
|
|||||||
# if the selected count drops below this number, so a rename or removal that
|
# 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.
|
# 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.
|
# Adding tests does not require a bump, but bumping keeps the guard tight.
|
||||||
18
|
16
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
global:
|
|
||||||
scrape_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
|
|
||||||
evaluation_interval: 15s
|
|
||||||
external_labels:
|
|
||||||
cluster: 'rustfs-dev' # Label to identify the cluster
|
|
||||||
replica: '1' # Replica identifier
|
|
||||||
|
|
||||||
rule_files:
|
|
||||||
- /etc/prometheus/rules/*.yml
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: 'otel-collector'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'otel-collector:8888' ] # Scrape metrics from Collector
|
|
||||||
scrape_interval: 10s
|
|
||||||
|
|
||||||
- job_name: 'rustfs-app-metrics'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'otel-collector:8889' ] # Application indicators
|
|
||||||
scrape_interval: 15s
|
|
||||||
metric_relabel_configs:
|
|
||||||
- source_labels: [ __name__ ]
|
|
||||||
regex: 'go_.*'
|
|
||||||
action: drop # Drop Go runtime metrics if not needed
|
|
||||||
|
|
||||||
- job_name: 'tempo'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'tempo:3200' ] # Scrape metrics from Tempo
|
|
||||||
|
|
||||||
- job_name: 'jaeger'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'jaeger:14269' ] # Jaeger admin port (14269 is standard for admin/metrics)
|
|
||||||
|
|
||||||
- job_name: 'loki'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'loki:3100' ]
|
|
||||||
|
|
||||||
- job_name: 'prometheus'
|
|
||||||
static_configs:
|
|
||||||
- targets: [ 'localhost:9090' ]
|
|
||||||
|
|
||||||
- job_name: 'vulture'
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- 'vulture:8080'
|
|
||||||
|
|
||||||
otlp:
|
|
||||||
promote_resource_attributes:
|
|
||||||
- service.instance.id
|
|
||||||
- service.name
|
|
||||||
- service.namespace
|
|
||||||
- cloud.availability_zone
|
|
||||||
- cloud.region
|
|
||||||
- container.name
|
|
||||||
- deployment.environment.name
|
|
||||||
- k8s.cluster.name
|
|
||||||
- k8s.container.name
|
|
||||||
- k8s.cronjob.name
|
|
||||||
- k8s.daemonset.name
|
|
||||||
- k8s.deployment.name
|
|
||||||
- k8s.job.name
|
|
||||||
- k8s.namespace.name
|
|
||||||
- k8s.pod.name
|
|
||||||
- k8s.replicaset.name
|
|
||||||
- k8s.statefulset.name
|
|
||||||
translation_strategy: NoUTF8EscapingWithSuffixes
|
|
||||||
|
|
||||||
storage:
|
|
||||||
tsdb:
|
|
||||||
out_of_order_time_window: 30m
|
|
||||||
@@ -5,5 +5,3 @@ self-hosted-runner:
|
|||||||
- sm-standard-2
|
- sm-standard-2
|
||||||
- sm-standard-4
|
- sm-standard-4
|
||||||
- dind-sm-standard-2
|
- dind-sm-standard-2
|
||||||
- smoke-testing
|
|
||||||
- pf-testing
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# 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 CI paths stay in sync
|
|
||||||
shell: bash
|
|
||||||
run: ./scripts/check_ci_paths_sync.sh
|
|
||||||
|
|
||||||
- name: Check io_uring lane --lib precondition
|
|
||||||
shell: bash
|
|
||||||
run: ./scripts/check_uring_lane_lib_only.sh
|
|
||||||
@@ -10,16 +10,16 @@ Use N/A when there is no related issue.
|
|||||||
|
|
||||||
## Summary of Changes
|
## Summary of Changes
|
||||||
<!--
|
<!--
|
||||||
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.
|
Briefly explain what changed and why reviewers should accept it.
|
||||||
|
Focus on behavior, compatibility, and review-relevant context.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
<!--
|
<!--
|
||||||
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.
|
List the commands or checks you ran, for example:
|
||||||
|
- `make pre-commit`
|
||||||
|
|
||||||
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.
|
Use N/A only when verification is not applicable.
|
||||||
|
|
||||||
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
|
## Impact
|
||||||
|
|||||||
@@ -4,24 +4,11 @@
|
|||||||
{ "workflow": ".github/workflows/ci.yml", "max_age_hours": 192 },
|
{ "workflow": ".github/workflows/ci.yml", "max_age_hours": 192 },
|
||||||
{ "workflow": ".github/workflows/coverage.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-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/e2e-s3tests.yml", "max_age_hours": 192 },
|
||||||
{ "workflow": ".github/workflows/fuzz.yml", "max_age_hours": 36 },
|
{ "workflow": ".github/workflows/fuzz.yml", "max_age_hours": 36 },
|
||||||
{ "workflow": ".github/workflows/mint.yml", "max_age_hours": 192 },
|
{ "workflow": ".github/workflows/mint.yml", "max_age_hours": 192 },
|
||||||
{
|
{ "workflow": ".github/workflows/minio-interop.yml", "max_age_hours": 36 },
|
||||||
"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/nightly-gnu.yml", "max_age_hours": 36 },
|
||||||
{ "workflow": ".github/workflows/performance-ab.yml", "max_age_hours": 36 },
|
{ "workflow": ".github/workflows/performance-ab.yml", "max_age_hours": 36 },
|
||||||
{
|
{ "workflow": ".github/workflows/runner-hygiene.yml", "max_age_hours": 792 }
|
||||||
"workflow": ".github/workflows/runner-hygiene.yml",
|
|
||||||
"max_age_hours": 792,
|
|
||||||
"never_ran_grace_until": "2026-09-02T06:37:00Z"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,11 +24,8 @@ on:
|
|||||||
- '.github/actions/**'
|
- '.github/actions/**'
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
- 'scripts/release/create_or_update_release.sh'
|
- '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_performance_ab_workflow.sh'
|
||||||
- 'scripts/security/check_preview_release_workflow.sh'
|
- 'scripts/security/check_preview_release_workflow.sh'
|
||||||
- 'scripts/security/check_tier_artifact_workflow.sh'
|
|
||||||
- 'scripts/security/check_workflow_pins.sh'
|
- 'scripts/security/check_workflow_pins.sh'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize, reopened, closed ]
|
types: [ opened, synchronize, reopened, closed ]
|
||||||
@@ -40,11 +37,8 @@ on:
|
|||||||
- '.github/actions/**'
|
- '.github/actions/**'
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
- 'scripts/release/create_or_update_release.sh'
|
- '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_performance_ab_workflow.sh'
|
||||||
- 'scripts/security/check_preview_release_workflow.sh'
|
- 'scripts/security/check_preview_release_workflow.sh'
|
||||||
- 'scripts/security/check_tier_artifact_workflow.sh'
|
|
||||||
- 'scripts/security/check_workflow_pins.sh'
|
- 'scripts/security/check_workflow_pins.sh'
|
||||||
schedule:
|
schedule:
|
||||||
# Daily, not weekly. This schedule exists to catch RustSec advisories
|
# Daily, not weekly. This schedule exists to catch RustSec advisories
|
||||||
@@ -152,12 +146,6 @@ jobs:
|
|||||||
- name: Check performance A/B workflow trust boundary
|
- name: Check performance A/B workflow trust boundary
|
||||||
run: ./scripts/security/check_performance_ab_workflow.sh
|
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:
|
dependency-review:
|
||||||
name: Dependency Review
|
name: Dependency Review
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ jobs:
|
|||||||
needs: [ build-check, prepare-platform-matrix ]
|
needs: [ build-check, prepare-platform-matrix ]
|
||||||
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
|
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 180
|
timeout-minutes: 150
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
# Release binaries ship without dial9 telemetry and therefore do not need
|
# Release binaries ship without dial9 telemetry and therefore do not need
|
||||||
@@ -408,9 +408,9 @@ jobs:
|
|||||||
|
|
||||||
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
||||||
# All cross targets in the matrix are Linux; zigbuild handles them.
|
# All cross targets in the matrix are Linux; zigbuild handles them.
|
||||||
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bin rustfs
|
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
|
||||||
else
|
else
|
||||||
cargo build --release --target ${{ matrix.target }} -p rustfs --bin rustfs
|
cargo build --release --target ${{ matrix.target }} -p rustfs --bins
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create release package
|
- name: Create release package
|
||||||
@@ -1033,55 +1033,6 @@ jobs:
|
|||||||
echo "🎉 Released $TAG successfully!"
|
echo "🎉 Released $TAG successfully!"
|
||||||
echo "📄 Release URL: ${{ needs.create-release.outputs.release_url }}"
|
echo "📄 Release URL: ${{ needs.create-release.outputs.release_url }}"
|
||||||
|
|
||||||
# Remove the internal preview releases once the deliverable release is live.
|
|
||||||
# Only the Releases are deleted; the -preview.N tags stay so the validated
|
|
||||||
# commit remains traceable.
|
|
||||||
cleanup-preview-releases:
|
|
||||||
name: Cleanup Preview Releases
|
|
||||||
needs: [ build-check, publish-release ]
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Delete preview releases for this target
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TAG="${{ needs.build-check.outputs.version }}"
|
|
||||||
RELEASES_JSON="${RUNNER_TEMP}/releases.json"
|
|
||||||
|
|
||||||
# Fetch before filtering: a failed listing must abort here instead of
|
|
||||||
# looking like "nothing to clean up".
|
|
||||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100" > "$RELEASES_JSON"
|
|
||||||
|
|
||||||
# Match only <target>-preview.<digits>. String operations, not a
|
|
||||||
# regex over the tag, so dots in the version cannot widen the match.
|
|
||||||
DELETED=0
|
|
||||||
while IFS= read -r preview_tag; do
|
|
||||||
[[ -n "$preview_tag" ]] || continue
|
|
||||||
echo "🧹 Deleting preview release $preview_tag (tag kept)"
|
|
||||||
gh release delete "$preview_tag" --repo "${GITHUB_REPOSITORY}" --yes
|
|
||||||
DELETED=$((DELETED + 1))
|
|
||||||
done < <(
|
|
||||||
jq -r --arg tag "$TAG" '
|
|
||||||
.[]
|
|
||||||
| select(.tag_name | startswith($tag + "-preview."))
|
|
||||||
| select(.tag_name | ltrimstr($tag + "-preview.") | test("^[0-9]+$"))
|
|
||||||
| .tag_name
|
|
||||||
' "$RELEASES_JSON"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$DELETED" -eq 0 ]]; then
|
|
||||||
echo "ℹ️ No preview releases to clean up for $TAG"
|
|
||||||
else
|
|
||||||
echo "✅ Removed $DELETED preview release(s) for $TAG"
|
|
||||||
fi
|
|
||||||
|
|
||||||
alert-on-failure:
|
alert-on-failure:
|
||||||
name: Alert on scheduled failure
|
name: Alert on scheduled failure
|
||||||
needs: [build-check, prepare-platform-matrix, build-rustfs, build-summary]
|
needs: [build-check, prepare-platform-matrix, build-rustfs, build-summary]
|
||||||
|
|||||||
@@ -212,9 +212,6 @@ jobs:
|
|||||||
install-build-packaging-tools: 'false'
|
install-build-packaging-tools: 'false'
|
||||||
|
|
||||||
- name: Build ci-feat-rio superset
|
- name: Build ci-feat-rio superset
|
||||||
env:
|
|
||||||
# --all-targets links the same test binaries as the reader lane.
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
run: |
|
||||||
cargo build -p rustfs -p rustfs-ecstore --all-targets --features rio-v2
|
cargo build -p rustfs -p rustfs-ecstore --all-targets --features rio-v2
|
||||||
cargo build -p rustfs --bins --features rio-v2,e2e-test-hooks
|
cargo build -p rustfs --bins --features rio-v2,e2e-test-hooks
|
||||||
@@ -243,9 +240,6 @@ jobs:
|
|||||||
install-build-packaging-tools: 'false'
|
install-build-packaging-tools: 'false'
|
||||||
|
|
||||||
- name: Build ci-feat-proto superset
|
- name: Build ci-feat-proto superset
|
||||||
env:
|
|
||||||
# Avoid an unbounded burst of concurrent test-binary links (#5394).
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
run: |
|
||||||
cargo build -p rustfs -p rustfs-protocols --all-targets --features swift
|
cargo build -p rustfs -p rustfs-protocols --all-targets --features swift
|
||||||
cargo build -p rustfs -p rustfs-protocols --all-targets --features sftp
|
cargo build -p rustfs -p rustfs-protocols --all-targets --features sftp
|
||||||
|
|||||||
@@ -12,10 +12,24 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# Reports the existing required checks for paths excluded by ci.yml.
|
# Companion to ci.yml for required status checks.
|
||||||
# Mixed PRs can trigger both workflows; their Quick Checks jobs use one shared
|
#
|
||||||
# action to keep validation coverage aligned. Keep this paths list in sync with
|
# ci.yml skips docs-only pull requests via paths-ignore, but the branch ruleset
|
||||||
# ci.yml's pull_request.paths-ignore via scripts/check_ci_paths_sync.sh.
|
# 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)
|
name: Continuous Integration (docs only)
|
||||||
|
|
||||||
@@ -45,6 +59,19 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
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:
|
quick-checks:
|
||||||
name: Quick Checks
|
name: Quick Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -55,8 +82,63 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run shared quick checks
|
- name: Install ripgrep
|
||||||
uses: ./.github/actions/quick-checks
|
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:
|
test-and-lint:
|
||||||
name: Test and Lint
|
name: Test and Lint
|
||||||
|
|||||||
+107
-165
@@ -100,7 +100,12 @@ jobs:
|
|||||||
- name: Typos check with custom config file
|
- name: Typos check with custom config file
|
||||||
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
|
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
|
||||||
|
|
||||||
# Fail early with compile-free checks shared with docs-only CI.
|
# 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.
|
||||||
quick-checks:
|
quick-checks:
|
||||||
name: Quick Checks
|
name: Quick Checks
|
||||||
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
||||||
@@ -112,8 +117,63 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run shared quick checks
|
- name: Install ripgrep
|
||||||
uses: ./.github/actions/quick-checks
|
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:
|
test-and-lint:
|
||||||
name: Test and Lint
|
name: Test and Lint
|
||||||
@@ -121,14 +181,22 @@ jobs:
|
|||||||
needs: [ quick-checks ]
|
needs: [ quick-checks ]
|
||||||
runs-on: sm-standard-4
|
runs-on: sm-standard-4
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
# Both lines are required. Job-level `permissions` replaces the workflow
|
||||||
|
# block rather than merging with it, so declaring only `actions: write`
|
||||||
|
# would drop `contents: read` and break this job's checkout and the
|
||||||
|
# repo-token the setup action hands to setup-protoc.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||||
with:
|
with:
|
||||||
# Checkout otherwise writes the token into .git/config, where a PR's
|
# This job's token can cancel runs and delete Actions caches. Checkout
|
||||||
# own build.rs or proc-macro could read it back out.
|
# otherwise writes it into .git/config, where a PR's own build.rs or
|
||||||
|
# proc-macro could read it back out.
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Rust environment
|
- name: Setup Rust environment
|
||||||
@@ -144,9 +212,6 @@ jobs:
|
|||||||
cache-save-if: 'false'
|
cache-save-if: 'false'
|
||||||
install-build-packaging-tools: 'false'
|
install-build-packaging-tools: 'false'
|
||||||
|
|
||||||
- name: Protect Connect test home
|
|
||||||
run: chmod go-w "$(realpath "$HOME")"
|
|
||||||
|
|
||||||
- name: Prepare test evidence
|
- name: Prepare test evidence
|
||||||
run: |
|
run: |
|
||||||
mkdir -p artifacts/test-and-lint
|
mkdir -p artifacts/test-and-lint
|
||||||
@@ -206,7 +271,6 @@ jobs:
|
|||||||
CARGO_BUILD_JOBS: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && '3' || '2' }}
|
CARGO_BUILD_JOBS: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && '3' || '2' }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p artifacts/test-and-lint
|
mkdir -p artifacts/test-and-lint
|
||||||
rm -f target/nextest/ci/junit.xml
|
|
||||||
./scripts/ci/resource_sampler.sh start nextest
|
./scripts/ci/resource_sampler.sh start nextest
|
||||||
trap './scripts/ci/resource_sampler.sh stop' EXIT
|
trap './scripts/ci/resource_sampler.sh stop' EXIT
|
||||||
set +e
|
set +e
|
||||||
@@ -215,12 +279,6 @@ jobs:
|
|||||||
--status-level all --final-status-level all \
|
--status-level all --final-status-level all \
|
||||||
2>&1 | tee artifacts/test-and-lint/nextest.log
|
2>&1 | tee artifacts/test-and-lint/nextest.log
|
||||||
status=${PIPESTATUS[0]}
|
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 "command=cargo nextest run --profile ci --all --exclude e2e_test"
|
||||||
echo "exit_status=${status}"
|
echo "exit_status=${status}"
|
||||||
@@ -252,9 +310,6 @@ jobs:
|
|||||||
} > artifacts/test-and-lint/doctest-diagnostics.txt
|
} > artifacts/test-and-lint/doctest-diagnostics.txt
|
||||||
exit "${status}"
|
exit "${status}"
|
||||||
|
|
||||||
- name: Check offline enrollment E2E root boundary
|
|
||||||
run: ./scripts/check_offline_enrollment_e2e.sh
|
|
||||||
|
|
||||||
- name: Upload test reports and diagnostics
|
- name: Upload test reports and diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
@@ -289,36 +344,41 @@ jobs:
|
|||||||
- name: Run rebalance/decommission migration proofs
|
- name: Run rebalance/decommission migration proofs
|
||||||
run: ./scripts/check_migration_gate_count.sh
|
run: ./scripts/check_migration_gate_count.sh
|
||||||
|
|
||||||
# Record the reason before this job completes as FAILURE. A separate
|
# Early stop. Once this job has failed the PR cannot merge, so the sibling
|
||||||
# dependent job cancels sibling lanes only after GitHub has preserved this
|
# lanes are burning runners on a result nobody can act on: on run
|
||||||
# required check's failure verdict.
|
# 30674613104 three lanes had already failed while Test and Lint and the
|
||||||
|
# rio-v2 variant kept going past 70 minutes.
|
||||||
|
#
|
||||||
|
# Only this job may cancel. The lanes that are NOT required checks
|
||||||
|
# (protocols, ILM, e2e, s3-tests) must never hold that power: a flake in
|
||||||
|
# one of them would turn the required "Test and Lint" into `cancelled`,
|
||||||
|
# which blocks the merge. Today a maintainer can merge with sftp red, and
|
||||||
|
# that has to stay true.
|
||||||
|
#
|
||||||
|
# These steps run last so the `if: always()` artifact upload above still
|
||||||
|
# captures logs and diagnostics before the run goes away.
|
||||||
- name: Annotate early-stop reason
|
- name: Annotate early-stop reason
|
||||||
if: >-
|
if: failure() && github.event_name == 'pull_request'
|
||||||
failure() && github.event_name == 'pull_request'
|
|
||||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
|
||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo "## CI early-stop"
|
echo "## CI early-stop"
|
||||||
echo "Job \`${GITHUB_JOB}\` (Test and Lint) failed; a follow-up job will cancel sibling lanes to free runners."
|
echo "Job \`${GITHUB_JOB}\` (Test and Lint) failed; cancelling run ${GITHUB_RUN_ID} to free runners."
|
||||||
echo "Sibling jobs showing **cancelled** were stopped by the early-stop follow-up, not by their own failure."
|
echo "Sibling jobs showing **cancelled** were stopped by this job, not by their own failure."
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# Preserve the required Test and Lint FAILURE verdict before stopping sibling
|
# curl rather than `gh`: every existing `gh` call in this repo runs on
|
||||||
# lanes. Cancelling from inside test-and-lint changed its own conclusion to
|
# ubuntu-latest, and the sm-standard-* images are custom and trimmed (they
|
||||||
# CANCELLED and hid the actionable failure in the PR checks UI.
|
# ship no C toolchain, see the e2e job below), so `gh` is not known to
|
||||||
cancel-after-test-and-lint-failure:
|
# exist here.
|
||||||
name: Cancel siblings after Test and Lint failure
|
#
|
||||||
if: >-
|
# Fork PRs are excluded explicitly instead of relying on the error path:
|
||||||
failure() && needs.test-and-lint.result == 'failure'
|
# their GITHUB_TOKEN is forced read-only and job-level permissions cannot
|
||||||
&& github.event_name == 'pull_request'
|
# raise it, so the call would always 403. Skipping keeps their logs clean.
|
||||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
- name: Cancel run on failure (same-repo PR only)
|
||||||
needs: [ test-and-lint ]
|
if: >-
|
||||||
runs-on: ubuntu-latest
|
failure() && github.event_name == 'pull_request'
|
||||||
timeout-minutes: 5
|
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||||
permissions:
|
continue-on-error: true
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Cancel remaining jobs
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -326,7 +386,7 @@ jobs:
|
|||||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Accept: application/vnd.github+json" \
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/cancel"
|
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/cancel" || true
|
||||||
|
|
||||||
# Dedicated serial lane for the ILM / lifecycle integration tests. These tests
|
# Dedicated serial lane for the ILM / lifecycle integration tests. These tests
|
||||||
# drive the object layer through process-global singletons (the GLOBAL_ENV
|
# drive the object layer through process-global singletons (the GLOBAL_ENV
|
||||||
@@ -373,38 +433,10 @@ jobs:
|
|||||||
# - test_noncurrent_{expiry,transition}_still_works_after_immediate_compensation_transition:
|
# - test_noncurrent_{expiry,transition}_still_works_after_immediate_compensation_transition:
|
||||||
# noncurrent transition/expiry after an immediate compensation transition.
|
# noncurrent transition/expiry after an immediate compensation transition.
|
||||||
- name: Run ignored ILM integration tests serially
|
- name: Run ignored ILM integration tests serially
|
||||||
env:
|
|
||||||
# Match the measured Test and Lint link budget. The default exposed
|
|
||||||
# all 14 pod CPUs and a cold cache spent the full 80m compiling
|
|
||||||
# without starting one ILM test (main run 32982910990).
|
|
||||||
CARGO_BUILD_JOBS: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && '3' || '2' }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p artifacts/ilm-integration
|
cargo nextest run -j1 --run-ignored ignored-only \
|
||||||
set +e
|
|
||||||
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 \
|
-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))) and not (test(test_noncurrent_expiry_still_works_after_immediate_compensation_transition) or test(test_noncurrent_transition_still_works_after_immediate_compensation_transition))'
|
||||||
--status-level all --final-status-level all \
|
|
||||||
2>&1 | tee artifacts/ilm-integration/nextest.log
|
|
||||||
status=${PIPESTATUS[0]}
|
|
||||||
{
|
|
||||||
echo "exit_status=${status}"
|
|
||||||
echo "finished_at=$(date --utc --iso-8601=seconds)"
|
|
||||||
echo
|
|
||||||
echo "Remaining test-related processes:"
|
|
||||||
pgrep -af 'cargo|nextest|target/.*/deps/' || true
|
|
||||||
} > artifacts/ilm-integration/diagnostics.txt
|
|
||||||
exit "${status}"
|
|
||||||
|
|
||||||
- name: Upload ILM test diagnostics
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: ilm-integration-${{ github.run_number }}-${{ github.run_attempt }}
|
|
||||||
path: |
|
|
||||||
artifacts/ilm-integration
|
|
||||||
target/nextest/ci/junit.xml
|
|
||||||
|
|
||||||
test-and-lint-rio-v2:
|
test-and-lint-rio-v2:
|
||||||
name: Test and Lint (rio-v2)
|
name: Test and Lint (rio-v2)
|
||||||
@@ -428,83 +460,14 @@ jobs:
|
|||||||
cache-save-if: 'false'
|
cache-save-if: 'false'
|
||||||
install-build-packaging-tools: 'false'
|
install-build-packaging-tools: 'false'
|
||||||
|
|
||||||
- name: Protect Connect test home
|
|
||||||
run: chmod go-w "$(realpath "$HOME")"
|
|
||||||
|
|
||||||
- name: Run rio-v2 clippy lints
|
- name: Run rio-v2 clippy lints
|
||||||
run: cargo clippy -p rustfs -p rustfs-ecstore --all-targets --features rio-v2 -- -D warnings
|
run: cargo clippy -p rustfs -p rustfs-ecstore --all-targets --features rio-v2 -- -D warnings
|
||||||
|
|
||||||
- name: Run rio-v2 feature tests
|
- name: Run rio-v2 feature tests
|
||||||
env:
|
|
||||||
# Match the main nextest lane's #5394 link-I/O guard. A cold feature
|
|
||||||
# cache otherwise fans out enough rust-lld processes to exhaust this
|
|
||||||
# job's 90-minute budget before any test starts.
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
run: |
|
||||||
# --profile ci so the quarantine list (and its junit flaky markers)
|
cargo nextest run -p rustfs -p rustfs-ecstore --features rio-v2
|
||||||
# covers this leg too; the default profile is the local no-retry
|
|
||||||
# profile and silently ignored quarantined flakes here (rustfs#6703).
|
|
||||||
cargo nextest run --profile ci -p rustfs -p rustfs-ecstore --features rio-v2
|
|
||||||
cargo test -p rustfs --doc --features rio-v2
|
cargo test -p rustfs --doc --features rio-v2
|
||||||
|
|
||||||
connect-short-credential-boundary:
|
|
||||||
name: Connect Short Credential Boundary
|
|
||||||
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
|
||||||
needs: [ quick-checks ]
|
|
||||||
runs-on: sm-standard-4
|
|
||||||
timeout-minutes: 60
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Rust environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
rust-version: stable
|
|
||||||
cache-shared-key: ci-dev
|
|
||||||
cache-save-if: 'false'
|
|
||||||
install-build-packaging-tools: 'false'
|
|
||||||
install-test-tools: 'false'
|
|
||||||
|
|
||||||
- name: Run short credential behavior tests
|
|
||||||
env:
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
|
||||||
cargo test -p rustfs --test connect_registration \
|
|
||||||
--features connect-e2e-short-credentials \
|
|
||||||
registration_enforces_build_profile_credential_lifetime -- --exact
|
|
||||||
cargo test -p rustfs --test connect_registration \
|
|
||||||
--features connect-e2e-short-credentials \
|
|
||||||
rotation_waits_for_threshold_and_stops_on_revocation -- --exact
|
|
||||||
|
|
||||||
- name: Reject short credentials in release builds
|
|
||||||
env:
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
|
||||||
log="$(mktemp)"
|
|
||||||
set +e
|
|
||||||
CARGO_TERM_COLOR=never cargo check -p rustfs --release \
|
|
||||||
--features connect-e2e-short-credentials >"$log" 2>&1
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
cat "$log"
|
|
||||||
expected='error: connect-e2e-short-credentials is restricted to debug builds'
|
|
||||||
summary="error: could not compile \`rustfs\` (lib) due to 1 previous error"
|
|
||||||
expected_count="$(grep -Fxc "$expected" "$log" || true)"
|
|
||||||
summary_count="$(grep -Fc "$summary" "$log" || true)"
|
|
||||||
error_count="$(grep -Ec '^error(:|\[)' "$log" || true)"
|
|
||||||
if [ "$status" -ne 101 ] || [ "$expected_count" -ne 1 ] \
|
|
||||||
|| [ "$summary_count" -ne 1 ] || [ "$error_count" -ne 2 ]; then
|
|
||||||
echo "release feature gate did not fail solely at the expected compile_error" >&2
|
|
||||||
rm -f "$log"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -f "$log"
|
|
||||||
|
|
||||||
test-and-lint-protocols:
|
test-and-lint-protocols:
|
||||||
name: "Test and Lint (${{ matrix.features.name }})"
|
name: "Test and Lint (${{ matrix.features.name }})"
|
||||||
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
||||||
@@ -541,23 +504,13 @@ jobs:
|
|||||||
cache-save-if: 'false'
|
cache-save-if: 'false'
|
||||||
install-build-packaging-tools: 'false'
|
install-build-packaging-tools: 'false'
|
||||||
|
|
||||||
- name: Protect Connect test home
|
|
||||||
run: chmod go-w "$(realpath "$HOME")"
|
|
||||||
|
|
||||||
- name: Run clippy with ${{ matrix.features.name }}
|
- name: Run clippy with ${{ matrix.features.name }}
|
||||||
run: |
|
run: |
|
||||||
cargo clippy -p rustfs -p rustfs-protocols --all-targets ${{ matrix.features.flags }} -- -D warnings
|
cargo clippy -p rustfs -p rustfs-protocols --all-targets ${{ matrix.features.flags }} -- -D warnings
|
||||||
|
|
||||||
- name: Run tests with ${{ matrix.features.name }}
|
- name: Run tests with ${{ matrix.features.name }}
|
||||||
env:
|
|
||||||
# Keep feature-test linking under the same bounded concurrency as the
|
|
||||||
# main nextest lane; Clippy is metadata-only and needs no such limit.
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
run: |
|
||||||
# --profile ci so the quarantine list (and its junit flaky markers)
|
cargo nextest run -p rustfs -p rustfs-protocols ${{ matrix.features.flags }}
|
||||||
# covers this leg too; the default profile is the local no-retry
|
|
||||||
# profile and silently ignored quarantined flakes here (rustfs#6703).
|
|
||||||
cargo nextest run --profile ci -p rustfs -p rustfs-protocols ${{ matrix.features.flags }}
|
|
||||||
|
|
||||||
build-rustfs-debug-binary:
|
build-rustfs-debug-binary:
|
||||||
name: Build RustFS Debug Binary
|
name: Build RustFS Debug Binary
|
||||||
@@ -850,17 +803,6 @@ jobs:
|
|||||||
cache-save-if: 'false'
|
cache-save-if: 'false'
|
||||||
install-build-packaging-tools: '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
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -121,22 +121,6 @@ jobs:
|
|||||||
create_latest=false
|
create_latest=false
|
||||||
source_ref="$GITHUB_SHA"
|
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
|
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
||||||
# Triggered by build workflow completion
|
# Triggered by build workflow completion
|
||||||
echo "🔗 Triggered by build workflow completion"
|
echo "🔗 Triggered by build workflow completion"
|
||||||
@@ -200,8 +184,8 @@ jobs:
|
|||||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||||
build_type="prerelease"
|
build_type="prerelease"
|
||||||
is_prerelease=true
|
is_prerelease=true
|
||||||
# Pre-GA policy: prereleases update latest until the first stable tag exists.
|
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||||
if prerelease_moves_latest; then
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||||
create_latest=true
|
create_latest=true
|
||||||
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
|
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
|
||||||
else
|
else
|
||||||
@@ -259,8 +243,8 @@ jobs:
|
|||||||
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
||||||
build_type="prerelease"
|
build_type="prerelease"
|
||||||
is_prerelease=true
|
is_prerelease=true
|
||||||
# Pre-GA policy: prereleases update latest until the first stable tag exists.
|
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||||
if prerelease_moves_latest; then
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||||
create_latest=true
|
create_latest=true
|
||||||
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
|
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
|
||||||
else
|
else
|
||||||
@@ -410,13 +394,11 @@ jobs:
|
|||||||
TAG_BASE="${VERSION}${VARIANT_SUFFIX}"
|
TAG_BASE="${VERSION}${VARIANT_SUFFIX}"
|
||||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:$TAG_BASE,${{ env.REGISTRY_GHCR }}:$TAG_BASE,${{ env.REGISTRY_QUAY }}:$TAG_BASE"
|
TAGS="${{ env.REGISTRY_DOCKERHUB }}:$TAG_BASE,${{ env.REGISTRY_GHCR }}:$TAG_BASE,${{ env.REGISTRY_QUAY }}:$TAG_BASE"
|
||||||
|
|
||||||
# Add latest when requested (stable releases, and prereleases before GA)
|
# Add channel tags for prereleases and latest for stable
|
||||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
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}"
|
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
|
||||||
fi
|
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
|
|
||||||
# Always add the channel tag for prereleases, independent of latest
|
|
||||||
if [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
|
||||||
# Prerelease channel tags (alpha, beta, rc)
|
# Prerelease channel tags (alpha, beta, rc)
|
||||||
if [[ "$VERSION" == *"alpha"* ]]; then
|
if [[ "$VERSION" == *"alpha"* ]]; then
|
||||||
CHANNEL="alpha"
|
CHANNEL="alpha"
|
||||||
@@ -573,7 +555,7 @@ jobs:
|
|||||||
"prerelease")
|
"prerelease")
|
||||||
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
||||||
echo "⚠️ This is a prerelease image - use with caution"
|
echo "⚠️ This is a prerelease image - use with caution"
|
||||||
# Prereleases move latest until the first stable tag exists (pre-GA policy).
|
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
|
||||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||||
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
|
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
# 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: |
|
|
||||||
cargo build -p rustfs --bins
|
|
||||||
: > target/debug/rustfs.features
|
|
||||||
|
|
||||||
- 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
|
|
||||||
cargo nextest run --profile e2e-distributed -p e2e_test -E "${FILTER}"
|
|
||||||
else
|
|
||||||
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.GITHUB_TOKEN }}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# 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: Upgrade Compatibility
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
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/**"
|
|
||||||
- "crates/kms/**"
|
|
||||||
- "crates/storage-api/**"
|
|
||||||
- "rustfs/**"
|
|
||||||
- "Cargo.lock"
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "[0-9]*.[0-9]*.[0-9]*"
|
|
||||||
schedule:
|
|
||||||
- cron: "17 3 * * 1"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUST_BACKTRACE: 1
|
|
||||||
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:
|
|
||||||
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:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Rust environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
cache-shared-key: ${{ matrix.cache_key }}
|
|
||||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
install-build-packaging-tools: "false"
|
|
||||||
|
|
||||||
- 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"
|
|
||||||
echo "RUSTFS_E2E_LOG_DIR=$RUNNER_TEMP/rustfs-upgrade-logs" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Build current RustFS binary
|
|
||||||
run: |
|
|
||||||
cargo build --locked -p rustfs --bin rustfs
|
|
||||||
: > target/debug/rustfs.features
|
|
||||||
|
|
||||||
- name: Run upgrade compatibility test
|
|
||||||
run: |
|
|
||||||
cargo test --locked -p e2e_test \
|
|
||||||
"upgrade_compatibility_test::${{ matrix.test }}" \
|
|
||||||
-- --ignored --exact --nocapture
|
|
||||||
|
|
||||||
- name: Upload server logs
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-server-logs-${{ github.run_number }}
|
|
||||||
path: ${{ runner.temp }}/rustfs-upgrade-logs
|
|
||||||
if-no-files-found: warn
|
|
||||||
retention-days: 14
|
|
||||||
@@ -173,7 +173,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
fuzz/artifacts/**
|
fuzz/artifacts/**
|
||||||
fuzz/corpus/${{ matrix.target }}/**
|
fuzz/corpus/${{ matrix.target }}/**
|
||||||
if-no-files-found: error
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
@@ -227,7 +227,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
fuzz/artifacts/**
|
fuzz/artifacts/**
|
||||||
fuzz/corpus/${{ matrix.target }}/**
|
fuzz/corpus/${{ matrix.target }}/**
|
||||||
if-no-files-found: error
|
if-no-files-found: ignore
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -20,27 +20,22 @@
|
|||||||
# each run with Docker and then runs the `#[ignore]` reader tests in
|
# each run with Docker and then runs the `#[ignore]` reader tests in
|
||||||
# rustfs/src/storage/minio_generated_read_test.rs.
|
# rustfs/src/storage/minio_generated_read_test.rs.
|
||||||
#
|
#
|
||||||
# Scope: MinIO-to-RustFS SSE read interop is implemented behind the `rio-v2`
|
# Scope: end-to-end MinIO-to-RustFS SSE interop is NOT implemented yet. Both
|
||||||
# feature for MinIO's builtin static-KMS deployments — SSE-S3 and SSE-KMS
|
# envelope parsers reject MinIO's own wrapped-DEK shape — see
|
||||||
# (single- and multipart) since rustfs/rustfs#6191, SSE-C detection since the
|
# `is_data_key_envelope` in crates/kms/src/encryption/dek.rs and the
|
||||||
# rustfs/backlog#1638 D2 close-out. This job is the standing evidence: it
|
# `deny_unknown_fields` `LocalSseDekEnvelope` in rustfs/src/storage/sse.rs — and
|
||||||
# regenerates real MinIO backend trees and proves byte-identical plaintext
|
# closing that gap is tracked in rustfs/backlog#1638. Treat this job as the
|
||||||
# reconstruction. KES/MinKMS-backed MinIO objects remain unreadable by design
|
# harness for #1638, not as standing evidence that a MinIO migration reads back.
|
||||||
# (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,
|
# Runner: GitHub-hosted `ubuntu-latest`. It reliably ships Docker + Python,
|
||||||
# unlike the self-hosted fleet, whose pods drift in Docker/pip availability
|
# unlike the self-hosted fleet, whose pods drift in Docker/pip availability
|
||||||
# (see the infra note in e2e-s3tests.yml). Nightly + manual only.
|
# (see the infra note in e2e-s3tests.yml). Nightly + manual only.
|
||||||
#
|
# DISABLED. This workflow is switched off in the repository's Actions settings
|
||||||
# Enablement: this workflow was long disabled in the repository's Actions
|
# (state: disabled_manually) and does not run on any trigger, including its cron
|
||||||
# settings (state: disabled_manually — a state that lives in GitHub's UI and is
|
# and workflow_dispatch. That state lives in GitHub's UI and is invisible when
|
||||||
# invisible in this file). The change that updated this banner also re-added
|
# reading this file, which has already misled at least one audit — hence this
|
||||||
# the .github/scheduled-validations.json entry; both only make sense together
|
# banner. Re-enabling is a UI action; anyone doing so should first check that the
|
||||||
# with re-enabling the workflow in the Actions settings. If it is ever disabled
|
# workflow still matches the current CI layout. See rustfs/backlog#1603.
|
||||||
# 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
|
name: minio-interop
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build x86_64 GNU
|
name: Build x86_64 GNU
|
||||||
runs-on: sm-standard-4
|
runs-on: sm-standard-2
|
||||||
timeout-minutes: 150
|
timeout-minutes: 150
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
@@ -55,198 +55,6 @@ jobs:
|
|||||||
- name: Build RustFS
|
- name: Build RustFS
|
||||||
run: cargo build --release --locked --target x86_64-unknown-linux-gnu -p rustfs --bins
|
run: cargo build --release --locked --target x86_64-unknown-linux-gnu -p rustfs --bins
|
||||||
|
|
||||||
- name: Build DEB package
|
|
||||||
id: deb
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Nightly snapshot name: rustfs-nightly-<YYYY-MM-DD> (Asia/Shanghai,
|
|
||||||
# matching the schedule timezone so the file date always matches the
|
|
||||||
# cron's intended day).
|
|
||||||
DEB_DATE="$(TZ=Asia/Shanghai date +%Y-%m-%d)"
|
|
||||||
DEB_FILE="rustfs-nightly-${DEB_DATE}.deb"
|
|
||||||
PKG_DIR="rustfs-nightly-${DEB_DATE}"
|
|
||||||
|
|
||||||
command -v fakeroot >/dev/null 2>&1 || sudo apt-get install -y -qq fakeroot
|
|
||||||
|
|
||||||
BIN="target/x86_64-unknown-linux-gnu/release/rustfs"
|
|
||||||
test -x "${BIN}" || { echo "rustfs binary not found: ${BIN}"; exit 1; }
|
|
||||||
|
|
||||||
mkdir -p "${PKG_DIR}/DEBIAN"
|
|
||||||
mkdir -p "${PKG_DIR}/usr/bin"
|
|
||||||
mkdir -p "${PKG_DIR}/etc/default"
|
|
||||||
mkdir -p "${PKG_DIR}/lib/systemd/system"
|
|
||||||
mkdir -p "${PKG_DIR}/usr/share/doc/rustfs"
|
|
||||||
|
|
||||||
cp "${BIN}" "${PKG_DIR}/usr/bin/rustfs"
|
|
||||||
chmod 755 "${PKG_DIR}/usr/bin/rustfs"
|
|
||||||
cp deploy/build/rustfs.service "${PKG_DIR}/lib/systemd/system/"
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/etc/default/rustfs" << 'ENVEOF'
|
|
||||||
# RustFS Environment Configuration
|
|
||||||
# See https://rustfs.com/docs/ for more information
|
|
||||||
# RUSTFS_VOLUMES=""
|
|
||||||
# RUSTFS_ROOT_USER=""
|
|
||||||
# RUSTFS_ROOT_PASSWORD=""
|
|
||||||
ENVEOF
|
|
||||||
|
|
||||||
# dpkg versions must start with a digit and cannot contain hyphens;
|
|
||||||
# a date-based snapshot version keeps the nightly installable
|
|
||||||
# alongside release packages.
|
|
||||||
DEB_VERSION="${DEB_DATE//-/.}~nightly"
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/DEBIAN/control" << EOF
|
|
||||||
Package: rustfs
|
|
||||||
Version: ${DEB_VERSION}
|
|
||||||
Section: utils
|
|
||||||
Priority: optional
|
|
||||||
Architecture: amd64
|
|
||||||
Depends: libc6 (>= 2.31)
|
|
||||||
Maintainer: RustFS Team <support@rustfs.com>
|
|
||||||
Description: High-performance distributed object storage
|
|
||||||
RustFS is a high-performance distributed object storage software
|
|
||||||
built using Rust. It is compatible with MinIO and S3 API.
|
|
||||||
Homepage: https://rustfs.com
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/DEBIAN/conffiles" << 'CONFFILES'
|
|
||||||
/etc/default/rustfs
|
|
||||||
CONFFILES
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/DEBIAN/postinst" << '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
|
|
||||||
echo "RustFS installed. Configure /etc/default/rustfs then: systemctl start rustfs"
|
|
||||||
POSTINST
|
|
||||||
chmod 755 "${PKG_DIR}/DEBIAN/postinst"
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/DEBIAN/prerm" << 'PRERM'
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
if [ -d /run/systemd/system ] && systemctl is-active --quiet rustfs; then
|
|
||||||
systemctl stop rustfs
|
|
||||||
fi
|
|
||||||
PRERM
|
|
||||||
chmod 755 "${PKG_DIR}/DEBIAN/prerm"
|
|
||||||
|
|
||||||
cat > "${PKG_DIR}/DEBIAN/postrm" << 'POSTRM'
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
if [ -d /run/systemd/system ]; then
|
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
|
||||||
POSTRM
|
|
||||||
chmod 755 "${PKG_DIR}/DEBIAN/postrm"
|
|
||||||
|
|
||||||
cp LICENSE "${PKG_DIR}/usr/share/doc/rustfs/"
|
|
||||||
cp README.md "${PKG_DIR}/usr/share/doc/rustfs/"
|
|
||||||
|
|
||||||
fakeroot dpkg-deb --build "${PKG_DIR}"
|
|
||||||
ls -lh "${DEB_FILE}"
|
|
||||||
echo "deb_file=${DEB_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 }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
# Persist the nightly deb on Cloudflare R2 (same channel as package.yml)
|
|
||||||
# so it can be downloaded later with a stable, unauthenticated URL —
|
|
||||||
# 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
|
|
||||||
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 }}
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
|
||||||
AWS_EC2_METADATA_DISABLED: true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ -z "$R2_ACCESS_KEY_ID" || -z "$R2_SECRET_ACCESS_KEY" || -z "$R2_ENDPOINT" || -z "$R2_BUCKET" ]]; then
|
|
||||||
echo "⚠️ R2 credentials missing, skipping upload"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID"
|
|
||||||
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
|
|
||||||
export AWS_DEFAULT_REGION="auto"
|
|
||||||
|
|
||||||
SOURCE_SHA="$(git rev-parse HEAD)"
|
|
||||||
if [[ "${SOURCE_SHA}" != "${GITHUB_SHA}" ]]; then
|
|
||||||
echo "Checkout SHA does not match the nightly build run" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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_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_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}" \
|
|
||||||
--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: 1, 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
|
|
||||||
|
|
||||||
# Live-Vault lane for the rustfs-kms suite (rustfs/backlog#1774).
|
# 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
|
# RUSTFS_KMS_VAULT_TOKEN is the single switch that adds the Vault KV2 and
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'flake.nix'
|
- 'flake.nix'
|
||||||
- 'flake.lock'
|
- 'flake.lock'
|
||||||
- 'nix/**'
|
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- '.github/workflows/nix.yml'
|
- '.github/workflows/nix.yml'
|
||||||
@@ -37,7 +36,6 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'flake.nix'
|
- 'flake.nix'
|
||||||
- 'flake.lock'
|
- 'flake.lock'
|
||||||
- 'nix/**'
|
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- '.github/workflows/nix.yml'
|
- '.github/workflows/nix.yml'
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
# 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: OIDC Keycloak Live
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/oidc-keycloak.yml"
|
|
||||||
- "crates/config/src/constants/oidc.rs"
|
|
||||||
- "crates/iam/src/federation/**"
|
|
||||||
- "crates/iam/src/oidc.rs"
|
|
||||||
- "rustfs/src/admin/handlers/oidc.rs"
|
|
||||||
- "rustfs/src/admin/handlers/sts.rs"
|
|
||||||
- "scripts/test/oidc_keycloak_live.sh"
|
|
||||||
- "scripts/test/fixtures/keycloak-rustfs-ci-realm.json"
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/oidc-keycloak.yml"
|
|
||||||
- "crates/config/src/constants/oidc.rs"
|
|
||||||
- "crates/iam/src/federation/**"
|
|
||||||
- "crates/iam/src/oidc.rs"
|
|
||||||
- "rustfs/src/admin/handlers/oidc.rs"
|
|
||||||
- "rustfs/src/admin/handlers/sts.rs"
|
|
||||||
- "scripts/test/oidc_keycloak_live.sh"
|
|
||||||
- "scripts/test/fixtures/keycloak-rustfs-ci-realm.json"
|
|
||||||
schedule:
|
|
||||||
- cron: "23 2 * * 1"
|
|
||||||
timezone: "Asia/Shanghai"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: oidc-keycloak-live-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
oidc-keycloak-live:
|
|
||||||
name: OIDC Keycloak live gate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Rust environment
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
cache-shared-key: oidc-keycloak-live
|
|
||||||
cache-save-if: "true"
|
|
||||||
install-build-packaging-tools: "false"
|
|
||||||
install-test-tools: "false"
|
|
||||||
|
|
||||||
- name: Build RustFS
|
|
||||||
run: cargo build --locked -p rustfs --bin rustfs
|
|
||||||
|
|
||||||
- name: Install pinned request signer
|
|
||||||
run: |
|
|
||||||
python3 -m pip install --user --upgrade pip "awscurl==0.44"
|
|
||||||
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
|
|
||||||
|
|
||||||
- name: Run live Keycloak discovery, JWT and STS checks
|
|
||||||
run: bash scripts/test/oidc_keycloak_live.sh ./target/debug/rustfs
|
|
||||||
|
|
||||||
- name: Upload service logs
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: oidc-keycloak-live-${{ github.run_number }}
|
|
||||||
path: ${{ runner.temp }}/rustfs-keycloak-live-*/**/*.log
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 3
|
|
||||||
|
|
||||||
alert-on-failure:
|
|
||||||
name: Alert on scheduled failure
|
|
||||||
needs: oidc-keycloak-live
|
|
||||||
if: >-
|
|
||||||
always() && github.event_name == 'schedule' &&
|
|
||||||
(needs.oidc-keycloak-live.result == 'failure' || needs.oidc-keycloak-live.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.GITHUB_TOKEN }}
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
# 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: cargo build --locked -p rustfs --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: 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: cargo build --locked -p rustfs --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: |
|
|
||||||
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.GITHUB_TOKEN }}
|
|
||||||
+99
-172
@@ -21,10 +21,10 @@
|
|||||||
# - workflow_run: automatically package after "Build and Release" completes
|
# - workflow_run: automatically package after "Build and Release" completes
|
||||||
# for a release tag (the mac/windows/linux binaries are already uploaded
|
# for a release tag (the mac/windows/linux binaries are already uploaded
|
||||||
# to the GitHub release before packaging starts)
|
# to the GitHub release before packaging starts)
|
||||||
# - workflow_dispatch: manual fallback with a release tag and/or exact build run ID
|
# - workflow_dispatch: manual fallback (backfill / re-run) with optional tag/run_id
|
||||||
#
|
#
|
||||||
# Flow:
|
# Flow:
|
||||||
# 1. Resolve and validate the selected Build workflow run and source identity
|
# 1. Resolve the triggering Build workflow run for the release tag
|
||||||
# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu) from build artifacts
|
# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu) from build artifacts
|
||||||
# 3. Build DEB packages for amd64 and arm64
|
# 3. Build DEB packages for amd64 and arm64
|
||||||
# 4. Build RPM packages for x86_64 and aarch64
|
# 4. Build RPM packages for x86_64 and aarch64
|
||||||
@@ -51,7 +51,7 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
build_run_id:
|
build_run_id:
|
||||||
description: "Build workflow run ID (when combined with tag, both must identify the same release commit)"
|
description: "Build workflow run ID (overrides tag lookup)"
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -82,9 +82,6 @@ jobs:
|
|||||||
version: ${{ steps.resolve.outputs.version }}
|
version: ${{ steps.resolve.outputs.version }}
|
||||||
build_type: ${{ steps.resolve.outputs.build_type }}
|
build_type: ${{ steps.resolve.outputs.build_type }}
|
||||||
build_run_id: ${{ steps.resolve.outputs.build_run_id }}
|
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 }}
|
tag: ${{ steps.resolve.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve build run
|
- name: Resolve build run
|
||||||
@@ -92,129 +89,90 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
|
||||||
REPOSITORY: ${{ github.repository }}
|
|
||||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||||
INPUT_RUN_ID: ${{ github.event.inputs.build_run_id }}
|
INPUT_RUN_ID: ${{ github.event.inputs.build_run_id }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
fail() {
|
# Determine tag
|
||||||
echo "❌ $1" >&2
|
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
||||||
exit 1
|
TAG="${HEAD_BRANCH}"
|
||||||
}
|
elif [[ -n "$INPUT_TAG" ]]; then
|
||||||
|
TAG="$INPUT_TAG"
|
||||||
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
|
else
|
||||||
BUILD_TYPE=development
|
TAG=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$BUILD_RUN_ID" ]]; then
|
echo "Tag: ${TAG:-<none>}"
|
||||||
[[ "$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
|
|
||||||
echo "Looking for build run for tag: $TAG"
|
|
||||||
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" ]]; then
|
# Determine build run ID
|
||||||
BUILD_RUN_ID=$(gh api --method GET \
|
BUILD_RUN_ID=""
|
||||||
"repos/${REPOSITORY}/actions/workflows/build.yml/runs" \
|
|
||||||
-f event=push -f status=success -F per_page=100 2>/dev/null |
|
if [[ -n "$INPUT_RUN_ID" ]]; then
|
||||||
jq -r --arg tag "$TAG" \
|
# Explicit run ID takes priority
|
||||||
'[.workflow_runs[] | select(.head_branch == $tag)][0].id // empty' || true)
|
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"
|
||||||
|
|
||||||
|
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 "")
|
||||||
|
|
||||||
|
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
|
||||||
fi
|
fi
|
||||||
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] || fail "no successful build run found for tag"
|
|
||||||
echo "Found build run: $BUILD_RUN_ID"
|
echo "Found build run: $BUILD_RUN_ID"
|
||||||
|
|
||||||
else
|
else
|
||||||
|
# No tag — latest successful main build
|
||||||
echo "No tag specified, looking for latest main build"
|
echo "No tag specified, looking for latest main build"
|
||||||
BUILD_RUN_ID=$(gh api --method GET \
|
BUILD_RUN_ID=$(gh api \
|
||||||
"repos/${REPOSITORY}/actions/workflows/build.yml/runs" \
|
"repos/${{ github.repository }}/actions/workflows/build.yml/runs?branch=main&status=success&per_page=1" \
|
||||||
-f branch=main -f status=success -F per_page=1 \
|
--jq '.workflow_runs[0].id' 2>/dev/null || echo "")
|
||||||
--jq '.workflow_runs[0].id // empty' 2>/dev/null || true)
|
|
||||||
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] || fail "no successful main build found"
|
if [[ -z "$BUILD_RUN_ID" || "$BUILD_RUN_ID" == "null" ]]; then
|
||||||
|
echo "❌ No successful main build found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Latest main build: $BUILD_RUN_ID"
|
echo "Latest main build: $BUILD_RUN_ID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fetch once and use the same immutable run metadata for identity,
|
# Determine version and build type
|
||||||
# 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
|
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"
|
VERSION="$TAG"
|
||||||
DEV_SEQUENCE=""
|
if [[ "$TAG" == *"-preview"* ]]; then
|
||||||
|
BUILD_TYPE="preview"
|
||||||
|
elif [[ "$TAG" == *"alpha"* || "$TAG" == *"beta"* || "$TAG" == *"rc"* ]]; then
|
||||||
|
BUILD_TYPE="prerelease"
|
||||||
|
else
|
||||||
|
BUILD_TYPE="release"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
VERSION="dev-${HEAD_SHA}"
|
SHORT_SHA=$(gh api "repos/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}" \
|
||||||
DEV_SEQUENCE="$RUN_NUMBER"
|
--jq '.head_sha' 2>/dev/null | head -c 7)
|
||||||
|
VERSION="dev-${SHORT_SHA}"
|
||||||
|
BUILD_TYPE="development"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "version=$VERSION"
|
echo "version=$VERSION"
|
||||||
echo "build_type=$BUILD_TYPE"
|
echo "build_type=$BUILD_TYPE"
|
||||||
echo "build_run_id=$BUILD_RUN_ID"
|
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}"
|
echo "tag=${TAG}"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
@@ -222,7 +180,6 @@ jobs:
|
|||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
echo " Build type: $BUILD_TYPE"
|
echo " Build type: $BUILD_TYPE"
|
||||||
echo " Build run ID: $BUILD_RUN_ID"
|
echo " Build run ID: $BUILD_RUN_ID"
|
||||||
echo " Build run number: $RUN_NUMBER"
|
|
||||||
|
|
||||||
# Build DEB and RPM packages for each architecture
|
# Build DEB and RPM packages for each architecture
|
||||||
package:
|
package:
|
||||||
@@ -249,22 +206,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
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
|
- name: Download binary artifact from build run
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
@@ -283,7 +224,7 @@ jobs:
|
|||||||
ZIP_FILE=$(find ./binary-artifact -name "*.zip" -type f | head -1)
|
ZIP_FILE=$(find ./binary-artifact -name "*.zip" -type f | head -1)
|
||||||
if [[ -z "$ZIP_FILE" ]]; then
|
if [[ -z "$ZIP_FILE" ]]; then
|
||||||
echo "❌ No binary artifact found"
|
echo "❌ No binary artifact found"
|
||||||
find ./binary-artifact -mindepth 1 -maxdepth 1 -print 2>/dev/null || true
|
ls -la ./binary-artifact/ || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -298,22 +239,24 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
chmod +x ./bin/rustfs
|
chmod +x ./bin/rustfs
|
||||||
stat --printf='%n %s bytes\n' ./bin/rustfs
|
ls -lh ./bin/rustfs
|
||||||
echo "✅ Binary extracted"
|
echo "✅ Binary extracted"
|
||||||
|
|
||||||
- name: Build DEB package
|
- name: Build DEB package
|
||||||
id: deb
|
id: deb
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
DEB_VERSION: ${{ steps.versions.outputs.deb_version }}
|
|
||||||
DEB_ARCH: ${{ matrix.deb_arch }}
|
|
||||||
DEB_FILE: ${{ steps.versions.outputs.deb_file }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PKG_DIR="${DEB_FILE%.deb}"
|
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}"
|
||||||
|
|
||||||
echo "Building DEB: ${DEB_FILE}"
|
echo "Building DEB: ${PKG_DIR}.deb"
|
||||||
|
|
||||||
mkdir -p "${PKG_DIR}/DEBIAN"
|
mkdir -p "${PKG_DIR}/DEBIAN"
|
||||||
mkdir -p "${PKG_DIR}/usr/bin"
|
mkdir -p "${PKG_DIR}/usr/bin"
|
||||||
@@ -390,32 +333,26 @@ jobs:
|
|||||||
cp LICENSE "${PKG_DIR}/usr/share/doc/rustfs/"
|
cp LICENSE "${PKG_DIR}/usr/share/doc/rustfs/"
|
||||||
cp README.md "${PKG_DIR}/usr/share/doc/rustfs/"
|
cp README.md "${PKG_DIR}/usr/share/doc/rustfs/"
|
||||||
|
|
||||||
fakeroot dpkg-deb --build "${PKG_DIR}" "$DEB_FILE"
|
fakeroot dpkg-deb --build "${PKG_DIR}"
|
||||||
|
|
||||||
[[ $(dpkg-deb -f "$DEB_FILE" Package) == rustfs ]]
|
DEB_FILE="${PKG_DIR}.deb"
|
||||||
[[ $(dpkg-deb -f "$DEB_FILE" Version) == "$DEB_VERSION" ]]
|
ls -lh "$DEB_FILE"
|
||||||
[[ $(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_file=$DEB_FILE" >> "$GITHUB_OUTPUT"
|
||||||
echo "✅ DEB built: $DEB_FILE"
|
echo "✅ DEB built: $DEB_FILE"
|
||||||
|
|
||||||
- name: Build RPM package
|
- name: Build RPM package
|
||||||
id: rpm
|
id: rpm
|
||||||
shell: bash
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.resolve.outputs.version }}"
|
||||||
|
RPM_ARCH="${{ matrix.rpm_arch }}"
|
||||||
|
|
||||||
echo "Building RPM for ${RPM_ARCH}"
|
echo "Building RPM for ${RPM_ARCH}"
|
||||||
|
|
||||||
sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential rpm
|
sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential
|
||||||
sudo gem install fpm
|
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,
|
# Create config file for fpm (DEB build creates it in its package dir structure,
|
||||||
# but fpm needs the file to exist before packaging)
|
# but fpm needs the file to exist before packaging)
|
||||||
@@ -430,10 +367,8 @@ jobs:
|
|||||||
|
|
||||||
fpm -s dir -t rpm \
|
fpm -s dir -t rpm \
|
||||||
--name rustfs \
|
--name rustfs \
|
||||||
--version "$RPM_VERSION" \
|
--version "$VERSION" \
|
||||||
--iteration "$RPM_RELEASE" \
|
|
||||||
--architecture "$RPM_ARCH" \
|
--architecture "$RPM_ARCH" \
|
||||||
--package "$RPM_FILE" \
|
|
||||||
--depends "glibc >= 2.31" \
|
--depends "glibc >= 2.31" \
|
||||||
--maintainer "RustFS Team <support@rustfs.com>" \
|
--maintainer "RustFS Team <support@rustfs.com>" \
|
||||||
--description "High-performance distributed object storage" \
|
--description "High-performance distributed object storage" \
|
||||||
@@ -475,16 +410,13 @@ jobs:
|
|||||||
LICENSE=/usr/share/doc/rustfs/LICENSE \
|
LICENSE=/usr/share/doc/rustfs/LICENSE \
|
||||||
README.md=/usr/share/doc/rustfs/README.md
|
README.md=/usr/share/doc/rustfs/README.md
|
||||||
|
|
||||||
if [[ ! -f "$RPM_FILE" ]]; then
|
RPM_FILE=$(ls -1 rustfs-*.rpm 2>/dev/null | head -1)
|
||||||
|
if [[ -z "$RPM_FILE" ]]; then
|
||||||
echo "❌ RPM build failed"
|
echo "❌ RPM build failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RPM_METADATA=$(rpm -qp --qf '%{NAME}\n%{VERSION}\n%{RELEASE}\n%{ARCH}\n' "$RPM_FILE")
|
ls -lh "$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_file=$RPM_FILE" >> "$GITHUB_OUTPUT"
|
||||||
echo "✅ RPM built: $RPM_FILE"
|
echo "✅ RPM built: $RPM_FILE"
|
||||||
|
|
||||||
@@ -505,9 +437,6 @@ jobs:
|
|||||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
AWS_EC2_METADATA_DISABLED: true
|
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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -525,6 +454,7 @@ jobs:
|
|||||||
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
|
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
|
||||||
export AWS_DEFAULT_REGION="auto"
|
export AWS_DEFAULT_REGION="auto"
|
||||||
|
|
||||||
|
BUILD_TYPE="${{ needs.resolve.outputs.build_type }}"
|
||||||
if [[ "$BUILD_TYPE" == "development" ]]; then
|
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||||
R2_PREFIX="artifacts/rustfs/packages/dev"
|
R2_PREFIX="artifacts/rustfs/packages/dev"
|
||||||
else
|
else
|
||||||
@@ -534,6 +464,9 @@ jobs:
|
|||||||
|
|
||||||
echo "📤 Uploading to $R2_PATH"
|
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
|
for f in "$DEB_FILE" "$RPM_FILE"; do
|
||||||
if [[ -n "$f" && -f "$f" ]]; then
|
if [[ -n "$f" && -f "$f" ]]; then
|
||||||
echo "Uploading: $f"
|
echo "Uploading: $f"
|
||||||
@@ -559,13 +492,14 @@ jobs:
|
|||||||
if: needs.resolve.outputs.tag != ''
|
if: needs.resolve.outputs.tag != ''
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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
|
# Upload the packages, then refresh the release checksums so the new
|
||||||
# assets are covered, matching the binary release flow.
|
# assets are covered, matching the binary release flow.
|
||||||
for f in "$DEB_FILE" "$RPM_FILE"; do
|
for f in "$DEB_FILE" "$RPM_FILE"; do
|
||||||
@@ -617,19 +551,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Print summary
|
- name: Print summary
|
||||||
shell: bash
|
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: |
|
run: |
|
||||||
{
|
echo "## 📦 Package Summary" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "## 📦 Package Summary"
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo ""
|
echo "| Item | Value |" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "| Item | Value |"
|
echo "|------|-------|" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "|------|-------|"
|
echo "| Version | \`${{ needs.resolve.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "| Version | \`${SUMMARY_VERSION}\` |"
|
echo "| Build Type | ${{ needs.resolve.outputs.build_type }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "| Build Type | ${SUMMARY_BUILD_TYPE} |"
|
echo "| Build Run | #${{ needs.resolve.outputs.build_run_id }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "| Build Run | #${SUMMARY_BUILD_RUN_ID} |"
|
echo "| Package Status | ${{ needs.package.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "| Package Status | ${SUMMARY_PACKAGE_STATUS} |"
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# Functional chain driver: runs the ten functional suites in a fixed order
|
|
||||||
# (upgrade -> s3 -> kms -> tier -> storage -> heal -> pool -> security ->
|
|
||||||
# replication -> performance). Each suite attempts the next handoff even
|
|
||||||
# when its tests fail.
|
|
||||||
#
|
|
||||||
# Each suite workflow can still be dispatched standalone (workflow_dispatch);
|
|
||||||
# only chain-triggered runs forward to the next suite via repository_dispatch,
|
|
||||||
# so a standalone run never drags the rest of the chain behind it.
|
|
||||||
#
|
|
||||||
# Why not workflow_run chaining: GitHub does not guarantee delivery of
|
|
||||||
# workflow_run events (they are fire-and-forget), and the head-SHA filter made
|
|
||||||
# newly added suites (storage) unable to trigger at all. Explicit
|
|
||||||
# repository_dispatch handoffs are verifiable and re-drivable.
|
|
||||||
|
|
||||||
name: RustFS Functional Chain
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_run:
|
|
||||||
# Entry point: start the chain after the nightly build completes. The
|
|
||||||
# build's own conclusion does not gate the chain; each suite reports its
|
|
||||||
# own result to rustfs/backlog and the dashboard.
|
|
||||||
workflows: ["Nightly GNU Build"]
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
start-chain:
|
|
||||||
name: Start functional chain (upgrade first)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'schedule') }}
|
|
||||||
steps:
|
|
||||||
- name: Dispatch first suite (upgrade)
|
|
||||||
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 start the functional chain" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
gh api --method POST repos/rustfs/rustfs/dispatches \
|
|
||||||
-f event_type='rustfs-chain-upgrade' \
|
|
||||||
-F 'client_payload[from_suite]=nightly-build'
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
name: RustFS Heal Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package_url:
|
|
||||||
description: 'Direct .deb URL (nightly/R2). Defaults to the latest nightly deb.'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
stop_node_gb:
|
|
||||||
description: 'Stop the outage node when surviving nodes reach N GiB'
|
|
||||||
required: false
|
|
||||||
default: '15'
|
|
||||||
warp_stop_gb:
|
|
||||||
description: 'Stop warp when surviving nodes reach N GiB'
|
|
||||||
required: false
|
|
||||||
default: '40'
|
|
||||||
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 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
|
|
||||||
|
|
||||||
# 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-shared-functional-tests
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
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_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
|
|
||||||
# Standalone manual run, or one link of the nightly functional chain
|
|
||||||
# (storage -> heal -> pool). Pool expansion no longer re-runs heal.
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
|
||||||
steps:
|
|
||||||
- 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Show environment
|
|
||||||
run: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
warp --version || true
|
|
||||||
df -h /data | tail -1
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
if: ${{ inputs.cleanup_before != '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: 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
|
|
||||||
./auto-testing/rustfs_heal_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
|
|
||||||
|
|
||||||
- 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
|
|
||||||
./auto-testing/rustfs_heal_test.sh "${ARGS[@]}" --log-file "${LOG_FILE}"
|
|
||||||
|
|
||||||
- name: Run heal test (write -> outage -> heal -> verify)
|
|
||||||
id: test
|
|
||||||
run: |
|
|
||||||
./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 || '15' }}" \
|
|
||||||
--warp-stop-gb "${{ inputs.warp_stop_gb || '40' }}" \
|
|
||||||
--log-file "${LOG_FILE}"
|
|
||||||
|
|
||||||
- name: Generate report
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${STEPS_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload test logs
|
|
||||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: rustfs-heal-test-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
path: |
|
|
||||||
${{ 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: Cleanup environment (after)
|
|
||||||
if: ${{ always() && 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: 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()
|
|
||||||
run: |
|
|
||||||
echo "RustFS heal test failed"
|
|
||||||
echo "Package source: ${{ inputs.package_url || 'nightly (R2 latest)' }}"
|
|
||||||
echo "See the uploaded log artifact for details."
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
name: RustFS KMS Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 420
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Show environment
|
|
||||||
run: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
docker --version || true
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${CASE_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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."
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
name: RustFS Performance Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
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
|
|
||||||
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:
|
|
||||||
performance-test:
|
|
||||||
runs-on: pf-testing
|
|
||||||
timeout-minutes: 900
|
|
||||||
# Run on manual dispatch, or when the nightly build completed successfully.
|
|
||||||
# Skipped when nightly failed.
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
|
||||||
steps:
|
|
||||||
- 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- 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: |
|
|
||||||
# Empty on automatic (workflow_run) runs -> full 30 rounds.
|
|
||||||
# Manual dispatch can restrict method(s)/size(s).
|
|
||||||
export WARP_METHODS="${{ inputs.test_method }}"
|
|
||||||
export WARP_SIZES="${{ inputs.object_size }}"
|
|
||||||
./auto-testing/rustfs_performance_test.sh \
|
|
||||||
--step 5 -y \
|
|
||||||
--warp-duration "${{ inputs.warp_duration || '5m' }}" \
|
|
||||||
--warp-concurrency "${{ inputs.warp_concurrency || '64' }}" \
|
|
||||||
--log-file "${LOG_FILE}"
|
|
||||||
|
|
||||||
- name: Analyze results
|
|
||||||
if: ${{ steps.benchmark.conclusion == 'success' }}
|
|
||||||
run: |
|
|
||||||
./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)
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.benchmark.outcome == 'failure' || steps.benchmark.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: 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."
|
|
||||||
@@ -1,709 +0,0 @@
|
|||||||
name: RustFS Pool Expansion Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (leave empty to use the latest nightly deb)'
|
|
||||||
required: false
|
|
||||||
package_url:
|
|
||||||
description: 'Direct .deb URL (nightly/R2/dev). Overrides rustfs_version.'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
pools:
|
|
||||||
description: 'Number of pools to expand to (2 = first rebalance only)'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
default: '3'
|
|
||||||
storage_threshold:
|
|
||||||
description: 'Stop writing when storage usage reaches N%'
|
|
||||||
required: false
|
|
||||||
default: '50'
|
|
||||||
warp_duration:
|
|
||||||
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
|
|
||||||
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 heal suite finishes.
|
|
||||||
types: [rustfs-chain-pool]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
# 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-shared-functional-tests
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
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_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
|
|
||||||
if: ${{ 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' }}
|
|
||||||
steps:
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Initialize pool test artifacts
|
|
||||||
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: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
warp --version || true
|
|
||||||
df -h /data | tail -1
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
if: ${{ inputs.cleanup_before != '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: Install RustFS package & start cluster
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
ARGS+=(--version "${{ inputs.rustfs_version }}")
|
|
||||||
else
|
|
||||||
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
|
|
||||||
fi
|
|
||||||
./auto-testing/rustfs_pool_expand.sh "${ARGS[@]}"
|
|
||||||
|
|
||||||
- name: Preflight checks
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
ARGS+=(--version "${{ inputs.rustfs_version }}")
|
|
||||||
else
|
|
||||||
ARGS+=(--package-url "${{ env.RUSTFS_NIGHTLY_PACKAGE_URL }}")
|
|
||||||
fi
|
|
||||||
./auto-testing/rustfs_pool_expand.sh "${ARGS[@]}"
|
|
||||||
|
|
||||||
- name: Reset dedicated pool proxy
|
|
||||||
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
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
set -o pipefail
|
|
||||||
STEPS="4,5,6"
|
|
||||||
if [ "${{ inputs.pools || '3' }}" = "3" ]; then
|
|
||||||
STEPS="$STEPS,7,8"
|
|
||||||
if [ "${{ inputs.run_decommission != 'false' }}" = "true" ]; then
|
|
||||||
STEPS="$STEPS,9"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
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' }}" \
|
|
||||||
--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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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"
|
|
||||||
require_nonempty "${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
|
|
||||||
[ "${failed}" -eq 0 ] || exit 1
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.pool_test.outcome == 'failure' || steps.pool_test.outcome == 'cancelled') }}
|
|
||||||
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 }}
|
|
||||||
REPORT_FILE: '${{ env.POOL_ARTIFACT_DIR }}/pool-report.md'
|
|
||||||
LOG_FILE: '${{ env.POOL_ARTIFACT_DIR }}/pool-test.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})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ -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: Upload test logs
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
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: Restore dedicated pool proxy
|
|
||||||
if: always()
|
|
||||||
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: Cleanup environment (after)
|
|
||||||
if: ${{ always() && 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()
|
|
||||||
run: |
|
|
||||||
echo "RustFS pool expansion test failed"
|
|
||||||
echo "Package source: ${{ inputs.package_url || inputs.rustfs_version || 'nightly (R2 latest)' }}"
|
|
||||||
echo "See the uploaded log artifact for details."
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
# 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_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 360
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Show environment
|
|
||||||
run: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
aws --version
|
|
||||||
df -h /data | tail -1 || true
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${CASE_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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: Performance)"
|
|
||||||
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-performance' \
|
|
||||||
-F 'client_payload[from_suite]=replication'; 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 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 **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 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."
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
name: RustFS S3 Compatibility Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 360
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Show environment
|
|
||||||
run: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
df -h /data | tail -1
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${CASE_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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."
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
# 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_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 360
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
|
||||||
env:
|
|
||||||
TEST_OUTCOME: ${{ steps.test.outcome }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
RESULT=failure
|
|
||||||
if [ "${TEST_OUTCOME}" = "success" ] && [ -s "${SECURITY_ARTIFACTS_DIR}/suite-report.md" ]; then
|
|
||||||
RESULT=success
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${SECURITY_ARTIFACTS_DIR}/suite-report.md"
|
|
||||||
elif [ -s "${SECURITY_ARTIFACTS_DIR}/suite-report.md" ]; then
|
|
||||||
echo "The suite did not complete successfully. See suite-report.md in this run's artifact for diagnostics."
|
|
||||||
else
|
|
||||||
echo "The suite did not produce a non-empty report."
|
|
||||||
fi
|
|
||||||
} > "${SECURITY_ARTIFACTS_DIR}/report.md"
|
|
||||||
cat "${SECURITY_ARTIFACTS_DIR}/report.md" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
SUITE: 'security'
|
|
||||||
SUITE_LABEL: 'Security'
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
||||||
REPORT_FILE: ${{ env.SECURITY_ARTIFACTS_DIR }}/report.md
|
|
||||||
LOG_FILE: ''
|
|
||||||
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})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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."
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
name: RustFS Storage Engine Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 360
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Show environment
|
|
||||||
run: |
|
|
||||||
uname -a
|
|
||||||
jq --version
|
|
||||||
openssl version
|
|
||||||
df -h /data | tail -1
|
|
||||||
|
|
||||||
- name: Cleanup environment (before)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${CASE_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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."
|
|
||||||
@@ -1,621 +0,0 @@
|
|||||||
name: RustFS Tier Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
rustfs_version:
|
|
||||||
description: 'RustFS release tag to test (e.g. 1.0.0-rc.4-preview.1)'
|
|
||||||
required: false
|
|
||||||
default: '1.0.0-rc.4-preview.1'
|
|
||||||
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
|
|
||||||
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: 420
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
|
||||||
steps:
|
|
||||||
- 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Prepare pinned RustFS CLI
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 "## 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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled' || steps.evidence_verify.outcome == 'failure' || steps.evidence_verify.outcome == 'cancelled' || steps.gate.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})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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: "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."
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
# 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_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
|
|
||||||
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: 420
|
|
||||||
if: ${{ 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}"
|
|
||||||
|
|
||||||
# auto-testing is private: clone it with the dedicated PF token (not
|
|
||||||
# GITHUB_TOKEN) and retry transient GitHub/network failures.
|
|
||||||
- name: Checkout auto-testing scripts (with retry)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf auto-testing
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if gh repo clone rustfs/auto-testing auto-testing -- --depth 1 --quiet; then
|
|
||||||
echo "auto-testing cloned (attempt ${attempt})"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf auto-testing
|
|
||||||
echo "clone attempt ${attempt} failed; retrying in $((attempt * 15))s" >&2
|
|
||||||
sleep $((attempt * 15))
|
|
||||||
done
|
|
||||||
echo "ERROR: unable to clone rustfs/auto-testing after 5 attempts" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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 [ "${RESULT}" = "success" ]; then
|
|
||||||
cat "${MATRIX_TABLE}"
|
|
||||||
echo ""
|
|
||||||
cat "${CASE_TABLE}"
|
|
||||||
echo ""
|
|
||||||
echo "## Log tail"
|
|
||||||
echo '```text'
|
|
||||||
tail -n 200 "${LOG_FILE}"
|
|
||||||
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}"
|
|
||||||
[ "${RESULT}" = "success" ]
|
|
||||||
|
|
||||||
- name: Upload functional report to dashboard
|
|
||||||
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: File failure issue in rustfs/backlog
|
|
||||||
if: ${{ always() && (failure() || steps.test.outcome == 'failure' || steps.test.outcome == 'cancelled') }}
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
|
||||||
EVIDENCE_OUTCOME: ${{ steps.evidence.outcome }}
|
|
||||||
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"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
TITLE="[functional][${SUITE}] ${SUITE_LABEL} suite failed (run ${GITHUB_RUN_ID})"
|
|
||||||
EXISTING="$(gh issue list -R rustfs/backlog --state all \
|
|
||||||
--search "in:title \"run ${GITHUB_RUN_ID}\"" \
|
|
||||||
--json number --jq '.[].number' || true)"
|
|
||||||
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 "- Attempt: ${GITHUB_RUN_ATTEMPT}"
|
|
||||||
echo "- Workflow Commit: ${GITHUB_SHA}"
|
|
||||||
echo "- Trigger: ${GITHUB_EVENT_NAME}"
|
|
||||||
echo "- Date: $(date -u +%Y-%m-%d)"
|
|
||||||
echo ""
|
|
||||||
echo "## Report (errors and symptoms)"
|
|
||||||
echo ""
|
|
||||||
if [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -s "${REPORT_FILE}" ]; then
|
|
||||||
redact < "${REPORT_FILE}"
|
|
||||||
elif [ "${EVIDENCE_OUTCOME}" = "success" ] && [ -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: Upload report and logs
|
|
||||||
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)
|
|
||||||
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."
|
|
||||||
@@ -42,7 +42,6 @@ jobs:
|
|||||||
- name: Check latest scheduled runs
|
- name: Check latest scheduled runs
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RUSTFS_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
python3 scripts/check_scheduled_validation_freshness.py \
|
python3 scripts/check_scheduled_validation_freshness.py \
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ on:
|
|||||||
- "Continuous Integration"
|
- "Continuous Integration"
|
||||||
- "coverage"
|
- "coverage"
|
||||||
- "e2e-nightly"
|
- "e2e-nightly"
|
||||||
- "e2e-distributed"
|
|
||||||
- "e2e-s3tests"
|
- "e2e-s3tests"
|
||||||
- "Fuzz"
|
- "Fuzz"
|
||||||
- "mint"
|
- "mint"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: overtrue/repo-visuals-action@ee2c632f6ce617e851fb46ea935ee8af762ebb93 # v1.4.0
|
- uses: overtrue/repo-visuals-action@72f34d24769ff5d341956da2f23952594ef2f1e2 # v1.3.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
output-branch: star-history
|
output-branch: star-history
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
# 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: Targets Integration
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- ".github/actions/setup/**"
|
|
||||||
- ".github/workflows/targets-integration.yml"
|
|
||||||
- "crates/targets/**"
|
|
||||||
- "Cargo.lock"
|
|
||||||
schedule:
|
|
||||||
- cron: "17 2 * * *"
|
|
||||||
timezone: "Asia/Shanghai"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: targets-integration-${{ github.ref }}-${{ github.event_name }}
|
|
||||||
cancel-in-progress: ${{ github.event_name != 'schedule' }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUST_BACKTRACE: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
targets-live:
|
|
||||||
name: PostgreSQL, MySQL, AMQP, and NATS
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 90
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
||||||
NO_PROXY: 127.0.0.1,localhost
|
|
||||||
RUSTFS_TEST_PG_DSN: postgres://postgres:rustfs@127.0.0.1:5432/rustfs_events
|
|
||||||
RUSTFS_TEST_MYSQL_DSN: root:testpass@tcp(127.0.0.1:3306)/testdb
|
|
||||||
RUSTFS_TEST_AMQP_URL: amqp://rustfs:rustfs@127.0.0.1:5672/%2f
|
|
||||||
RUSTFS_TEST_NATS_URL: nats://127.0.0.1:4222
|
|
||||||
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: targets-live-lane
|
|
||||||
cache-save-if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
|
|
||||||
install-build-packaging-tools: 'false'
|
|
||||||
install-test-tools: 'false'
|
|
||||||
|
|
||||||
- name: Start target services
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
mkdir -p artifacts/targets-live/services
|
|
||||||
docker run -d --name rustfs-targets-postgres \
|
|
||||||
-e POSTGRES_PASSWORD=rustfs \
|
|
||||||
-e POSTGRES_DB=rustfs_events \
|
|
||||||
-p 5432:5432 postgres:16
|
|
||||||
docker run -d --name rustfs-targets-mysql \
|
|
||||||
-e MYSQL_ROOT_PASSWORD=testpass \
|
|
||||||
-e MYSQL_DATABASE=testdb \
|
|
||||||
-p 3306:3306 mysql:8.0.36
|
|
||||||
docker run -d --name rustfs-targets-rabbitmq \
|
|
||||||
-e RABBITMQ_DEFAULT_USER=rustfs \
|
|
||||||
-e RABBITMQ_DEFAULT_PASS=rustfs \
|
|
||||||
-p 5672:5672 rabbitmq:3
|
|
||||||
docker run -d --name rustfs-targets-nats \
|
|
||||||
-p 4222:4222 -p 8222:8222 nats:2 -js -m 8222
|
|
||||||
|
|
||||||
for _ in $(seq 1 120); do
|
|
||||||
docker exec rustfs-targets-postgres pg_isready -U postgres -d rustfs_events >/dev/null 2>&1 && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
docker exec rustfs-targets-postgres pg_isready -U postgres -d rustfs_events
|
|
||||||
|
|
||||||
for _ in $(seq 1 120); do
|
|
||||||
docker exec rustfs-targets-mysql mysqladmin ping -h 127.0.0.1 -uroot -ptestpass --silent >/dev/null 2>&1 && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
docker exec rustfs-targets-mysql mysqladmin ping -h 127.0.0.1 -uroot -ptestpass --silent
|
|
||||||
|
|
||||||
for _ in $(seq 1 120); do
|
|
||||||
docker exec rustfs-targets-rabbitmq rabbitmq-diagnostics -q ping >/dev/null 2>&1 && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
docker exec rustfs-targets-rabbitmq rabbitmq-diagnostics -q ping
|
|
||||||
|
|
||||||
for _ in $(seq 1 120); do
|
|
||||||
curl -fsS http://127.0.0.1:8222/healthz >/dev/null 2>&1 && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
curl -fsS http://127.0.0.1:8222/healthz
|
|
||||||
|
|
||||||
- name: Run live target tests
|
|
||||||
env:
|
|
||||||
CARGO_BUILD_JOBS: "2"
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
timeout --verbose --signal=TERM --kill-after=30s 75m bash <<'TESTS' \
|
|
||||||
2>&1 | tee artifacts/targets-live/tests.log
|
|
||||||
result=0
|
|
||||||
|
|
||||||
echo "::group::PostgreSQL"
|
|
||||||
cargo test --locked -p rustfs-targets --test postgres_integration -- --ignored --test-threads=1 || result=1
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
echo "::group::MySQL"
|
|
||||||
cargo test --locked -p rustfs-targets --test mysql_integration -- --ignored --test-threads=1 || result=1
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
echo "::group::AMQP"
|
|
||||||
cargo test --locked -p rustfs-targets --test amqp_integration -- --ignored --test-threads=1 || result=1
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
echo "::group::NATS integration"
|
|
||||||
cargo test --locked -p rustfs-targets --test nats_jetstream_validation_integration -- --ignored --test-threads=1 || result=1
|
|
||||||
cargo test --locked -p rustfs-targets --test nats_jetstream_regression_guards -- --ignored --test-threads=1 || result=1
|
|
||||||
cargo test --locked -p rustfs-targets --lib target::nats::jetstream -- --ignored --test-threads=1 || result=1
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
exit "${result}"
|
|
||||||
TESTS
|
|
||||||
status=${PIPESTATUS[0]}
|
|
||||||
{
|
|
||||||
echo "exit_status=${status}"
|
|
||||||
echo "finished_at=$(date --utc --iso-8601=seconds)"
|
|
||||||
echo
|
|
||||||
echo "Remaining test-related processes:"
|
|
||||||
pgrep -af 'cargo|target/.*/deps/' || true
|
|
||||||
} > artifacts/targets-live/diagnostics.txt
|
|
||||||
exit "${status}"
|
|
||||||
|
|
||||||
- name: Collect service logs
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
mkdir -p artifacts/targets-live/services
|
|
||||||
for container in postgres mysql rabbitmq nats; do
|
|
||||||
docker logs --tail 500 "rustfs-targets-${container}" \
|
|
||||||
> "artifacts/targets-live/services/${container}.log" 2>&1 || true
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Stop target services
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
docker rm -f \
|
|
||||||
rustfs-targets-postgres \
|
|
||||||
rustfs-targets-mysql \
|
|
||||||
rustfs-targets-rabbitmq \
|
|
||||||
rustfs-targets-nats >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
- name: Upload target integration diagnostics
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
||||||
with:
|
|
||||||
name: targets-integration-${{ github.run_number }}-${{ github.run_attempt }}
|
|
||||||
path: artifacts/targets-live
|
|
||||||
|
|
||||||
alert-on-failure:
|
|
||||||
name: Alert on scheduled failure
|
|
||||||
needs: [targets-live]
|
|
||||||
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.GITHUB_TOKEN }}
|
|
||||||
+3
-5
@@ -33,7 +33,6 @@ profile.json
|
|||||||
*.zst
|
*.zst
|
||||||
.secrets
|
.secrets
|
||||||
*.go
|
*.go
|
||||||
!crates/zip/tests/fixtures/snowball/**/generate/*.go
|
|
||||||
*.pb
|
*.pb
|
||||||
*.svg
|
*.svg
|
||||||
deploy/logs/*.log.*
|
deploy/logs/*.log.*
|
||||||
@@ -56,10 +55,11 @@ docs/*
|
|||||||
!docs/architecture/**
|
!docs/architecture/**
|
||||||
!docs/operations/
|
!docs/operations/
|
||||||
!docs/operations/**
|
!docs/operations/**
|
||||||
!docs/postmortems/
|
|
||||||
!docs/postmortems/**
|
|
||||||
!docs/testing/
|
!docs/testing/
|
||||||
!docs/testing/**
|
!docs/testing/**
|
||||||
|
docs/heal-scanner-logging-governance.md
|
||||||
|
docs/benchmark/rustfs-target-bench/
|
||||||
|
docs/benchmark/*.md
|
||||||
.codegraph/*
|
.codegraph/*
|
||||||
.docker/test/compat/data/*
|
.docker/test/compat/data/*
|
||||||
.docker/test/compat/kms/*
|
.docker/test/compat/kms/*
|
||||||
@@ -83,8 +83,6 @@ worktrees/*
|
|||||||
|
|
||||||
# Local AI-agent review artifacts (omo evidence dumps)
|
# Local AI-agent review artifacts (omo evidence dumps)
|
||||||
.omo/
|
.omo/
|
||||||
# Legacy per-tool skill dir; skills live in .agents/skills (shared by all agents)
|
|
||||||
.mimocode/
|
|
||||||
|
|
||||||
# insta scratch files; the accepted .snap files ARE the assertions and are committed
|
# insta scratch files; the accepted .snap files ARE the assertions and are committed
|
||||||
*.snap.new
|
*.snap.new
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: issue-triage
|
||||||
|
description: Triage a GitHub issue — determine if it is already fixed, needs implementation, or should be closed. Searches related commits and PRs, verifies implementation status, and posts a triage comment or closes the issue. Use when the user provides an issue URL and asks whether it can be closed or needs work.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
### 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 --search "fixes #<N> OR closes #<N> OR #<N>" --state all --json number,title,state,mergedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
If the issue mentions specific PRs, check their status:
|
||||||
|
```bash
|
||||||
|
gh pr view <PR_N> --json state,mergedAt,title
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify implementation
|
||||||
|
|
||||||
|
For each linked or related PR that is merged, verify the fix is actually present on the current main branch:
|
||||||
|
```bash
|
||||||
|
git log --oneline main | grep -i "<keyword>"
|
||||||
|
# or
|
||||||
|
git log --oneline main --grep="<PR_N>"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the issue describes a specific defect, check the relevant code to confirm the fix is in place:
|
||||||
|
```bash
|
||||||
|
grep -n "<pattern>" 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**: Close with a summary comment listing what was fixed and which PRs.
|
||||||
|
- **Some items fixed, some remaining**: Comment with status of each item. Do not close.
|
||||||
|
- **Not yet implemented**: Comment with a summary of what remains. Do not close.
|
||||||
|
- **Superseded or no longer relevant**: Close with explanation.
|
||||||
|
|
||||||
|
### 5. Take action
|
||||||
|
|
||||||
|
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 issue labels if needed:
|
||||||
|
```bash
|
||||||
|
gh issue edit <N> --repo <owner/repo> --add-label "completed" --remove-label "needs-triage"
|
||||||
|
```
|
||||||
|
|
||||||
|
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 issues: `gh issue list --repo <repo> --author <user> --state open --json number,title,updatedAt`
|
||||||
|
2. For each issue, run steps 1-5 above.
|
||||||
|
3. Report a summary table of all triaged issues with verdicts.
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
### Issue Triage: #<N> — <title>
|
||||||
|
|
||||||
|
**State**: OPEN / CLOSED
|
||||||
|
**Linked PRs**: <list with merge status>
|
||||||
|
|
||||||
|
#### Assessment
|
||||||
|
<what was requested vs what is implemented>
|
||||||
|
|
||||||
|
#### Verdict
|
||||||
|
- Close — all items resolved by <PR list>
|
||||||
|
- Keep open — <remaining items>
|
||||||
|
- Not started — <what needs to be done>
|
||||||
|
|
||||||
|
#### Action taken
|
||||||
|
- Closed with comment / Commented / No action
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
name: pr-review
|
||||||
|
description: Review a GitHub PR end-to-end from a URL or number — fetch metadata, inspect the diff, run multi-role adversarial review, check CI status, and post the review comment. Use when the user provides a PR link and asks to review it.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PR Review
|
||||||
|
|
||||||
|
Use this skill when the user provides a GitHub PR URL or number and asks to review it. This covers the full review lifecycle: data gathering, code review, CI verification, and posting the result.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Read `AGENTS.md` for the repository's adversarial validation policy and change-style rules.
|
||||||
|
- The `adversarial-validation` skill handles the review role playbooks; this skill orchestrates the workflow around it.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Gather PR context
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr view <N> --json title,author,state,body,additions,deletions,changedFiles,commits,baseRefName,headRefName
|
||||||
|
gh pr diff <N> --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> --json title,body,state
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fetch the diff and classify the change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin pull/<N>/head:pr-<N>
|
||||||
|
git diff main...pr-<N> --stat
|
||||||
|
```
|
||||||
|
|
||||||
|
Classify the change by risk tier (per AGENTS.md):
|
||||||
|
- **Exempt**: docs/comments/instruction-only, formatting, typos.
|
||||||
|
- **Mechanical**: renames, file moves, test-only or tooling changes.
|
||||||
|
- **Standard** (default): any behavior change.
|
||||||
|
- **High risk**: locking, erasure coding, quorum/heal, replication, multipart, RPC, lifecycle/tiering, metadata formats, persistence/fsync, IAM/KMS/auth, on-disk/on-wire formats, S3 API-visible behavior.
|
||||||
|
|
||||||
|
### 3. Cluster changed files and delegate review
|
||||||
|
|
||||||
|
Group the changed files into logical clusters (by crate or functional area). For each cluster, spawn a subagent with a focused review prompt that includes:
|
||||||
|
- The cluster's changed files and their diffs.
|
||||||
|
- The applicable adversarial role probes (from the `adversarial-validation` skill).
|
||||||
|
- The repository's AGENTS.md rules relevant to that domain.
|
||||||
|
|
||||||
|
For standard-tier changes: correctness adversary + simplicity adversary + test-coverage skeptic, plus every role whose domain the diff touches.
|
||||||
|
For high-risk changes: run all seven roles.
|
||||||
|
|
||||||
|
Each subagent must produce findings (concrete failure scenario with file:line) or a null report ("attacked X, Y, Z — no break found").
|
||||||
|
|
||||||
|
### 4. Check CI status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr checks <N>
|
||||||
|
```
|
||||||
|
|
||||||
|
If any checks fail, investigate:
|
||||||
|
```bash
|
||||||
|
gh run view --log-failed --job=<JOB_ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
Determine whether failures are pre-existing (on main), flaky, or caused by the PR.
|
||||||
|
|
||||||
|
### 5. Synthesize findings
|
||||||
|
|
||||||
|
Combine all subagent findings into a structured review:
|
||||||
|
- **Summary**: one-paragraph overview of the change and overall assessment.
|
||||||
|
- **Findings**: each finding with severity (critical/major/minor/nit), file:line, concrete failure scenario, and suggested fix.
|
||||||
|
- **CI status**: pass/fail with notes on any failures.
|
||||||
|
- **Verdict**: APPROVE, REQUEST_CHANGES, or COMMENT.
|
||||||
|
|
||||||
|
### 6. Post the review
|
||||||
|
|
||||||
|
Write the review body to a temp file and post via CLI:
|
||||||
|
```bash
|
||||||
|
# Request changes
|
||||||
|
gh pr review <N> --request-changes --body-file /tmp/pr_review.md
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
gh pr review <N> --approve --body-file /tmp/pr_review.md
|
||||||
|
|
||||||
|
# Comment only (no verdict)
|
||||||
|
gh pr review <N> --comment --body-file /tmp/pr_review.md
|
||||||
|
```
|
||||||
|
|
||||||
|
For inline comments on specific lines, use the GitHub API:
|
||||||
|
```bash
|
||||||
|
cat > /tmp/pr_review.json <<'EOF'
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
```
|
||||||
|
|
||||||
|
Always use `--body-file` or `--input`, never inline multiline `--body`.
|
||||||
|
|
||||||
|
### 7. Handle follow-up
|
||||||
|
|
||||||
|
If the review requests changes:
|
||||||
|
- Monitor for new commits: `gh pr view <N> --json commits`
|
||||||
|
- Re-review changed files only: `git diff pr-<N>..origin/pull/<N>/head`
|
||||||
|
- Update the review when findings are addressed.
|
||||||
|
|
||||||
|
If CI was failing due to pre-existing main breakage:
|
||||||
|
- Comment on the PR noting the failure is pre-existing.
|
||||||
|
- Suggest updating the branch: `gh pr update-branch <N>`
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
### PR Review: #<N> — <title>
|
||||||
|
|
||||||
|
**Author**: <author>
|
||||||
|
**Risk tier**: exempt | mechanical | standard | high-risk
|
||||||
|
**Changed files**: <count> across <cluster count> clusters
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
<one-paragraph overview>
|
||||||
|
|
||||||
|
#### Findings
|
||||||
|
| Severity | Location | Finding |
|
||||||
|
|----------|----------|---------|
|
||||||
|
| critical | file:line | concrete failure scenario |
|
||||||
|
|
||||||
|
#### CI Status
|
||||||
|
- All checks pass / Failing: <details>
|
||||||
|
|
||||||
|
#### Verdict
|
||||||
|
APPROVE / REQUEST_CHANGES / COMMENT
|
||||||
|
|
||||||
|
## 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 (>50 files), cluster aggressively and delegate in parallel to keep review time reasonable.
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: rustfs-fmt-check
|
- id: rustfs-dev-check
|
||||||
name: Rust formatting
|
name: rustfs dev-check
|
||||||
entry: cargo fmt --all --check
|
entry: make dev-check
|
||||||
language: system
|
language: system
|
||||||
types: [rust]
|
types: [rust]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ This file contains repository-wide rules. Use the nearest subdirectory
|
|||||||
|
|
||||||
1. System/developer instructions.
|
1. System/developer instructions.
|
||||||
2. The current user request.
|
2. The current user request.
|
||||||
3. Applicable `AGENTS.md` files, with the nearest file winning conflicts.
|
3. The nearest `AGENTS.md`.
|
||||||
4. Selected skills and reference documents.
|
4. This file.
|
||||||
|
|
||||||
Nested instructions add to ancestor rules; they do not discard non-conflicting
|
|
||||||
rules. A skill cannot expand the user's requested scope or grant authorization.
|
|
||||||
|
|
||||||
## Operating Model
|
## Operating Model
|
||||||
|
|
||||||
@@ -24,29 +21,58 @@ rules. A skill cannot expand the user's requested scope or grant authorization.
|
|||||||
- Do not load every skill or inspect unrelated modules preemptively. Select a
|
- Do not load every skill or inspect unrelated modules preemptively. Select a
|
||||||
skill only when its description directly matches the request or changed
|
skill only when its description directly matches the request or changed
|
||||||
surface.
|
surface.
|
||||||
- Resolve repository workflow skills under `.agents/skills/` when a global
|
|
||||||
skill has the same name, unless the user explicitly selects another path.
|
|
||||||
- Avoid repeated reads and equivalent verification commands once enough
|
- Avoid repeated reads and equivalent verification commands once enough
|
||||||
evidence exists.
|
evidence exists.
|
||||||
- Search for relevant symbols/headings before reading long files; return only
|
|
||||||
matching ranges. If output is truncated, narrow the query instead of repeating
|
|
||||||
a full read. Keep reusable raw logs in task artifacts and report the evidence.
|
|
||||||
- Reuse authorization already given in the conversation. Resolve routine choices
|
|
||||||
within that scope and continue independent work while a material question is
|
|
||||||
pending. Before requesting missing approval, prepare the concrete result that
|
|
||||||
is already authorized; retain explicit merge and release gates.
|
|
||||||
|
|
||||||
## Task-Specific Guidance
|
## Worktree and Disk Hygiene
|
||||||
|
|
||||||
Read only the reference needed for the current task, once per unchanged context:
|
- 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 an `overtrue/` feature branch unless
|
||||||
|
the user requests another 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.
|
||||||
|
|
||||||
- Before code changes or artifact-heavy work, read [implementation rules](.agents/references/implementation.md).
|
## Change Style
|
||||||
For a read-only code review, use its change-style and boundary sections as needed.
|
|
||||||
- Before commits, pushes, PR creation/updates, or posting to PRs/issues/discussions,
|
- Preserve existing control flow unless changing it is required for correctness.
|
||||||
read [Git and PR rules](.agents/references/pull-requests.md).
|
- Prefer a direct local edit over new files, wrappers, managers, or speculative
|
||||||
Reuse existing authorization; a reference does not authorize posting, merging, or publishing.
|
abstractions.
|
||||||
- Preserve unrelated work. Never commit from a shared checkout or delete another task's artifacts.
|
- Add a helper only when it removes current duplication, names a real domain
|
||||||
- Source comments, commits, PR titles, and PR bodies are in English.
|
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.
|
||||||
|
|
||||||
## Sources of Truth
|
## Sources of Truth
|
||||||
|
|
||||||
@@ -55,7 +81,6 @@ Read only the reference needed for the current task, once per unchanged context:
|
|||||||
- CI gates: `.github/workflows/ci.yml`.
|
- CI gates: `.github/workflows/ci.yml`.
|
||||||
- PR format: `.github/pull_request_template.md`.
|
- PR format: `.github/pull_request_template.md`.
|
||||||
- Architecture routing: `ARCHITECTURE.md` and `docs/architecture/README.md`.
|
- Architecture routing: `ARCHITECTURE.md` and `docs/architecture/README.md`.
|
||||||
- Knowledge-base index and documentation rules: `docs/architecture/README.md`.
|
|
||||||
- Agent skills: `.agents/skills/*/SKILL.md`.
|
- Agent skills: `.agents/skills/*/SKILL.md`.
|
||||||
|
|
||||||
Do not commit one-shot plans, trackers, migration ledgers, benchmark snapshots,
|
Do not commit one-shot plans, trackers, migration ledgers, benchmark snapshots,
|
||||||
@@ -93,13 +118,12 @@ runtime/build output:
|
|||||||
- Use `make pre-commit` only when its repository-wide fast checks add confidence
|
- Use `make pre-commit` only when its repository-wide fast checks add confidence
|
||||||
beyond the focused checks.
|
beyond the focused checks.
|
||||||
|
|
||||||
### Broad Cross-Module Changes
|
### Broad or High-Risk Changes
|
||||||
|
|
||||||
Do not run `make pre-pr` by default before opening a PR. Consider it only when
|
After the required adversarial review, run `make pre-pr` when targeted coverage
|
||||||
the final diff is broad, spans multiple modules, and targeted checks cannot
|
cannot bound the impact, including dependency/toolchain/build-matrix changes,
|
||||||
bound the impact. Decide dynamically from the affected boundaries and risks;
|
unbounded cross-crate APIs, or locking, durability, erasure coding, replication,
|
||||||
otherwise use the scoped formatting, linting, compilation, and test checks
|
RPC, IAM/KMS/auth, cryptography, on-disk/on-wire, and S3-visible behavior.
|
||||||
above.
|
|
||||||
|
|
||||||
`make pre-pr` includes `make pre-commit`; never run both for the same unchanged
|
`make pre-pr` includes `make pre-commit`; never run both for the same unchanged
|
||||||
diff. Do not repeat a check already covered by a successful umbrella gate.
|
diff. Do not repeat a check already covered by a successful umbrella gate.
|
||||||
@@ -116,25 +140,67 @@ requested adversarial/design reviews, and agent-instruction changes that alter
|
|||||||
execution. Ordinary questions, diagnoses, status reports, non-adversarial code
|
execution. Ordinary questions, diagnoses, status reports, non-adversarial code
|
||||||
reviews, and low-risk planning do not trigger it.
|
reviews, and low-risk planning do not trigger it.
|
||||||
|
|
||||||
For applicable work and substantial PR reviews, read the [risk tiers and review shape](.agents/references/adversarial-validation.md).
|
Risk and review shape:
|
||||||
Load only the matching domain probes; ordinary reviews do not become adversarial
|
|
||||||
merely because this reference exists.
|
|
||||||
|
|
||||||
A review has no finding quota; `No findings` is a complete outcome. A request to
|
- **Exempt:** documentation, comments, formatting, or typos with no runtime,
|
||||||
find problems is not evidence that a defect exists. Before reporting a candidate,
|
build, test, or agent-execution effect.
|
||||||
check callers, invariants, and existing tests for evidence that disproves it.
|
- **Mechanical:** renames, moves, test/tooling-only changes, and agent-rule
|
||||||
Findings need `file:line` and a concrete failure or violation of an explicit
|
changes. Run correctness and simplicity lenses.
|
||||||
requirement. Missing required tests/checks are verification gaps, not proof of a
|
- **Standard:** localized behavior changes. Run one integrated final-diff pass
|
||||||
runtime bug; name the unprotected behavior or unmet gate. Keep optional style or
|
covering correctness, simplicity, and test coverage; add only domain lenses
|
||||||
refactoring preferences out of defect findings unless that review was requested.
|
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.
|
||||||
|
|
||||||
Fix or rebut supported findings within the authorized scope. Once the required
|
Available domain lenses are security, concurrency/durability, compatibility,
|
||||||
passes are complete, stop. Reopen only for changed code, new evidence, an
|
and performance. Select `.agents/skills/adversarial-validation/SKILL.md` for an
|
||||||
unresolved finding, or an explicit re-review request; an unchanged diff does not
|
explicit adversarial request, a high-risk change, or a substantial PR review;
|
||||||
need another pass at every conversation turn or workflow handoff.
|
then read only its matching role references. A routine standard pass does not
|
||||||
|
load the playbook unless the reviewer needs a RustFS-specific probe.
|
||||||
|
|
||||||
|
A finding must name a concrete input/state/interleaving and wrong outcome, or a
|
||||||
|
specific missing regression check, with `file:line`. Resolve it by fixing the
|
||||||
|
diff or rebutting it with code-path/test/invariant evidence. After a non-trivial
|
||||||
|
fix, rerun only affected lenses.
|
||||||
|
|
||||||
For high-risk PRs, record one concise verdict per covered lens in the PR body.
|
For high-risk PRs, record one concise verdict per covered lens in the PR body.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## Security Baseline
|
## Security Baseline
|
||||||
|
|
||||||
- Never commit secrets, credentials, or key material.
|
- Never commit secrets, credentials, or key material.
|
||||||
@@ -171,6 +237,12 @@ Use `.agents/skills/rustfs-logging-governance/SKILL.md` for logging changes.
|
|||||||
- `DataUsageCacheInfo` and `DataUsageEntry` keep their hand-written map
|
- `DataUsageCacheInfo` and `DataUsageEntry` keep their hand-written map
|
||||||
serialization and new fields remain `#[serde(default)]` for older readers.
|
serialization and new fields remain `#[serde(default)]` for older readers.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
Use Rust API naming: `SCREAMING_SNAKE_CASE` constants/statics, `snake_case`
|
||||||
|
functions/variables, and `PascalCase` types. Do not rename unrelated existing
|
||||||
|
violations.
|
||||||
|
|
||||||
## Scoped Guidance
|
## Scoped Guidance
|
||||||
|
|
||||||
Before editing, locate the nearest instructions with:
|
Before editing, locate the nearest instructions with:
|
||||||
@@ -180,4 +252,4 @@ git ls-files '*AGENTS.md'
|
|||||||
```
|
```
|
||||||
|
|
||||||
The nearest file wins for domain invariants. Keep generic workflow and
|
The nearest file wins for domain invariants. Keep generic workflow and
|
||||||
validation policy in this root file and its task-specific references.
|
validation policy in this root file.
|
||||||
|
|||||||
+16
-29
@@ -62,7 +62,7 @@ rustfs/ # Workspace root (virtual manifest)
|
|||||||
│ ├── utils/ # Pure utility functions
|
│ ├── utils/ # Pure utility functions
|
||||||
│ ├── ... # (see "Crate Reference" below)
|
│ ├── ... # (see "Crate Reference" below)
|
||||||
│ └── e2e_test/ # End-to-end integration tests
|
│ └── e2e_test/ # End-to-end integration tests
|
||||||
└── docs/ # Agent knowledge base: contracts, runbooks, testing rules (index: docs/architecture/README.md)
|
└── docs/ # Design documents and analysis
|
||||||
```
|
```
|
||||||
|
|
||||||
### Main Crate Layers (`rustfs/src/`)
|
### Main Crate Layers (`rustfs/src/`)
|
||||||
@@ -73,7 +73,7 @@ The main crate is organized in layers, top to bottom:
|
|||||||
|-------|-----------|----------------|
|
|-------|-----------|----------------|
|
||||||
| **Server** | `server/` | HTTP listener, TLS, CORS, compression, middleware, graceful shutdown |
|
| **Server** | `server/` | HTTP listener, TLS, CORS, compression, middleware, graceful shutdown |
|
||||||
| **Admin** | `admin/` | Admin API routing, 30+ handler modules, web console |
|
| **Admin** | `admin/` | Admin API routing, 30+ handler modules, web console |
|
||||||
| **App** | `app/` | Use-case orchestration: object (per-operation modules under `app/object/`, re-exported as `object_usecase`), bucket_usecase, multipart_usecase |
|
| **App** | `app/` | Use-case orchestration: object_usecase, bucket_usecase, multipart_usecase |
|
||||||
| **Storage** | `storage/` | S3 API translation, erasure-coded FS, SSE encryption, RPC, concurrency |
|
| **Storage** | `storage/` | S3 API translation, erasure-coded FS, SSE encryption, RPC, concurrency |
|
||||||
| **Auth** | `auth.rs` | S3 signature verification, credential validation |
|
| **Auth** | `auth.rs` | S3 signature verification, credential validation |
|
||||||
| **Config** | `config/` | CLI parsing, config struct, workload profiles |
|
| **Config** | `config/` | CLI parsing, config struct, workload profiles |
|
||||||
@@ -92,8 +92,8 @@ refactors.
|
|||||||
|
|
||||||
| Domain | Current workspace crates | Responsibility |
|
| Domain | Current workspace crates | Responsibility |
|
||||||
|--------|--------------------------|----------------|
|
|--------|--------------------------|----------------|
|
||||||
| Foundation | `checksums`, `common`, `config`, `data-usage`, `heal-contracts`, `scanner-metrics`, `utils` | Shared configuration, data-usage models, heal domain contracts, scanner telemetry types, utilities, and checksums. |
|
| Foundation | `checksums`, `common`, `config`, `data-usage`, `utils` | Shared configuration, data-usage models, utilities, and checksums. |
|
||||||
| I/O and storage | `concurrency`, `ecstore`, `filemeta`, `heal`, `io-core`, `io-metrics`, `lifecycle`, `lock`, `object-capacity`, `object-data-cache`, `replication`, `rio`, `rio-v2`, `s3-client`, `scanner`, `storage-api` | Erasure-coded object storage, metadata, recovery, lifecycle, replication, locking, cache, I/O pipelines, and the engine-side S3 client for remote tier/transition targets. |
|
| I/O and storage | `concurrency`, `ecstore`, `filemeta`, `heal`, `io-core`, `io-metrics`, `lifecycle`, `lock`, `object-capacity`, `object-data-cache`, `replication`, `rio`, `rio-v2`, `scanner`, `storage-api` | Erasure-coded object storage, metadata, recovery, lifecycle, replication, locking, cache, and I/O pipelines. |
|
||||||
| Security and identity | `credentials`, `crypto`, `iam`, `keystone`, `kms`, `policy`, `security-governance`, `signer`, `tls-runtime`, `trusted-proxies` | Credentials, authentication, authorization, encryption, key management, TLS, and security contracts. |
|
| Security and identity | `credentials`, `crypto`, `iam`, `keystone`, `kms`, `policy`, `security-governance`, `signer`, `tls-runtime`, `trusted-proxies` | Credentials, authentication, authorization, encryption, key management, TLS, and security contracts. |
|
||||||
| Protocols and contracts | `extension-schema`, `madmin`, `protos`, `protocols`, `s3-ops`, `s3-types`, `s3select-api`, `s3select-query` | Admin, inter-node, S3, S3 Select, and optional protocol contracts. |
|
| Protocols and contracts | `extension-schema`, `madmin`, `protos`, `protocols`, `s3-ops`, `s3-types`, `s3select-api`, `s3select-query` | Admin, inter-node, S3, S3 Select, and optional protocol contracts. |
|
||||||
| Operations and integration | `audit`, `notify`, `obs`, `targets`, `zip` | Auditing, observability, event delivery, notification targets, and archive support. |
|
| Operations and integration | `audit`, `notify`, `obs`, `targets`, `zip` | Auditing, observability, event delivery, notification targets, and archive support. |
|
||||||
@@ -115,15 +115,8 @@ default build (lifecycle:
|
|||||||
1. **Layers flow downward.** Server → Admin/App → Storage → ecstore → rio/io-core.
|
1. **Layers flow downward.** Server → Admin/App → Storage → ecstore → rio/io-core.
|
||||||
No upward imports.
|
No upward imports.
|
||||||
|
|
||||||
2. **Leaf crates depend only on external crates, with adjudicated exceptions
|
2. **Leaf crates have zero internal dependencies.** `config`, `credentials`, `crypto`,
|
||||||
pinned by a guard.** `config`, `credentials`, and `crypto` take no internal
|
`io-metrics`, and `madmin` should depend only on external crates.
|
||||||
dependency. `io-metrics` takes exactly `rustfs-s3-ops` (transitively
|
|
||||||
`rustfs-s3-types`), a pure contract crate with no I/O and no global state —
|
|
||||||
adjudicated in rustfs/backlog#1834. `madmin` left the leaf set when #6166 made
|
|
||||||
it the SigV4-signed admin SDK client; its internal dependency surface is pinned
|
|
||||||
to exactly `rustfs-signer`. Both pins live in the leaf allowlist in
|
|
||||||
`scripts/check_architecture_migration_rules.sh`; any other internal dependency
|
|
||||||
fails the guard ([crate boundaries](docs/architecture/crate-boundaries.md)).
|
|
||||||
- ✅ RESOLVED: the historical `utils → config` and `common → filemeta`/`madmin`
|
- ✅ RESOLVED: the historical `utils → config` and `common → filemeta`/`madmin`
|
||||||
edges were removed; do not reintroduce them (see Known Structural Issues).
|
edges were removed; do not reintroduce them (see Known Structural Issues).
|
||||||
|
|
||||||
@@ -135,7 +128,7 @@ default build (lifecycle:
|
|||||||
`crates/ecstore/src/bucket/replication/replication_state.rs`) — a naming
|
`crates/ecstore/src/bucket/replication/replication_state.rs`) — a naming
|
||||||
collision, not copies; renaming is tracked in rustfs/backlog#1847.
|
collision, not copies; renaming is tracked in rustfs/backlog#1847.
|
||||||
- `LastMinuteLatency` has two deliberately different implementations: the
|
- `LastMinuteLatency` has two deliberately different implementations: the
|
||||||
per-second bucketed accumulator in `crates/scanner-metrics/src/last_minute.rs` and
|
per-second bucketed accumulator in `crates/common/src/last_minute.rs` and
|
||||||
the in-memory endpoint-health sample tracker in
|
the in-memory endpoint-health sample tracker in
|
||||||
`crates/ecstore/src/bucket/bucket_target_sys.rs` (its doc comment explains
|
`crates/ecstore/src/bucket/bucket_target_sys.rs` (its doc comment explains
|
||||||
why it stays local).
|
why it stays local).
|
||||||
@@ -145,19 +138,15 @@ default build (lifecycle:
|
|||||||
`BackpressureSettings` copy that lingered in io-metrics was removed
|
`BackpressureSettings` copy that lingered in io-metrics was removed
|
||||||
(rustfs/backlog#1833).
|
(rustfs/backlog#1833).
|
||||||
|
|
||||||
4. **ecstore does not *serve* HTTP or the S3 wire protocol.** It operates on
|
4. **ecstore does not know about HTTP or S3 protocol details.** It operates on
|
||||||
storage-level abstractions (objects, buckets, disks, pools) and holds no
|
storage-level abstractions (objects, buckets, disks, pools).
|
||||||
wire or DTO types of the serving surface. *Consuming* remote S3-compatible
|
- ⚠️ VIOLATED: 58 files under `crates/ecstore/src` reference `s3s`
|
||||||
endpoints (ILM tier warm backends, transition targets) is a legitimate
|
(`rg -l 's3s' crates/ecstore/src | wc -l`), `crates/ecstore/src/client/`
|
||||||
engine capability, but it lives in the dedicated `rustfs-s3-client` crate
|
is a ~9.4K-line embedded S3 HTTP client, and `crates/ecstore/Cargo.toml`
|
||||||
(`crates/s3-client`, extracted from the formerly embedded
|
depends on `s3s`, `http`, `hyper`/`hyper-util`/`hyper-rustls`, and
|
||||||
`crates/ecstore/src/client/` by rustfs/backlog#1842), not inside ecstore.
|
`reqwest`. Target state: the engine's need to act as an S3 client
|
||||||
- ⚠️ PARTIALLY VIOLATED: serving-side `s3s` references remain in ecstore
|
(tiering, replication targets) is served by an extracted client crate,
|
||||||
(bucket metadata/replication/lifecycle DTOs and error mapping). The
|
and ecstore holds no wire or DTO types.
|
||||||
count is ratcheted shrink-only by `scripts/check_s3s_footprint.sh`
|
|
||||||
(`S3S_ECSTORE_FILES_BASELINE`; the `object_lock` module was converted to
|
|
||||||
storage-level types as the first ratchet step). Target state: the
|
|
||||||
baseline reaches zero and ecstore's `Cargo.toml` drops `s3s`.
|
|
||||||
|
|
||||||
5. **The `rustfs` binary crate is the only place that wires everything together.**
|
5. **The `rustfs` binary crate is the only place that wires everything together.**
|
||||||
Individual crates should be testable in isolation.
|
Individual crates should be testable in isolation.
|
||||||
@@ -332,8 +321,6 @@ The binary (`main.rs`) boots in this order:
|
|||||||
|
|
||||||
- **"Where is replication configured?"**
|
- **"Where is replication configured?"**
|
||||||
`admin/handlers/replication.rs` and `admin/handlers/site_replication.rs` for API,
|
`admin/handlers/replication.rs` and `admin/handlers/site_replication.rs` for API,
|
||||||
`rustfs/src/site_replication/` for the site-replication service subsystem
|
|
||||||
(state, peer transport, retry queue, repair, hooks),
|
|
||||||
`ecstore/src/bucket/replication/` for engine
|
`ecstore/src/bucket/replication/` for engine
|
||||||
|
|
||||||
- **"Where do I add a new admin endpoint?"**
|
- **"Where do I add a new admin endpoint?"**
|
||||||
|
|||||||
@@ -8,21 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Multipart admission queue**: an `UploadPart` waiting for a foreground write permit now waits at most 10 s by default (`RUSTFS_PUT_MULTIPART_FOREGROUND_ADMISSION_WAIT_TIMEOUT_MS`, previously 30 s), so a queued part returns S3 `SlowDown` before the client's socket write timeout drops the connection. Separately, the API listener no longer forces a 4 MiB `SO_RCVBUF` on every accepted socket (kernel autotuning applies; `RUSTFS_HTTP_SOCKET_RECV_BUFFER_BYTES` restores a fixed size), so a queued part no longer lets up to 8 MiB of unread body accumulate in kernel memory per connection, which is what throttled whole nodes under SDK-default multipart concurrency. Fixes #7385.
|
|
||||||
- **Helm Ingress**: `customAnnotations` are now merged with class-specific annotations (nginx/traefik) instead of being ignored when `ingress.className` is set.
|
- **Helm Ingress**: `customAnnotations` are now merged with class-specific annotations (nginx/traefik) instead of being ignored when `ingress.className` is set.
|
||||||
- **Per-pool erasure parity**: Erasure parity (STANDARD and reduced-redundancy) is now resolved independently for every pool instead of reusing the first pool's value. A heterogeneous topology — for example a 4-drive pool plus a 2-drive pool created during expansion — previously inherited the first pool's parity and could resolve to zero data shards in the smaller pool, panicking Reed-Solomon construction on write. Automatic parity now resolves per pool (for example `2+2` in the 4-drive pool and `1+1` in the 2-drive pool). Fixes #4801.
|
- **Per-pool erasure parity**: Erasure parity (STANDARD and reduced-redundancy) is now resolved independently for every pool instead of reusing the first pool's value. A heterogeneous topology — for example a 4-drive pool plus a 2-drive pool created during expansion — previously inherited the first pool's parity and could resolve to zero data shards in the smaller pool, panicking Reed-Solomon construction on write. Automatic parity now resolves per pool (for example `2+2` in the 4-drive pool and `1+1` in the 2-drive pool). Fixes #4801.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **On-Demand Migration**: Lazy, pull-style migration of an existing S3-compatible bucket into RustFS. A local bucket is attached to an external source bucket; a GET for a key that does not exist locally fetches it from the source, streams it to the client, and stores it locally in the same pass, so every later read is served locally. The module is on by default; set `RUSTFS_ON_DEMAND_MIGRATION_ENABLED=false` on every node to turn it off. A bucket with no source configured behaves exactly as before — the runtime never intervenes on its reads and makes no outbound call. Operator guide at `docs/operations/on-demand-migration.md`.
|
|
||||||
- Per-bucket configuration persisted as `on-demand-migration.json` in the bucket metadata: source provider (`s3`, `aws`, `minio`, `rustfs`, `r2`, `gcs`), endpoint, region, addressing style, credentials and TLS material, an optional key-prefix filter and source-prefix rewrite, and a policy block covering the inline size threshold, multipart part size, concurrency, queue capacity, timeouts, bandwidth limit and negative-cache TTL
|
|
||||||
- Admin routes under `/rustfs/admin/v3/on-demand-migration/{bucket}`: `PUT` (with `?dry-run=true` to validate and probe the source without saving), `GET`, `DELETE`, `GET .../status`, plus `POST .../backfill?op=start|cancel` and `GET .../backfill` for the background full-backfill job with its resumable checkpoint. Authorized by the new `admin:GetBucketOnDemandMigration` and `admin:SetBucketOnDemandMigration` actions; every response redacts `secret_key` and `session_token`
|
|
||||||
- Read paths: an object at or below `policy.inline_max_bytes` (16 MiB by default) is teed to the client and to the local store in a single source read; a larger object or a Range read streams through and a background pull stores the whole object. A HEAD miss is proxied to the source and stores nothing (`policy.head = local_only` disables it). Every source-backed response carries `x-rustfs-on-demand-migration: source`
|
|
||||||
- Protections: a per-source circuit breaker, a per-key negative cache, singleflight per key, a concurrency limit and a bounded pull queue shared by the inline and background paths, an optional bandwidth limit, an anti-loop request marker, and the shared outbound-endpoint (SSRF) policy
|
|
||||||
- Metrics under `rustfs_on_demand_migration_*` (`requests_total`, `pulled_bytes_total`, `pulled_objects_total`, `pull_failures_total`, `inflight_pulls`, `queue_depth`, `source_latency_seconds_*`, `breaker_state`), mirrored per node by the admin status route
|
|
||||||
- Listings: `ListObjects` v1 remains local with ordinary key markers. `ListObjectsV2` can merge source objects when `policy.list_through = true`; this is off by default
|
|
||||||
- Upgrade and rollback: finish upgrading every node before enabling ODM. An rc.5 node that writes bucket configuration drops the ODM fields from metadata; neither a later restart nor moving the service out of ECStore recovers them. Before rollback, disable ODM and securely retain the original full configuration and credentials. After every node returns to a compatible version, restore and validate that configuration. Redacted exports cannot replace the credential backup; source-only objects are unavailable through RustFS while ODM is disabled. See the upgrade and rollback section of `docs/operations/on-demand-migration.md`
|
|
||||||
- Optional Google dependencies: default and `full` server builds retain native GCS support. `cargo build -p rustfs --no-default-features --features ftps,webdav` excludes Google SDKs while preserving configuration decoding and redaction; native GCS ODM and tier operations require the `gcs` feature. Do not use that build with existing GCS-tiered data
|
|
||||||
- Limitations: PUT and DELETE never reach the source; a source object updated after it was pulled is not re-fetched; SSE-C source objects are unsupported and answer 424; `Last-Modified` on a pulled object is the local write time, with the source timestamp kept in metadata
|
|
||||||
- **NATS JetStream Publish Path**: Opt-in at-least-once delivery for the NATS notify and audit targets. A NATS Core publish flushes to the connection without awaiting a broker acknowledgement, so an event can be lost across a broker restart or a reconnect after the send queue has already cleared it. A queued event now clears only after the JetStream `PublishAck`, so bucket notifications survive those interruptions. Off by default and byte-identical to the NATS Core path when disabled.
|
- **NATS JetStream Publish Path**: Opt-in at-least-once delivery for the NATS notify and audit targets. A NATS Core publish flushes to the connection without awaiting a broker acknowledgement, so an event can be lost across a broker restart or a reconnect after the send queue has already cleared it. A queued event now clears only after the JetStream `PublishAck`, so bucket notifications survive those interruptions. Off by default and byte-identical to the NATS Core path when disabled.
|
||||||
- Three configuration keys per target: `JETSTREAM_ENABLE`, `JETSTREAM_STREAM_NAME`, and `JETSTREAM_ACK_TIMEOUT_SECS`, under the `RUSTFS_NOTIFY_NATS_` and `RUSTFS_AUDIT_NATS_` prefixes
|
- Three configuration keys per target: `JETSTREAM_ENABLE`, `JETSTREAM_STREAM_NAME`, and `JETSTREAM_ACK_TIMEOUT_SECS`, under the `RUSTFS_NOTIFY_NATS_` and `RUSTFS_AUDIT_NATS_` prefixes
|
||||||
- Durable store-and-forward with a stable dedup id sent as the `Nats-Msg-Id` header, so a replay after a crash is collapsed by the server duplicate window
|
- Durable store-and-forward with a stable dedup id sent as the `Nats-Msg-Id` header, so a replay after a crash is collapsed by the server duplicate window
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ what Claude Code needs on top: commands and pointers.
|
|||||||
cargo build --release --bin rustfs # production binary
|
cargo build --release --bin rustfs # production binary
|
||||||
cargo check -p <crate> # fast type-check one crate
|
cargo check -p <crate> # fast type-check one crate
|
||||||
cargo test -p <crate> # test one crate
|
cargo test -p <crate> # test one crate
|
||||||
cargo fmt --all --check # for Rust changes; see AGENTS.md verification tiers
|
cargo fmt --all # format (required before PR)
|
||||||
make pre-commit # fast gate: fmt + arch checks + quick-check (NO clippy/tests)
|
make pre-commit # fast gate: fmt + arch checks + quick-check (NO clippy/tests)
|
||||||
make pre-pr # optional full gate for broad cross-module changes
|
make pre-pr # full pre-PR gate: fmt + arch checks + clippy + tests
|
||||||
make build-docker BUILD_OS=ubuntu22.04
|
make build-docker BUILD_OS=ubuntu22.04
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ make build-docker BUILD_OS=ubuntu22.04
|
|||||||
|
|
||||||
## Where to look (do not duplicate here)
|
## Where to look (do not duplicate here)
|
||||||
|
|
||||||
- Agent knowledge base index and doc-writing rules: [docs/architecture/README.md](docs/architecture/README.md)
|
|
||||||
- Crate membership: `Cargo.toml` `[workspace].members`
|
- Crate membership: `Cargo.toml` `[workspace].members`
|
||||||
- Architecture, layering, crate map: [ARCHITECTURE.md](ARCHITECTURE.md)
|
- Architecture, layering, crate map: [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||||
- Migration guardrails & readiness contracts: [docs/architecture/](docs/architecture/README.md)
|
- Migration guardrails & readiness contracts: [docs/architecture/](docs/architecture/README.md)
|
||||||
@@ -42,5 +41,5 @@ make build-docker BUILD_OS=ubuntu22.04
|
|||||||
|
|
||||||
Repo-wide domain invariants (dual internal metadata keys, defensive UUID
|
Repo-wide domain invariants (dual internal metadata keys, defensive UUID
|
||||||
reads, unversioned tier buckets) live in [AGENTS.md](AGENTS.md) under
|
reads, unversioned tier buckets) live in [AGENTS.md](AGENTS.md) under
|
||||||
"Cross-Cutting Storage Invariants" — read them before touching metadata or
|
"Cross-Cutting Domain Invariants" — read them before touching metadata or
|
||||||
tiering code.
|
tiering code.
|
||||||
|
|||||||
+41
-28
@@ -62,20 +62,12 @@ make test
|
|||||||
# Fast pre-commit gate — see below for exactly what it runs
|
# Fast pre-commit gate — see below for exactly what it runs
|
||||||
make pre-commit
|
make pre-commit
|
||||||
|
|
||||||
# Optional full gate for broad cross-module changes (pre-commit + clippy + tests)
|
# Full pre-PR gate (pre-commit gates + clippy + tests)
|
||||||
make pre-pr
|
make pre-pr
|
||||||
```
|
```
|
||||||
|
|
||||||
> `make test` requires [cargo-nextest](https://nexte.st) (CI runs it and only nextest honours `.config/nextest.toml` test-groups). Install it with `cargo install cargo-nextest --locked` or a prebuilt binary (see https://nexte.st/docs/installation/). To run the plain `cargo test` fallback anyway (results not authoritative — serialization semantics differ from CI), set `RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1`.
|
> `make test` requires [cargo-nextest](https://nexte.st) (CI runs it and only nextest honours `.config/nextest.toml` test-groups). Install it with `cargo install cargo-nextest --locked` or a prebuilt binary (see https://nexte.st/docs/installation/). To run the plain `cargo test` fallback anyway (results not authoritative — serialization semantics differ from CI), set `RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1`.
|
||||||
|
|
||||||
> Some guard checks are Python (`test-wiring-check` in `make pre-commit`, plus the
|
|
||||||
> security-coverage and scheduled-validation self-tests in `make test`) and import
|
|
||||||
> `tomllib`, so they need **Python 3.11+**. Make resolves the interpreter through
|
|
||||||
> `scripts/python_bin.sh`, which prefers a `python3.11`+ on `PATH` and otherwise falls
|
|
||||||
> back to `uv run --python 3.12`. macOS ships `/usr/bin/python3` at 3.9, so install a
|
|
||||||
> newer one (`brew install python@3.12`) or [uv](https://docs.astral.sh/uv/); pin a
|
|
||||||
> specific interpreter with `RUSTFS_PYTHON=/path/to/python3.12`.
|
|
||||||
|
|
||||||
> For the full test-layer taxonomy (unit / ecstore black-box / e2e / s3s-e2e / S3 compatibility / chaos / fuzz / bench), each layer's entry command, the naming conventions the migration gate depends on, and the serial/nextest rules, see [docs/testing/README.md](docs/testing/README.md).
|
> For the full test-layer taxonomy (unit / ecstore black-box / e2e / s3s-e2e / S3 compatibility / chaos / fuzz / bench), each layer's entry command, the naming conventions the migration gate depends on, and the serial/nextest rules, see [docs/testing/README.md](docs/testing/README.md).
|
||||||
|
|
||||||
> For the event, timeout, required-status, and local reproduction matrix, see [docs/testing/ci-gates.md](docs/testing/ci-gates.md).
|
> For the event, timeout, required-status, and local reproduction matrix, see [docs/testing/ci-gates.md](docs/testing/ci-gates.md).
|
||||||
@@ -96,30 +88,34 @@ make pre-pr
|
|||||||
8. `quick-check` — `cargo check --workspace --exclude e2e_test`
|
8. `quick-check` — `cargo check --workspace --exclude e2e_test`
|
||||||
|
|
||||||
**`make pre-commit` does NOT run clippy and does NOT run any tests.**
|
**`make pre-commit` does NOT run clippy and does NOT run any tests.**
|
||||||
It does not replace the scoped Clippy and test checks applicable to a change.
|
A green `make pre-commit` is not enough to open a pull request.
|
||||||
|
|
||||||
`make pre-pr` is the **full** gate: it runs all of the guard checks above,
|
`make pre-pr` is the **full** gate: it runs all of the guard checks above,
|
||||||
then `clippy-check` (`cargo clippy --all-targets --all-features -- -D warnings`)
|
then `clippy-check` (`cargo clippy --all-targets --all-features -- -D warnings`)
|
||||||
and `test` (shell script tests, workspace tests excluding `e2e_test`, and doc
|
and `test` (shell script tests, workspace tests excluding `e2e_test`, and doc
|
||||||
tests). Complete the applicable multi-role adversarial review described in
|
tests). Complete the applicable multi-role adversarial review described in
|
||||||
`AGENTS.md` first. Do not run `make pre-pr` locally by default before opening or
|
`AGENTS.md` before running `make pre-pr`; then run the gate before opening or
|
||||||
updating a pull request. Consider it only for a broad change that spans multiple
|
updating a pull request. This is what CI enforces.
|
||||||
modules and whose impact cannot be bounded by targeted checks; decide from the
|
|
||||||
affected boundaries and risks. CI still runs its configured repository gates.
|
|
||||||
|
|
||||||
### 🔒 Git Pre-commit Hooks (optional)
|
### 🔒 Git Pre-commit Hooks (optional)
|
||||||
|
|
||||||
The optional hook uses the checked-in `.pre-commit-config.yaml`. Install [pre-commit](https://pre-commit.com/#installation), then run this from the checkout or a linked worktree:
|
Git hooks are **not** versioned in this repository, so a fresh clone has no
|
||||||
|
active pre-commit hook. If you add your own `.git/hooks/pre-commit` (a good
|
||||||
|
choice is a one-liner that runs `make pre-commit`), you can mark it executable
|
||||||
|
with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup-hooks
|
make setup-hooks
|
||||||
```
|
```
|
||||||
|
|
||||||
The hook runs `cargo fmt --all --check` when staged files include Rust source. It does not compile the workspace or run tests. Fix formatting with `cargo fmt --all`, inspect and stage the result, then commit again.
|
Or manually:
|
||||||
|
|
||||||
`pre-commit install` resolves Git's hook directory for linked worktrees and preserves an existing hook in migration mode. If you use `core.hooksPath`, keep that hook manager and integrate `pre-commit run` there; the installer refuses to silently replace that configuration.
|
```bash
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
A local hook provides early formatting feedback. With or without it, follow the verification tiers in `AGENTS.md`, run relevant behavioral tests, and satisfy the CI merge gates. `make pre-commit` and `make dev-check` remain explicit broader commands.
|
With or without a hook, the expectation is the same: run `make pre-commit`
|
||||||
|
before committing and `make pre-pr` before opening a pull request.
|
||||||
|
|
||||||
### 📝 Formatting Configuration
|
### 📝 Formatting Configuration
|
||||||
|
|
||||||
@@ -131,16 +127,34 @@ fn_call_width = 90
|
|||||||
single_line_let_else_max_width = 100
|
single_line_let_else_max_width = 100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🚫 Commit Prevention
|
||||||
|
|
||||||
|
If you set up a pre-commit hook and your code doesn't meet the formatting requirements, the hook will:
|
||||||
|
|
||||||
|
1. **Block the commit** and show clear error messages
|
||||||
|
2. **Provide exact commands** to fix the issues
|
||||||
|
3. **Guide you through** the resolution process
|
||||||
|
|
||||||
|
Example output when formatting fails:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Code formatting check failed!
|
||||||
|
💡 Please run 'cargo fmt --all' to format your code before committing.
|
||||||
|
|
||||||
|
🔧 Quick fix:
|
||||||
|
cargo fmt --all
|
||||||
|
git add .
|
||||||
|
git commit
|
||||||
|
```
|
||||||
|
|
||||||
### 🔄 Development Workflow
|
### 🔄 Development Workflow
|
||||||
|
|
||||||
1. **Make your changes**
|
1. **Make your changes**
|
||||||
2. **Format your code**: `make fmt` or `cargo fmt --all`
|
2. **Format your code**: `make fmt` or `cargo fmt --all`
|
||||||
3. **Select relevant checks** using the validation tier in `AGENTS.md`; use `make pre-commit` when its broader fast gate adds useful coverage
|
3. **Run the fast gate**: `make pre-commit` (no clippy, no tests)
|
||||||
4. **Commit your changes**: `git commit -m "your message"`
|
4. **Commit your changes**: `git commit -m "your message"`
|
||||||
5. **Complete the applicable multi-role adversarial review** for non-exempt changes (see `AGENTS.md`)
|
5. **Complete the applicable multi-role adversarial review** for non-exempt changes (see `AGENTS.md`)
|
||||||
6. **Run applicable scoped checks before opening/updating a PR**; consider
|
6. **Run the full gate before opening/updating a PR**: `make pre-pr` (clippy + tests)
|
||||||
`make pre-pr` only for broad cross-module changes whose impact cannot be
|
|
||||||
bounded by targeted checks
|
|
||||||
7. **Push to your branch**: `git push`
|
7. **Push to your branch**: `git push`
|
||||||
|
|
||||||
### 🛠️ IDE Integration
|
### 🛠️ IDE Integration
|
||||||
@@ -179,12 +193,11 @@ Configure your IDE to:
|
|||||||
#### Pre-commit hook not running?
|
#### Pre-commit hook not running?
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pre-commit validate-config
|
# Check if hook is executable
|
||||||
pre-commit run --all-files
|
ls -la .git/hooks/pre-commit
|
||||||
# Inspect any configured hook manager; do not overwrite it.
|
|
||||||
git config --get core.hooksPath
|
# Make it executable if needed
|
||||||
# Install if no separate hook manager is configured.
|
chmod +x .git/hooks/pre-commit
|
||||||
make setup-hooks
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Formatting issues?
|
#### Formatting issues?
|
||||||
|
|||||||
Generated
+518
-1162
File diff suppressed because it is too large
Load Diff
+93
-112
@@ -26,10 +26,8 @@ members = [
|
|||||||
"crates/e2e_test", # End-to-end test suite
|
"crates/e2e_test", # End-to-end test suite
|
||||||
"crates/filemeta", # File metadata management
|
"crates/filemeta", # File metadata management
|
||||||
"crates/heal", # Erasure set and object healing
|
"crates/heal", # Erasure set and object healing
|
||||||
"crates/heal-contracts", # Heal request/response channel contracts
|
|
||||||
"crates/iam", # Identity and Access Management
|
"crates/iam", # Identity and Access Management
|
||||||
"crates/keystone", # OpenStack Keystone integration
|
"crates/keystone", # OpenStack Keystone integration
|
||||||
"crates/license", # License and entitlement provider contracts
|
|
||||||
"crates/lifecycle", # Lifecycle rule evaluation contracts
|
"crates/lifecycle", # Lifecycle rule evaluation contracts
|
||||||
"crates/kms", # Key Management Service
|
"crates/kms", # Key Management Service
|
||||||
"crates/lock", # Distributed locking implementation
|
"crates/lock", # Distributed locking implementation
|
||||||
@@ -46,13 +44,11 @@ members = [
|
|||||||
"crates/rio-v2", # MinIO on-disk format compatibility I/O layer (feature-gated, ships in no default build)
|
"crates/rio-v2", # MinIO on-disk format compatibility I/O layer (feature-gated, ships in no default build)
|
||||||
"crates/replication", # Replication contracts and wire formats
|
"crates/replication", # Replication contracts and wire formats
|
||||||
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
||||||
"crates/s3-client", # S3 client for engine-side consumption of remote S3 endpoints (tiering, transition targets)
|
|
||||||
"crates/s3-types", # S3 event type definitions
|
"crates/s3-types", # S3 event type definitions
|
||||||
"crates/s3-ops", # S3 operation definitions and mapping
|
"crates/s3-ops", # S3 operation definitions and mapping
|
||||||
"crates/s3select-api", # S3 Select API interface
|
"crates/s3select-api", # S3 Select API interface
|
||||||
"crates/s3select-query", # S3 Select query engine
|
"crates/s3select-query", # S3 Select query engine
|
||||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||||
"crates/scanner-metrics", # Scanner metrics and cycle telemetry
|
|
||||||
"crates/security-governance", # Security governance contracts
|
"crates/security-governance", # Security governance contracts
|
||||||
"crates/extension-schema", # Extension schema contracts
|
"crates/extension-schema", # Extension schema contracts
|
||||||
"crates/signer", # client signer
|
"crates/signer", # client signer
|
||||||
@@ -72,8 +68,8 @@ resolver = "3"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rustfs/rustfs"
|
repository = "https://github.com/rustfs/rustfs"
|
||||||
rust-version = "1.98.0"
|
rust-version = "1.97.1"
|
||||||
version = "1.0.0-rc.5"
|
version = "1.0.0-rc.3"
|
||||||
homepage = "https://rustfs.com"
|
homepage = "https://rustfs.com"
|
||||||
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
||||||
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
||||||
@@ -90,62 +86,58 @@ redundant_clone = "warn"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# RustFS Internal Crates
|
# RustFS Internal Crates
|
||||||
rustfs = { path = "./rustfs", version = "1.0.0-rc.5" }
|
rustfs = { path = "./rustfs", version = "1.0.0-rc.3" }
|
||||||
rustfs-heal = { path = "crates/heal", version = "1.0.0-rc.5" }
|
rustfs-heal = { path = "crates/heal", version = "1.0.0-rc.3" }
|
||||||
rustfs-heal-contracts = { path = "crates/heal-contracts", version = "1.0.0-rc.5" }
|
rustfs-audit = { path = "crates/audit", version = "1.0.0-rc.3" }
|
||||||
rustfs-scanner-metrics = { path = "crates/scanner-metrics", version = "1.0.0-rc.5" }
|
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-rc.3" }
|
||||||
rustfs-audit = { path = "crates/audit", version = "1.0.0-rc.5" }
|
rustfs-common = { path = "crates/common", version = "1.0.0-rc.3" }
|
||||||
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-rc.5" }
|
rustfs-data-usage = { path = "crates/data-usage", version = "1.0.0-rc.3" }
|
||||||
rustfs-common = { path = "crates/common", version = "1.0.0-rc.5" }
|
rustfs-config = { path = "./crates/config", version = "1.0.0-rc.3" }
|
||||||
rustfs-data-usage = { path = "crates/data-usage", version = "1.0.0-rc.5" }
|
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-rc.3" }
|
||||||
rustfs-config = { path = "./crates/config", version = "1.0.0-rc.5" }
|
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-rc.3" }
|
||||||
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-rc.5" }
|
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-rc.3" }
|
||||||
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-rc.5" }
|
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-rc.3" }
|
||||||
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-rc.5" }
|
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-rc.3" }
|
||||||
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-rc.5" }
|
rustfs-iam = { path = "crates/iam", version = "1.0.0-rc.3" }
|
||||||
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-rc.5" }
|
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-rc.3" }
|
||||||
rustfs-iam = { path = "crates/iam", version = "1.0.0-rc.5" }
|
rustfs-lifecycle = { path = "crates/lifecycle", version = "1.0.0-rc.3" }
|
||||||
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-rc.5" }
|
rustfs-kms = { path = "crates/kms", version = "1.0.0-rc.3" }
|
||||||
rustfs-license = { path = "crates/license", version = "1.0.0-rc.5" }
|
rustfs-lock = { path = "crates/lock", version = "1.0.0-rc.3" }
|
||||||
rustfs-lifecycle = { path = "crates/lifecycle", version = "1.0.0-rc.5" }
|
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-rc.3" }
|
||||||
rustfs-kms = { path = "crates/kms", version = "1.0.0-rc.5" }
|
rustfs-notify = { path = "crates/notify", version = "1.0.0-rc.3" }
|
||||||
rustfs-lock = { path = "crates/lock", version = "1.0.0-rc.5" }
|
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-rc.3" }
|
||||||
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-rc.5" }
|
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-rc.3" }
|
||||||
rustfs-notify = { path = "crates/notify", version = "1.0.0-rc.5" }
|
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-rc.3" }
|
||||||
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-rc.5" }
|
rustfs-object-data-cache = { path = "crates/object-data-cache", version = "1.0.0-rc.3", default-features = false }
|
||||||
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-rc.5" }
|
rustfs-log-analyzer = { path = "crates/log-analyzer", version = "1.0.0-rc.3" }
|
||||||
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-rc.5" }
|
rustfs-obs = { path = "crates/obs", version = "1.0.0-rc.3" }
|
||||||
rustfs-object-data-cache = { path = "crates/object-data-cache", version = "1.0.0-rc.5", default-features = false }
|
rustfs-policy = { path = "crates/policy", version = "1.0.0-rc.3" }
|
||||||
rustfs-log-analyzer = { path = "crates/log-analyzer", version = "1.0.0-rc.5" }
|
rustfs-protos = { path = "crates/protos", version = "1.0.0-rc.3" }
|
||||||
rustfs-obs = { path = "crates/obs", version = "1.0.0-rc.5" }
|
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-rc.3" }
|
||||||
rustfs-policy = { path = "crates/policy", version = "1.0.0-rc.5" }
|
rustfs-replication = { path = "crates/replication", version = "1.0.0-rc.3" }
|
||||||
rustfs-protos = { path = "crates/protos", version = "1.0.0-rc.5" }
|
rustfs-rio = { path = "crates/rio", version = "1.0.0-rc.3" }
|
||||||
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-rc.5" }
|
rustfs-rio-v2 = { path = "crates/rio-v2", version = "1.0.0-rc.3" }
|
||||||
rustfs-replication = { path = "crates/replication", version = "1.0.0-rc.5" }
|
rustfs-s3-types = { path = "crates/s3-types", version = "1.0.0-rc.3" }
|
||||||
rustfs-rio = { path = "crates/rio", version = "1.0.0-rc.5" }
|
rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-rc.3" }
|
||||||
rustfs-rio-v2 = { path = "crates/rio-v2", version = "1.0.0-rc.5" }
|
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-rc.3" }
|
||||||
rustfs-s3-client = { path = "crates/s3-client", version = "1.0.0-rc.5" }
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-rc.3" }
|
||||||
rustfs-s3-types = { path = "crates/s3-types", version = "1.0.0-rc.5" }
|
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-rc.3" }
|
||||||
rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-rc.5" }
|
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-rc.3" }
|
||||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-rc.5" }
|
rustfs-extension-schema = { path = "crates/extension-schema", version = "1.0.0-rc.3" }
|
||||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-rc.5" }
|
rustfs-signer = { path = "crates/signer", version = "1.0.0-rc.3" }
|
||||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-rc.5" }
|
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-rc.3" }
|
||||||
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-rc.5" }
|
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-rc.3" }
|
||||||
rustfs-extension-schema = { path = "crates/extension-schema", version = "1.0.0-rc.5" }
|
rustfs-targets = { path = "crates/targets", version = "1.0.0-rc.3" }
|
||||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-rc.5" }
|
rustfs-test-utils = { path = "crates/test-utils", version = "1.0.0-rc.3" }
|
||||||
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-rc.5" }
|
rustfs-tls-runtime = { path = "crates/tls-runtime", version = "1.0.0-rc.3" }
|
||||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-rc.5" }
|
rustfs-utils = { path = "crates/utils", version = "1.0.0-rc.3" }
|
||||||
rustfs-targets = { path = "crates/targets", version = "1.0.0-rc.5" }
|
rustfs-zip = { path = "./crates/zip", version = "1.0.0-rc.3" }
|
||||||
rustfs-test-utils = { path = "crates/test-utils", version = "1.0.0-rc.5" }
|
|
||||||
rustfs-tls-runtime = { path = "crates/tls-runtime", version = "1.0.0-rc.5" }
|
|
||||||
rustfs-utils = { path = "crates/utils", version = "1.0.0-rc.5" }
|
|
||||||
rustfs-zip = { path = "./crates/zip", version = "1.0.0-rc.5" }
|
|
||||||
|
|
||||||
# Async Runtime and Networking
|
# Async Runtime and Networking
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
async_zip = { default-features = false, version = "0.0.19" }
|
async_zip = { default-features = false, version = "0.0.19" }
|
||||||
mysql_async = { default-features = false, version = "0.37.1" }
|
mysql_async = { default-features = false, version = "0.37" }
|
||||||
async-compression = { version = "0.4.44" }
|
async-compression = { version = "0.4.43" }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
async-nats = { version = "0.50.0", default-features = false }
|
async-nats = { version = "0.50.0", default-features = false }
|
||||||
@@ -155,9 +147,9 @@ futures-core = "0.3.34"
|
|||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.1"
|
||||||
futures-util = "0.3.34"
|
futures-util = "0.3.34"
|
||||||
pollster = "1.0.1"
|
pollster = "1.0.1"
|
||||||
pulsar = { default-features = false, version = "6.9.0" }
|
pulsar = { default-features = false, version = "6.8.0" }
|
||||||
lapin = { default-features = false, version = "4.10.0" }
|
lapin = { default-features = false, version = "4.10.0" }
|
||||||
hyper = { version = "1.11.1" }
|
hyper = { version = "1.11.0" }
|
||||||
hyper-rustls = { default-features = false, version = "0.27.9" }
|
hyper-rustls = { default-features = false, version = "0.27.9" }
|
||||||
hyper-util = { version = "0.1.20" }
|
hyper-util = { version = "0.1.20" }
|
||||||
http = "1.5.0"
|
http = "1.5.0"
|
||||||
@@ -165,10 +157,10 @@ http-body = "1.1.0"
|
|||||||
http-body-util = "0.1.5"
|
http-body-util = "0.1.5"
|
||||||
minlz = "1.2.3"
|
minlz = "1.2.3"
|
||||||
reqwest = "0.13.4"
|
reqwest = "0.13.4"
|
||||||
rustfs-kafka-async = { version = "1.3.1" }
|
rustfs-kafka-async = { version = "1.2.0" }
|
||||||
socket2 = { version = "0.6.5" }
|
socket2 = { version = "0.6.5" }
|
||||||
tokio = { version = "1.53.1" }
|
tokio = { version = "1.53.1" }
|
||||||
tokio-rustls = { default-features = false, version = "0.26.5" }
|
tokio-rustls = { default-features = false, version = "0.26.4" }
|
||||||
tokio-stream = { version = "0.1.19" }
|
tokio-stream = { version = "0.1.19" }
|
||||||
tokio-test = "0.4.5"
|
tokio-test = "0.4.5"
|
||||||
tokio-util = { version = "0.7.19" }
|
tokio-util = { version = "0.7.19" }
|
||||||
@@ -176,7 +168,7 @@ tonic = { version = "0.14.6" }
|
|||||||
tonic-prost = { version = "0.14.6" }
|
tonic-prost = { version = "0.14.6" }
|
||||||
tonic-prost-build = { version = "0.14.6" }
|
tonic-prost-build = { version = "0.14.6" }
|
||||||
tower = { version = "0.5.3" }
|
tower = { version = "0.5.3" }
|
||||||
tower-http = { version = "0.7.1" }
|
tower-http = { version = "0.7.0" }
|
||||||
|
|
||||||
# Serialization and Data Formats
|
# Serialization and Data Formats
|
||||||
apache-avro = { version = "0.22.0", features = ["snappy", "zstandard"] }
|
apache-avro = { version = "0.22.0", features = ["snappy", "zstandard"] }
|
||||||
@@ -191,7 +183,6 @@ rmp = { version = "0.8.15" }
|
|||||||
rmp-serde = { version = "1.3.1" }
|
rmp-serde = { version = "1.3.1" }
|
||||||
serde = { version = "1.0.229" }
|
serde = { version = "1.0.229" }
|
||||||
serde_ignored = { version = "0.1" }
|
serde_ignored = { version = "0.1" }
|
||||||
serde_with = { version = "3", default-features = false, features = ["macros", "std"] }
|
|
||||||
serde_json = { version = "1.0.151" }
|
serde_json = { version = "1.0.151" }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
|
|
||||||
@@ -200,10 +191,10 @@ serde_urlencoded = "0.7.1"
|
|||||||
# matching stable releases are not available yet, while previous stable lines
|
# matching stable releases are not available yet, while previous stable lines
|
||||||
# have incompatible APIs. Keep them exact-pinned and monitor upstream for stable
|
# have incompatible APIs. Keep them exact-pinned and monitor upstream for stable
|
||||||
# releases.
|
# releases.
|
||||||
aes-gcm = { version = "0.11.1" }
|
aes-gcm = { version = "=0.11.1" }
|
||||||
argon2 = { version = "0.6.0" }
|
argon2 = { version = "=0.6.0-rc.8" }
|
||||||
blake2 = "0.11.0"
|
blake2 = "=0.11.0-rc.6"
|
||||||
chacha20poly1305 = { version = "0.11.0" }
|
chacha20poly1305 = { version = "=0.11.0" }
|
||||||
crc-fast = "1.10.0"
|
crc-fast = "1.10.0"
|
||||||
hmac = { version = "0.13.0" }
|
hmac = { version = "0.13.0" }
|
||||||
jsonwebtoken = { version = "11.0.0" }
|
jsonwebtoken = { version = "11.0.0" }
|
||||||
@@ -235,44 +226,38 @@ tokio-postgres-rustls = "0.14.0"
|
|||||||
# Utilities and Tools
|
# Utilities and Tools
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
arc-swap = "1.9.2"
|
arc-swap = "1.9.2"
|
||||||
# RUSTFS_COMPAT_TODO(tokio-tar-extension-limits): keep the fork pin while Snowball and Swift still depend on it. Remove after Snowball uses a released tar-codec/tar-framing API that exposes precedence-resolved MinIO vendor records, RustFS preserves cancellation-safe ownership of large streamed members, footerless minio-go input is accepted only at an authenticated complete request boundary, the existing resource-limit, cancellation, and error-fuse regressions pass, and Swift no longer needs this fork.
|
astral-tokio-tar = "0.6.4"
|
||||||
astral-tokio-tar = { git = "https://github.com/cxymds/tokio-tar.git", rev = "603756478b7668436e464519c77ccac22a99ba96" }
|
|
||||||
# Candidate Snowball parser versions exercised by rustfs-zip compatibility fixtures.
|
|
||||||
tar-codec = "0.0.14"
|
|
||||||
tar-framing = "0.0.14"
|
|
||||||
atoi = "3.1.0"
|
atoi = "3.1.0"
|
||||||
atomic_enum = "0.3.0"
|
atomic_enum = "0.3.0"
|
||||||
aws-config = { version = "1.12.0" }
|
aws-config = { version = "1.11.0" }
|
||||||
aws-credential-types = { version = "1.3.0" }
|
aws-credential-types = { version = "1.3.0" }
|
||||||
aws-sdk-kms = { default-features = false, version = "1.118.0" }
|
aws-sdk-kms = { default-features = false, version = "1.116.0" }
|
||||||
aws-sdk-s3 = { default-features = false, version = "1.145.0" }
|
aws-sdk-s3 = { default-features = false, version = "1.143.0" }
|
||||||
aws-sdk-sts = { default-features = false, version = "1.114.0" }
|
aws-sdk-sts = { default-features = false, version = "1.112.0" }
|
||||||
aws-smithy-async = { version = "1.3.0" }
|
|
||||||
aws-smithy-http-client = { default-features = false, version = "1.4.0" }
|
aws-smithy-http-client = { default-features = false, version = "1.4.0" }
|
||||||
aws-smithy-runtime-api = { version = "1.16.0" }
|
aws-smithy-runtime-api = { version = "1.15.0" }
|
||||||
aws-smithy-types = { version = "1.6.3" }
|
aws-smithy-types = { version = "1.6.2" }
|
||||||
|
base64 = "0.23.1"
|
||||||
base64-simd = "0.8.0"
|
base64-simd = "0.8.0"
|
||||||
brotli = "9.0.0"
|
brotli = "8.0.4"
|
||||||
clap = { version = "4.6.6" }
|
clap = { version = "4.6.6" }
|
||||||
const-str = { version = "1.1.0" }
|
const-str = { version = "1.1.0" }
|
||||||
convert_case = "0.12.0"
|
convert_case = "0.11.0"
|
||||||
criterion = { version = "0.8" }
|
criterion = { version = "0.8" }
|
||||||
crossbeam-queue = "0.3.14"
|
crossbeam-queue = "0.3.13"
|
||||||
crossbeam-channel = "0.5.17"
|
crossbeam-channel = "0.5.16"
|
||||||
crossbeam-deque = "0.8.8"
|
crossbeam-deque = "0.8.7"
|
||||||
crossbeam-utils = "0.8.23"
|
crossbeam-utils = "0.8.22"
|
||||||
datafusion = { default-features = false, version = "55.0.0" }
|
datafusion = { default-features = false, version = "55.0.0" }
|
||||||
derive_builder = "0.20.2"
|
derive_builder = "0.20.2"
|
||||||
enumset = "1.1.14"
|
enumset = "1.1.14"
|
||||||
faster-hex = "0.10.0"
|
faster-hex = "0.10.0"
|
||||||
flate2 = "1.1.10"
|
flate2 = "1.1.9"
|
||||||
glob = "0.3.4"
|
glob = "0.3.4"
|
||||||
google-cloud-storage = "1.18.0"
|
google-cloud-storage = "1.17.0"
|
||||||
google-cloud-auth = "1.16.0"
|
google-cloud-auth = "1.15.0"
|
||||||
hashbrown = { version = "0.17.1" }
|
hashbrown = { version = "0.17.1" }
|
||||||
# Base32 for RFC 6238 TOTP shared secrets (RFC 4648 unpadded, the alphabet
|
hex = "0.4.3"
|
||||||
# every authenticator app expects). Already in the graph transitively.
|
|
||||||
data-encoding = "2.11.1"
|
|
||||||
hex-simd = "0.8.0"
|
hex-simd = "0.8.0"
|
||||||
highway = { version = "1.3.0" }
|
highway = { version = "1.3.0" }
|
||||||
hostname = "0.4.2"
|
hostname = "0.4.2"
|
||||||
@@ -290,36 +275,32 @@ mime_guess = "2.0.5"
|
|||||||
moka = { version = "0.12.16" }
|
moka = { version = "0.12.16" }
|
||||||
netif = "0.1.6"
|
netif = "0.1.6"
|
||||||
num_cpus = { version = "1.17.0" }
|
num_cpus = { version = "1.17.0" }
|
||||||
nvml-wrapper = "0.13.0"
|
nvml-wrapper = "0.12.1"
|
||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
path-absolutize = "4.0.1"
|
path-absolutize = "4.0.1"
|
||||||
percent-encoding = "2.3.2"
|
percent-encoding = "2.3.2"
|
||||||
# Server-side QR rendering for TOTP enrollment, so neither the console nor the
|
|
||||||
# CLI needs its own QR encoder. No default features: the image/render backends
|
|
||||||
# pull in an image stack this only needs SVG and text output from.
|
|
||||||
qrcode-rs = { version = "2.0.0", default-features = false, features = ["std", "svg"] }
|
|
||||||
pin-project-lite = "0.2.17"
|
pin-project-lite = "0.2.17"
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
rand = { version = "0.10.2" }
|
rand = { version = "0.10.2" }
|
||||||
ratelimit = "2.0.0"
|
ratelimit = "2.0.0"
|
||||||
rayon = "1.12.0"
|
rayon = "1.12.0"
|
||||||
rustfs-erasure-codec = { version = "8.0.2" }
|
reed-solomon-erasure = { package = "rustfs-erasure-codec", version = "8.0.2" }
|
||||||
reed-solomon-simd = "3.1.0"
|
reed-solomon-simd = "3.1.0"
|
||||||
regex = { version = "1.13.1" }
|
regex = { version = "1.13.1" }
|
||||||
rumqttc = { package = "rumqttc-next", version = "0.34.0" }
|
rumqttc = { package = "rumqttc-next", version = "0.34.0" }
|
||||||
redis = { version = "1.7.0" }
|
redis = { version = "1.6.0" }
|
||||||
rustify = { version = "0.7", default-features = false }
|
rustify = { version = "0.7", default-features = false }
|
||||||
rustix = { version = "1.1.4" }
|
rustix = { version = "1.1.4" }
|
||||||
rust-embed = { version = "8.12.0" }
|
rust-embed = { version = "8.12.0" }
|
||||||
rustc-hash = { version = "2.1.3" }
|
rustc-hash = { version = "2.1.3" }
|
||||||
s3s = { git = "https://github.com/rustfs/s3s.git", rev = "bdcb6259339c41369f9f1c60e3a42b5ab8da607b", version = "0.15.0", features = ["minio"] }
|
s3s = { git = "https://github.com/rustfs/s3s.git", rev = "e080e38c56a3b43acbacce55710d765a5ce9003d" }
|
||||||
serial_test = "4.0.1"
|
serial_test = "4.0.1"
|
||||||
shadow-rs = { default-features = false, version = "2.0.0" }
|
shadow-rs = { default-features = false, version = "2.0.0" }
|
||||||
siphasher = "1.0.3"
|
siphasher = "1.0.3"
|
||||||
smallvec = { version = "1.16.0" }
|
smallvec = { version = "1.15.2" }
|
||||||
compact_str = "0.10.0"
|
compact_str = "0.10.0"
|
||||||
snap = "1.1.2"
|
snap = "1.1.2"
|
||||||
starshard = { version = "2.3.0" }
|
starshard = { version = "2.2.2" }
|
||||||
strum = { version = "0.28.0" }
|
strum = { version = "0.28.0" }
|
||||||
sysinfo = "0.39.6"
|
sysinfo = "0.39.6"
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
@@ -335,7 +316,7 @@ tracing-subscriber = { version = "0.3.23" }
|
|||||||
transform-stream = "0.3.1"
|
transform-stream = "0.3.1"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = { version = "1.26.0" }
|
uuid = { version = "1.25.0" }
|
||||||
vaultrs = { version = "0.8.0" }
|
vaultrs = { version = "0.8.0" }
|
||||||
tar = "0.4.46"
|
tar = "0.4.46"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
@@ -344,12 +325,12 @@ windows = { version = "0.62.2" }
|
|||||||
windows-sys = "0.61.2"
|
windows-sys = "0.61.2"
|
||||||
xxhash-rust = { version = "0.8.18" }
|
xxhash-rust = { version = "0.8.18" }
|
||||||
zip = "8.6.0"
|
zip = "8.6.0"
|
||||||
zstd = "0.14.0"
|
zstd = "0.13.3"
|
||||||
|
|
||||||
# Observability and Metrics
|
# Observability and Metrics
|
||||||
metrics = "0.24.6"
|
metrics = "0.24.6"
|
||||||
metrics-util = "0.20"
|
metrics-util = "0.20"
|
||||||
dial9-tokio-telemetry = "0.5.0"
|
dial9-tokio-telemetry = "0.3"
|
||||||
opentelemetry = { version = "0.32.0" }
|
opentelemetry = { version = "0.32.0" }
|
||||||
opentelemetry-appender-tracing = { version = "0.32.0" }
|
opentelemetry-appender-tracing = { version = "0.32.0" }
|
||||||
opentelemetry-otlp = { version = "0.32.0" }
|
opentelemetry-otlp = { version = "0.32.0" }
|
||||||
@@ -362,18 +343,18 @@ pyroscope = { version = "2.1.1" }
|
|||||||
# FTP and SFTP
|
# FTP and SFTP
|
||||||
libunftp = { version = "0.23.0" }
|
libunftp = { version = "0.23.0" }
|
||||||
unftp-core = "0.1.0"
|
unftp-core = "0.1.0"
|
||||||
suppaftp = { version = "11.0.0" }
|
suppaftp = { version = "10.0.2" }
|
||||||
rcgen = { version = "0.14.10", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] }
|
rcgen = { version = "0.14.9", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] }
|
||||||
russh = { version = "0.63.2" }
|
russh = { version = "0.63.0" }
|
||||||
russh-sftp = "2.4.0"
|
russh-sftp = "2.4.0"
|
||||||
|
|
||||||
# WebDAV
|
# WebDAV
|
||||||
dav-server = "0.11.0"
|
dav-server = "0.11.0"
|
||||||
|
|
||||||
# Performance Analysis and Memory Profiling
|
# Performance Analysis and Memory Profiling
|
||||||
rustfs-mimalloc = { version = "0.5.3" }
|
rustfs-mimalloc = { version = "0.5.0" }
|
||||||
# Preserve Unicode focus filters until rustfs/backlog#2302 is resolved.
|
rustfs-mimalloc-sys = { version = "0.5.0" }
|
||||||
hotpath = { version = "=0.25.0", default-features = false }
|
hotpath = { version = "0.24.0", default-features = false }
|
||||||
# Snapshot testing for output format regression detection
|
# Snapshot testing for output format regression detection
|
||||||
insta = { version = "1.48" }
|
insta = { version = "1.48" }
|
||||||
|
|
||||||
@@ -392,8 +373,8 @@ opt-level = 3
|
|||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
debug = 0
|
debug = 0
|
||||||
strip = "symbols"
|
|
||||||
split-debuginfo = "off"
|
split-debuginfo = "off"
|
||||||
|
strip = "symbols"
|
||||||
|
|
||||||
[profile.production]
|
[profile.production]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ SHELL := $(shell which bash)
|
|||||||
.SHELLFLAGS = -eu -o pipefail -c
|
.SHELLFLAGS = -eu -o pipefail -c
|
||||||
|
|
||||||
DOCKER_CLI ?= docker
|
DOCKER_CLI ?= docker
|
||||||
# Python interpreter for the repository's helper scripts. They import tomllib
|
|
||||||
# (Python 3.11+), while macOS still ships /usr/bin/python3 at 3.9, so calls go
|
|
||||||
# through a resolver that picks a new-enough interpreter (or falls back to uv).
|
|
||||||
# Override with RUSTFS_PYTHON=/path/to/python3.12, or replace the resolver via
|
|
||||||
# RUSTFS_PYTHON_BIN=<command>.
|
|
||||||
RUSTFS_PYTHON_BIN ?= ./scripts/python_bin.sh
|
|
||||||
IMAGE_NAME ?= rustfs:v1.0.0
|
IMAGE_NAME ?= rustfs:v1.0.0
|
||||||
CONTAINER_NAME ?= rustfs-dev
|
CONTAINER_NAME ?= rustfs-dev
|
||||||
# Docker build configurations
|
# Docker build configurations
|
||||||
|
|||||||
@@ -12,11 +12,12 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/14181" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14181" alt="rustfs%2Frustfs | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/14181" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14181" alt="rustfs%2Frustfs | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
<a href="https://runacap.com/ross-index/q4-2025/" target="_blank" rel="noopener"><img style="width: 260px; height: 55px" src="https://runacap.com/wp-content/uploads/2026/01/ROSS_badge_white_Q4_2025.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q4 2025 | Runa Capital" height="55" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://docs.rustfs.com/en/installation">Getting Started</a>
|
<a href="https://docs.rustfs.com/installation/">Getting Started</a>
|
||||||
· <a href="https://docs.rustfs.com/">Docs</a>
|
· <a href="https://docs.rustfs.com/">Docs</a>
|
||||||
· <a href="https://github.com/rustfs/rustfs/issues">Bug reports</a>
|
· <a href="https://github.com/rustfs/rustfs/issues">Bug reports</a>
|
||||||
· <a href="https://github.com/rustfs/rustfs/discussions">Discussions</a>
|
· <a href="https://github.com/rustfs/rustfs/discussions">Discussions</a>
|
||||||
@@ -48,33 +49,16 @@ Unlike other storage systems, RustFS is released under the permissible Apache 2.
|
|||||||
- **Open Source**: Licensed under Apache 2.0, encouraging unrestricted community contributions and commercial usage.
|
- **Open Source**: Licensed under Apache 2.0, encouraging unrestricted community contributions and commercial usage.
|
||||||
- **User-Friendly**: Designed with simplicity in mind for easy deployment and management.
|
- **User-Friendly**: Designed with simplicity in mind for easy deployment and management.
|
||||||
|
|
||||||
Status legend: ✅ Available — shipped and covered by CI gates; 🧪 Preview — shipped behind an opt-in flag or with a bounded compatibility claim.
|
| Feature | Status | Feature | Status |
|
||||||
|
| :---------------------- | :----------- | :----------------------- | :--------------- |
|
||||||
| Feature | Status | Feature | Status |
|
| **S3 Core Features** | ✅ Available | **Bitrot Protection** | ✅ Available |
|
||||||
| :------------------------------- | :----------- | :--------------------------------- | :----------- |
|
| **Upload / Download** | ✅ Available | **Single Node Mode** | ✅ Available |
|
||||||
| **S3 Core Features** | ✅ Available | **Distributed Mode** | ✅ Available |
|
| **Versioning** | ✅ Available | **Bucket Replication** | ✅ Available |
|
||||||
| **Upload / Download** | ✅ Available | **Single Node Mode** | ✅ Available |
|
| **Logging** | ✅ Available | **Lifecycle Management** | 🚧 Under Testing |
|
||||||
| **Versioning** | ✅ Available | **Bitrot Protection** | ✅ Available |
|
| **Event Notifications** | ✅ Available | **Distributed Mode** | 🚧 Under Testing |
|
||||||
| **Object Lock (WORM)** | ✅ Available | **Healing & Scanner** | ✅ Available |
|
| **K8s Helm Charts** | ✅ Available | **RustFS KMS** | 🚧 Under Testing |
|
||||||
| **Server-Side Encryption** | ✅ Available | **Pool Expansion / Decommission** | ✅ Available |
|
| **Keystone Auth** | ✅ Available | **Multi-Tenancy** | ✅ Available |
|
||||||
| **RustFS KMS** | ✅ Available | **Bucket Replication** | ✅ Available |
|
| **Swift API** | ✅ Available | **Swift Metadata Ops** | 🚧 Partial |
|
||||||
| **Lifecycle Management (ILM)** | ✅ Available | **Site Replication** | ✅ Available |
|
|
||||||
| **ILM Tiering (Remote S3)** | ✅ Available | **Bucket Quota** | ✅ Available |
|
|
||||||
| **S3 Select** | ✅ Available | **Event Notifications** | ✅ Available |
|
|
||||||
| **S3 Tables (Iceberg REST)** | 🧪 Preview | **Audit Logging** | ✅ Available |
|
|
||||||
| **IAM / Policies** | ✅ Available | **Logging & Observability** | ✅ Available |
|
|
||||||
| **OIDC / SSO** | ✅ Available | **Web Console** | ✅ Available |
|
|
||||||
| **Keystone Auth** | ✅ Available | **K8s Helm Charts** | ✅ Available |
|
|
||||||
| **Swift API** | ✅ Available | **FTPS / WebDAV** | ✅ Available |
|
|
||||||
| **Multi-Tenancy** | ✅ Available | **SFTP** | ✅ Available |
|
|
||||||
| **MinIO On-Disk Compatibility** | 🧪 Preview | | |
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- **RustFS KMS**: Vault (KV2 / Transit) and AWS KMS backends are supported for production. The `Local` and `Static` backends are for development and testing only. See [KMS backend security properties](docs/operations/kms-backend-security.md).
|
|
||||||
- **Swift API / SFTP**: opt-in cargo features (`--features swift`, `--features sftp`, or `full`). FTPS and WebDAV are enabled in the default build.
|
|
||||||
- **S3 Tables**: ships as an Iceberg REST Catalog with automated PyIceberg and DuckDB coverage; other engines and vendor profiles carry bounded claims listed in the [S3 Tables support matrix](docs/architecture/s3-tables-support-matrix.md).
|
|
||||||
- **MinIO On-Disk Compatibility**: gated behind the `rio-v2` feature and not part of the default build. Objects MinIO encrypted are not readable by RustFS. See [MinIO file-format interoperability](docs/architecture/minio-file-format-compat.md).
|
|
||||||
|
|
||||||
## RustFS vs MinIO Performance
|
## RustFS vs MinIO Performance
|
||||||
|
|
||||||
@@ -109,15 +93,6 @@ Star RustFS on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Pool expansion notice:**
|
|
||||||
>
|
|
||||||
> - A single-node single-drive (SNSD) deployment is supported only as a standalone local path. It cannot expand in place or be added as a Pool. To move to a multi-drive topology, create a new deployment and migrate data through S3.
|
|
||||||
> - Keep an existing multi-drive Pool's endpoints and Erasure Set width unchanged; expand by appending a new Pool. With ellipsis-based expansion, every Pool argument must contain an ellipsis expression and expand to at least two drive endpoints.
|
|
||||||
> - Single-node multi-drive Pools and multi-node Pools with one drive per node are allowed, subject to valid Erasure Set geometry and EC settings; acceptance does not guarantee host-failure tolerance.
|
|
||||||
>
|
|
||||||
> These topology rules follow MinIO, but automatic parity selection differs between the projects. See the [Pool layout compatibility and regression tests](docs/testing/pool-layout-compatibility.md) before expanding a deployment.
|
|
||||||
|
|
||||||
To get started with RustFS, follow these steps:
|
To get started with RustFS, follow these steps:
|
||||||
|
|
||||||
### 1. One-click Installation (Option 1)
|
### 1. One-click Installation (Option 1)
|
||||||
@@ -141,7 +116,7 @@ chown -R 10001:10001 data logs
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# Using specific version
|
# Using specific version
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.5
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.3
|
||||||
```
|
```
|
||||||
|
|
||||||
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
||||||
@@ -271,26 +246,6 @@ nix build
|
|||||||
nix run
|
nix run
|
||||||
```
|
```
|
||||||
|
|
||||||
The flake also exports a NixOS module and the RustFS `rc` client. Add the
|
|
||||||
module to your system and provide credentials through runtime files (for
|
|
||||||
example, sops-nix or agenix) so secrets are never stored in the Nix store:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
imports = [ inputs.rustfs.nixosModules.rustfs ];
|
|
||||||
|
|
||||||
services.rustfs = {
|
|
||||||
enable = true;
|
|
||||||
accessKeyFile = "/run/secrets/rustfs-access-key";
|
|
||||||
secretKeyFile = "/run/secrets/rustfs-secret-key";
|
|
||||||
volumes = [ "/var/lib/rustfs" ];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Install the S3-compatible client with
|
|
||||||
`nix profile install github:rustfs/rustfs#rustfs-client` (the executable is named
|
|
||||||
`rc`), or use `inputs.rustfs.packages.${pkgs.system}.rustfs-client` in a system
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
### 6\. X-CMD (Option 6)
|
### 6\. X-CMD (Option 6)
|
||||||
|
|
||||||
If you are an [x-cmd](https://www.x-cmd.com/install/rustfs) user:
|
If you are an [x-cmd](https://www.x-cmd.com/install/rustfs) user:
|
||||||
|
|||||||
+4
-18
@@ -12,11 +12,12 @@
|
|||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/14181" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14181" alt="rustfs%2Frustfs | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/14181" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14181" alt="rustfs%2Frustfs | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
<a href="https://runacap.com/ross-index/q4-2025/" target="_blank" rel="noopener"><img style="width: 260px; height: 55px" src="https://runacap.com/wp-content/uploads/2026/01/ROSS_badge_white_Q4_2025.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q4 2025 | Runa Capital" height="55" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://docs.rustfs.com/zh/installation">快速开始</a>
|
<a href="https://docs.rustfs.com/installation/">快速开始</a>
|
||||||
· <a href="https://docs.rustfs.com/">文档</a>
|
· <a href="https://docs.rustfs.com/">文档</a>
|
||||||
· <a href="https://github.com/rustfs/rustfs/issues">报告 Bug</a>
|
· <a href="https://github.com/rustfs/rustfs/issues">报告 Bug</a>
|
||||||
· <a href="https://github.com/rustfs/rustfs/discussions">社区讨论</a>
|
· <a href="https://github.com/rustfs/rustfs/discussions">社区讨论</a>
|
||||||
@@ -89,15 +90,6 @@ RustFS 是一个基于 Rust 构建的高性能分布式对象存储系统。Rust
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Pool 扩容 Notice:**
|
|
||||||
>
|
|
||||||
> - 单节点单盘(SNSD)部署仅支持使用本地路径独立运行,不支持原地扩容,也不能作为 Pool 加入集群。如需改为多盘拓扑,请创建新部署并通过 S3 迁移数据。
|
|
||||||
> - 已有多盘 Pool 的端点和 Erasure Set 宽度应保持不变,扩容应追加新的 Pool。使用省略号表达式扩容时,每个 Pool 参数都必须包含省略号表达式,并展开为至少两个磁盘端点。
|
|
||||||
> - 允许单节点多盘 Pool,也允许多节点、每节点一盘的 Pool,但必须满足 Erasure Set 布局和 EC 配置要求;配置合法不代表能够容忍整台主机故障。
|
|
||||||
>
|
|
||||||
> 这些拓扑规则与 MinIO 一致,但两者的默认 parity 选择方式存在差异。扩容前请阅读 [Pool 布局兼容性与回归测试说明](docs/testing/pool-layout-compatibility.md)。
|
|
||||||
|
|
||||||
请按照以下步骤快速上手 RustFS:
|
请按照以下步骤快速上手 RustFS:
|
||||||
|
|
||||||
### 1. 一键安装脚本 (选项 1)
|
### 1. 一键安装脚本 (选项 1)
|
||||||
@@ -121,7 +113,7 @@ chown -R 10001:10001 data logs
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# 使用指定版本运行
|
# 使用指定版本运行
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.5
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-rc.3
|
||||||
```
|
```
|
||||||
|
|
||||||
如果您通过绑定挂载启用 TLS 证书目录,也请用同样方式准备该目录:
|
如果您通过绑定挂载启用 TLS 证书目录,也请用同样方式准备该目录:
|
||||||
@@ -200,12 +192,6 @@ nix build
|
|||||||
nix run
|
nix run
|
||||||
```
|
```
|
||||||
|
|
||||||
该 Flake 同时提供 NixOS 模块和 RustFS `rc` 客户端。将
|
|
||||||
`inputs.rustfs.nixosModules.rustfs` 加入 `imports`,并通过运行时密钥文件
|
|
||||||
(例如 sops-nix 或 agenix)配置 `accessKeyFile` 与 `secretKeyFile`,避免密钥
|
|
||||||
进入 Nix store。客户端包为
|
|
||||||
`inputs.rustfs.packages.${pkgs.system}.rustfs-client`,安装后的命令名为 `rc`。
|
|
||||||
|
|
||||||
### 6\. X-CMD (Option 6)
|
### 6\. X-CMD (Option 6)
|
||||||
|
|
||||||
如果你是 [x-cmd](https://www.x-cmd.com/install/rustfs) 用户:
|
如果你是 [x-cmd](https://www.x-cmd.com/install/rustfs) 用户:
|
||||||
|
|||||||
+2
-5
@@ -33,11 +33,8 @@ Applies to all paths under `crates/`.
|
|||||||
|
|
||||||
## Type Casting
|
## Type Casting
|
||||||
|
|
||||||
- Never use `as` for numeric conversions that may truncate or overflow. Use
|
- Never use `as` for numeric conversions that may truncate or overflow. Use `try_into()` with explicit error handling, or clamp with `value.max(0) as usize` when the domain is bounded.
|
||||||
`try_into()` with typed error handling; clamp or saturate only when the domain
|
- `f64 as usize` saturates but is fragile; clamp to `[0, usize::MAX as f64]` first.
|
||||||
explicitly requires it.
|
|
||||||
- Before converting floating-point input to an integer, validate finiteness,
|
|
||||||
sign, and the destination range. A lower-bound clamp alone is insufficient.
|
|
||||||
- Treat every `as` cast in a PR review as a potential bug; require justification.
|
- Treat every `as` cast in a PR review as a potential bug; require justification.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "time",
|
|||||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rustfs-targets = { workspace = true, features = ["test-support"] }
|
async-trait = { workspace = true }
|
||||||
temp-env = { workspace = true }
|
temp-env = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -564,21 +564,88 @@ impl AuditRuntimeFacade {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::AuditPipeline;
|
use super::AuditPipeline;
|
||||||
use crate::{AuditEntry, AuditError, AuditRegistry};
|
use crate::{AuditEntry, AuditError, AuditRegistry};
|
||||||
use rustfs_targets::testkit::MockTarget;
|
use async_trait::async_trait;
|
||||||
|
use rustfs_targets::arn::TargetID;
|
||||||
|
use rustfs_targets::store::{Key, Store};
|
||||||
|
use rustfs_targets::target::{EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||||
|
use rustfs_targets::{StoreError, Target, TargetError};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::{Mutex, Notify};
|
||||||
|
|
||||||
/// Builds a mock target whose `save()` outcome is fixed at construction so tests can force
|
/// Mock target whose `save()` outcome is fixed at construction so tests can
|
||||||
/// full-success / full-failure / partial-failure fan-outs.
|
/// force full-success / full-failure / partial-failure fan-outs.
|
||||||
fn mock_target(id: &str, fail: bool) -> MockTarget {
|
#[derive(Clone)]
|
||||||
let target = MockTarget::new(id, "webhook");
|
struct MockTarget {
|
||||||
if fail { target.with_save_failures(usize::MAX) } else { target }
|
id: TargetID,
|
||||||
|
fail: bool,
|
||||||
|
health_gate: Option<(Arc<Notify>, Arc<Notify>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockTarget {
|
||||||
|
fn new(id: &str, fail: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
id: TargetID::new(id.to_string(), "webhook".to_string()),
|
||||||
|
fail,
|
||||||
|
health_gate: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_health_gate(mut self, started: Arc<Notify>, release: Arc<Notify>) -> Self {
|
||||||
|
self.health_gate = Some((started, release));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for MockTarget
|
||||||
|
where
|
||||||
|
E: rustfs_targets::PluginEvent,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
if let Some((started, release)) = &self.health_gate {
|
||||||
|
started.notify_one();
|
||||||
|
release.notified().await;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
if self.fail {
|
||||||
|
Err(TargetError::Configuration("forced save failure".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipeline_with(targets: Vec<MockTarget>) -> AuditPipeline {
|
fn pipeline_with(targets: Vec<MockTarget>) -> AuditPipeline {
|
||||||
let mut registry = AuditRegistry::new();
|
let mut registry = AuditRegistry::new();
|
||||||
for target in targets {
|
for target in targets {
|
||||||
registry.add_target(target.target_id().to_string(), Box::new(target));
|
registry.add_target(target.id.to_string(), Box::new(target));
|
||||||
}
|
}
|
||||||
AuditPipeline::new(Arc::new(Mutex::new(registry)))
|
AuditPipeline::new(Arc::new(Mutex::new(registry)))
|
||||||
}
|
}
|
||||||
@@ -591,7 +658,7 @@ mod tests {
|
|||||||
// dispatch must return Err rather than swallowing the failures as Ok.
|
// dispatch must return Err rather than swallowing the failures as Ok.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatch_returns_err_when_all_targets_fail() {
|
async fn dispatch_returns_err_when_all_targets_fail() {
|
||||||
let pipeline = pipeline_with(vec![mock_target("a:webhook", true), mock_target("b:webhook", true)]);
|
let pipeline = pipeline_with(vec![MockTarget::new("a:webhook", true), MockTarget::new("b:webhook", true)]);
|
||||||
let result = pipeline.dispatch(entry()).await;
|
let result = pipeline.dispatch(entry()).await;
|
||||||
assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
|
assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
|
||||||
}
|
}
|
||||||
@@ -600,13 +667,13 @@ mod tests {
|
|||||||
// so dispatch reports success (degradation is logged, not propagated).
|
// so dispatch reports success (degradation is logged, not propagated).
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatch_returns_ok_on_partial_failure() {
|
async fn dispatch_returns_ok_on_partial_failure() {
|
||||||
let pipeline = pipeline_with(vec![mock_target("ok:webhook", false), mock_target("bad:webhook", true)]);
|
let pipeline = pipeline_with(vec![MockTarget::new("ok:webhook", false), MockTarget::new("bad:webhook", true)]);
|
||||||
pipeline.dispatch(entry()).await.expect("partial success should return Ok");
|
pipeline.dispatch(entry()).await.expect("partial success should return Ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatch_returns_ok_when_all_targets_succeed() {
|
async fn dispatch_returns_ok_when_all_targets_succeed() {
|
||||||
let pipeline = pipeline_with(vec![mock_target("a:webhook", false), mock_target("b:webhook", false)]);
|
let pipeline = pipeline_with(vec![MockTarget::new("a:webhook", false), MockTarget::new("b:webhook", false)]);
|
||||||
pipeline.dispatch(entry()).await.expect("all-success should return Ok");
|
pipeline.dispatch(entry()).await.expect("all-success should return Ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,10 +686,9 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn health_probe_does_not_hold_the_registry_lock() {
|
async fn health_probe_does_not_hold_the_registry_lock() {
|
||||||
|
let started = Arc::new(Notify::new());
|
||||||
let release = Arc::new(Notify::new());
|
let release = Arc::new(Notify::new());
|
||||||
let target = mock_target("blocked", false).with_health_gate(release.clone());
|
let pipeline = pipeline_with(vec![MockTarget::new("blocked", false).with_health_gate(started.clone(), release.clone())]);
|
||||||
let started = target.health_started();
|
|
||||||
let pipeline = pipeline_with(vec![target]);
|
|
||||||
let registry = Arc::clone(&pipeline.registry);
|
let registry = Arc::clone(&pipeline.registry);
|
||||||
let snapshot_task = tokio::spawn(async move { pipeline.snapshot_target_health().await });
|
let snapshot_task = tokio::spawn(async move { pipeline.snapshot_target_health().await });
|
||||||
started.notified().await;
|
started.notified().await;
|
||||||
@@ -640,14 +706,14 @@ mod tests {
|
|||||||
// whole-batch loss instead of returning Ok.
|
// whole-batch loss instead of returning Ok.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatch_batch_returns_err_when_all_targets_fail() {
|
async fn dispatch_batch_returns_err_when_all_targets_fail() {
|
||||||
let pipeline = pipeline_with(vec![mock_target("a:webhook", true)]);
|
let pipeline = pipeline_with(vec![MockTarget::new("a:webhook", true)]);
|
||||||
let result = pipeline.dispatch_batch(vec![entry(), entry()]).await;
|
let result = pipeline.dispatch_batch(vec![entry(), entry()]).await;
|
||||||
assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
|
assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatch_batch_returns_ok_when_all_targets_succeed() {
|
async fn dispatch_batch_returns_ok_when_all_targets_succeed() {
|
||||||
let pipeline = pipeline_with(vec![mock_target("a:webhook", false), mock_target("b:webhook", false)]);
|
let pipeline = pipeline_with(vec![MockTarget::new("a:webhook", false), MockTarget::new("b:webhook", false)]);
|
||||||
pipeline
|
pipeline
|
||||||
.dispatch_batch(vec![entry(), entry()])
|
.dispatch_batch(vec![entry(), entry()])
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -286,10 +286,70 @@ impl AuditRegistry {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::AuditRegistry;
|
use super::AuditRegistry;
|
||||||
use crate::AuditError;
|
use crate::{AuditEntry, AuditError};
|
||||||
use rustfs_targets::TargetError;
|
use rustfs_targets::arn::TargetID;
|
||||||
use rustfs_targets::target::ChannelTargetType;
|
use rustfs_targets::store::{Key, Store};
|
||||||
use rustfs_targets::testkit::MockTarget;
|
use rustfs_targets::target::{ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||||
|
use rustfs_targets::{StoreError, Target, TargetError};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CloseTestTarget {
|
||||||
|
id: TargetID,
|
||||||
|
close_calls: Arc<AtomicUsize>,
|
||||||
|
fail_on_close: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloseTestTarget {
|
||||||
|
fn new(id: TargetID, close_calls: Arc<AtomicUsize>, fail_on_close: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
close_calls,
|
||||||
|
fail_on_close,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Target<AuditEntry> for CloseTestTarget {
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, _event: Arc<EntityTarget<AuditEntry>>) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
if self.fail_on_close {
|
||||||
|
Err(TargetError::Unknown("close failed".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<AuditEntry> + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_registers_amqp_factory() {
|
fn registry_registers_amqp_factory() {
|
||||||
@@ -301,21 +361,23 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn close_all_returns_first_error_and_clears_targets() {
|
async fn close_all_returns_first_error_and_clears_targets() {
|
||||||
let mut registry = AuditRegistry::new();
|
let mut registry = AuditRegistry::new();
|
||||||
let ok = MockTarget::new("ok", "webhook");
|
let ok_calls = Arc::new(AtomicUsize::new(0));
|
||||||
let ok_observer = ok.clone();
|
let fail_calls = Arc::new(AtomicUsize::new(0));
|
||||||
let fail = MockTarget::new("fail", "webhook")
|
|
||||||
.with_close_failures(usize::MAX)
|
|
||||||
.with_close_failure_error(|| TargetError::Unknown("close failed".to_string()));
|
|
||||||
let fail_observer = fail.clone();
|
|
||||||
|
|
||||||
registry.add_target(ok.target_id().to_string(), Box::new(ok));
|
let ok_id = TargetID::new("ok".to_string(), "webhook".to_string());
|
||||||
registry.add_target(fail.target_id().to_string(), Box::new(fail));
|
let fail_id = TargetID::new("fail".to_string(), "webhook".to_string());
|
||||||
|
|
||||||
|
registry.add_target(ok_id.to_string(), Box::new(CloseTestTarget::new(ok_id, Arc::clone(&ok_calls), false)));
|
||||||
|
registry.add_target(
|
||||||
|
fail_id.to_string(),
|
||||||
|
Box::new(CloseTestTarget::new(fail_id, Arc::clone(&fail_calls), true)),
|
||||||
|
);
|
||||||
|
|
||||||
let result = registry.close_all().await;
|
let result = registry.close_all().await;
|
||||||
|
|
||||||
assert!(matches!(result, Err(AuditError::Target(TargetError::Unknown(_)))));
|
assert!(matches!(result, Err(AuditError::Target(TargetError::Unknown(_)))));
|
||||||
assert_eq!(ok_observer.close_call_count(), 1);
|
assert_eq!(ok_calls.load(Ordering::SeqCst), 1);
|
||||||
assert_eq!(fail_observer.close_call_count(), 1);
|
assert_eq!(fail_calls.load(Ordering::SeqCst), 1);
|
||||||
assert!(registry.list_targets().is_empty());
|
assert!(registry.list_targets().is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-11
@@ -577,17 +577,76 @@ fn warn_audit_state(state: &str, reason: Option<&str>) {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{AuditSystem, AuditSystemState};
|
use super::{AuditSystem, AuditSystemState};
|
||||||
use crate::{AuditEntry, AuditError};
|
use crate::{AuditEntry, AuditError};
|
||||||
|
use async_trait::async_trait;
|
||||||
use rustfs_targets::ReplayWorkerManager;
|
use rustfs_targets::ReplayWorkerManager;
|
||||||
use rustfs_targets::testkit::MockTarget;
|
use rustfs_targets::arn::TargetID;
|
||||||
|
use rustfs_targets::store::{Key, Store};
|
||||||
|
use rustfs_targets::target::{EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||||
|
use rustfs_targets::{StoreError, Target, TargetError};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TestTarget {
|
||||||
|
close_calls: Arc<AtomicUsize>,
|
||||||
|
id: TargetID,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestTarget {
|
||||||
|
fn new(id: &str, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
close_calls: Arc::new(AtomicUsize::new(0)),
|
||||||
|
id: TargetID::new(id.to_string(), name.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for TestTarget
|
||||||
|
where
|
||||||
|
E: rustfs_targets::PluginEvent,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn reload_with_empty_config_stops_existing_runtime() {
|
async fn reload_with_empty_config_stops_existing_runtime() {
|
||||||
let system = AuditSystem::new();
|
let system = AuditSystem::new();
|
||||||
let target = MockTarget::new("primary", "webhook");
|
let target = TestTarget::new("primary", "webhook");
|
||||||
let observer = target.clone();
|
let close_calls = Arc::clone(&target.close_calls);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut registry = system.registry.lock().await;
|
let mut registry = system.registry.lock().await;
|
||||||
@@ -612,7 +671,7 @@ mod tests {
|
|||||||
assert_eq!(system.get_state().await, AuditSystemState::Stopped);
|
assert_eq!(system.get_state().await, AuditSystemState::Stopped);
|
||||||
assert!(system.list_targets().await.is_empty());
|
assert!(system.list_targets().await.is_empty());
|
||||||
assert_eq!(system.runtime_status_snapshot().await, ReplayWorkerManager::new().snapshot(0));
|
assert_eq!(system.runtime_status_snapshot().await, ReplayWorkerManager::new().snapshot(0));
|
||||||
assert_eq!(observer.close_call_count(), 1);
|
assert_eq!(close_calls.load(Ordering::SeqCst), 1);
|
||||||
assert_eq!(*system.config.read().await, Some(rustfs_config::server_config::Config(HashMap::new())));
|
assert_eq!(*system.config.read().await, Some(rustfs_config::server_config::Config(HashMap::new())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +693,7 @@ mod tests {
|
|||||||
// Seed a target + replay worker so both critical sections touch real state.
|
// Seed a target + replay worker so both critical sections touch real state.
|
||||||
{
|
{
|
||||||
let mut registry = system.registry.lock().await;
|
let mut registry = system.registry.lock().await;
|
||||||
registry.add_target("primary:webhook".to_string(), Box::new(MockTarget::new("primary", "webhook")));
|
registry.add_target("primary:webhook".to_string(), Box::new(TestTarget::new("primary", "webhook")));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut replay_workers = system.stream_cancellers.write().await;
|
let mut replay_workers = system.stream_cancellers.write().await;
|
||||||
@@ -734,8 +793,8 @@ mod tests {
|
|||||||
async fn commit_closes_old_targets_before_installing_new() {
|
async fn commit_closes_old_targets_before_installing_new() {
|
||||||
let system = AuditSystem::new();
|
let system = AuditSystem::new();
|
||||||
|
|
||||||
let old = MockTarget::new("old", "webhook");
|
let old = TestTarget::new("old", "webhook");
|
||||||
let old_observer = old.clone();
|
let old_close = Arc::clone(&old.close_calls);
|
||||||
{
|
{
|
||||||
let mut registry = system.registry.lock().await;
|
let mut registry = system.registry.lock().await;
|
||||||
registry.add_target("old:webhook".to_string(), Box::new(old));
|
registry.add_target("old:webhook".to_string(), Box::new(old));
|
||||||
@@ -750,17 +809,17 @@ mod tests {
|
|||||||
*state = AuditSystemState::Running;
|
*state = AuditSystemState::Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new = MockTarget::new("new", "webhook");
|
let new = TestTarget::new("new", "webhook");
|
||||||
let new_observer = new.clone();
|
let new_close = Arc::clone(&new.close_calls);
|
||||||
system
|
system
|
||||||
.commit_runtime_targets(vec![Box::new(new)], AuditSystemState::Running)
|
.commit_runtime_targets(vec![Box::new(new)], AuditSystemState::Running)
|
||||||
.await
|
.await
|
||||||
.expect("commit should succeed");
|
.expect("commit should succeed");
|
||||||
|
|
||||||
// Old target closed exactly once during the pre-install shutdown.
|
// Old target closed exactly once during the pre-install shutdown.
|
||||||
assert_eq!(old_observer.close_call_count(), 1);
|
assert_eq!(old_close.load(Ordering::SeqCst), 1);
|
||||||
// New target installed and left open.
|
// New target installed and left open.
|
||||||
assert_eq!(new_observer.close_call_count(), 0);
|
assert_eq!(new_close.load(Ordering::SeqCst), 0);
|
||||||
assert_eq!(system.list_targets().await, vec!["new:webhook".to_string()]);
|
assert_eq!(system.list_targets().await, vec!["new:webhook".to_string()]);
|
||||||
// Old replay worker stopped; the store-less new target adds none.
|
// Old replay worker stopped; the store-less new target adds none.
|
||||||
assert_eq!(system.runtime_status_snapshot().await.replay_worker_count, 0);
|
assert_eq!(system.runtime_status_snapshot().await.replay_worker_count, 0);
|
||||||
|
|||||||
@@ -12,16 +12,136 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use rustfs_audit::{AuditEntry, AuditError, AuditPipeline, AuditRegistry, AuditRuntimeFacade, AuditRuntimeView};
|
use rustfs_audit::{AuditEntry, AuditError, AuditPipeline, AuditRegistry, AuditRuntimeFacade, AuditRuntimeView};
|
||||||
use rustfs_targets::SharedTarget;
|
use rustfs_targets::arn::TargetID;
|
||||||
use rustfs_targets::testkit::MockTarget;
|
use rustfs_targets::store::{Key, Store};
|
||||||
|
use rustfs_targets::target::{EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||||
|
use rustfs_targets::{SharedTarget, StoreError, Target, TargetError};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
/// Builds a target whose `save()` always fails, used to exercise the dispatch
|
#[derive(Clone)]
|
||||||
|
struct TestTarget {
|
||||||
|
close_calls: Arc<AtomicUsize>,
|
||||||
|
id: TargetID,
|
||||||
|
init_calls: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestTarget {
|
||||||
|
fn new(id: &str, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
close_calls: Arc::new(AtomicUsize::new(0)),
|
||||||
|
id: TargetID::new(id.to_string(), name.to_string()),
|
||||||
|
init_calls: Arc::new(AtomicUsize::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for TestTarget
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> Result<(), TargetError> {
|
||||||
|
self.init_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A target whose `save()` always fails, used to exercise the dispatch
|
||||||
/// failure-propagation paths.
|
/// failure-propagation paths.
|
||||||
fn failing_target(id: &str, name: &str) -> MockTarget {
|
#[derive(Clone)]
|
||||||
MockTarget::new(id, name).with_save_failures(usize::MAX)
|
struct FailingTarget {
|
||||||
|
id: TargetID,
|
||||||
|
save_calls: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FailingTarget {
|
||||||
|
fn new(id: &str, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id: TargetID::new(id.to_string(), name.to_string()),
|
||||||
|
save_calls: Arc::new(AtomicUsize::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for FailingTarget
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
self.save_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Err(TargetError::Storage("disk full".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> Result<(), TargetError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipeline_with_targets(targets: Vec<(&str, SharedTarget<AuditEntry>)>) -> AuditPipeline {
|
fn pipeline_with_targets(targets: Vec<(&str, SharedTarget<AuditEntry>)>) -> AuditPipeline {
|
||||||
@@ -34,8 +154,8 @@ fn pipeline_with_targets(targets: Vec<(&str, SharedTarget<AuditEntry>)>) -> Audi
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn audit_pipeline_dispatch_propagates_total_failure() {
|
async fn audit_pipeline_dispatch_propagates_total_failure() {
|
||||||
let failing = failing_target("primary", "webhook");
|
let failing = FailingTarget::new("primary", "webhook");
|
||||||
let observer = failing.clone();
|
let save_calls = Arc::clone(&failing.save_calls);
|
||||||
let pipeline = pipeline_with_targets(vec![("primary:webhook", Arc::new(failing))]);
|
let pipeline = pipeline_with_targets(vec![("primary:webhook", Arc::new(failing))]);
|
||||||
|
|
||||||
let result = pipeline.dispatch(Arc::new(AuditEntry::default())).await;
|
let result = pipeline.dispatch(Arc::new(AuditEntry::default())).await;
|
||||||
@@ -44,13 +164,13 @@ async fn audit_pipeline_dispatch_propagates_total_failure() {
|
|||||||
matches!(result, Err(AuditError::Target(_))),
|
matches!(result, Err(AuditError::Target(_))),
|
||||||
"dispatch must surface an error when every target fails, got {result:?}"
|
"dispatch must surface an error when every target fails, got {result:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(observer.save_call_count(), 1, "the failing target should have been invoked");
|
assert_eq!(save_calls.load(Ordering::SeqCst), 1, "the failing target should have been invoked");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn audit_pipeline_dispatch_tolerates_partial_failure() {
|
async fn audit_pipeline_dispatch_tolerates_partial_failure() {
|
||||||
let failing = failing_target("primary", "webhook");
|
let failing = FailingTarget::new("primary", "webhook");
|
||||||
let healthy = MockTarget::new("secondary", "webhook");
|
let healthy = TestTarget::new("secondary", "webhook");
|
||||||
let pipeline = pipeline_with_targets(vec![
|
let pipeline = pipeline_with_targets(vec![
|
||||||
("primary:webhook", Arc::new(failing)),
|
("primary:webhook", Arc::new(failing)),
|
||||||
("secondary:webhook", Arc::new(healthy)),
|
("secondary:webhook", Arc::new(healthy)),
|
||||||
@@ -66,7 +186,7 @@ async fn audit_pipeline_dispatch_tolerates_partial_failure() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn audit_pipeline_dispatch_batch_propagates_total_failure() {
|
async fn audit_pipeline_dispatch_batch_propagates_total_failure() {
|
||||||
let failing = failing_target("primary", "webhook");
|
let failing = FailingTarget::new("primary", "webhook");
|
||||||
let pipeline = pipeline_with_targets(vec![("primary:webhook", Arc::new(failing))]);
|
let pipeline = pipeline_with_targets(vec![("primary:webhook", Arc::new(failing))]);
|
||||||
|
|
||||||
let entries = vec![Arc::new(AuditEntry::default()), Arc::new(AuditEntry::default())];
|
let entries = vec![Arc::new(AuditEntry::default()), Arc::new(AuditEntry::default())];
|
||||||
@@ -80,8 +200,8 @@ async fn audit_pipeline_dispatch_batch_propagates_total_failure() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn audit_pipeline_dispatch_batch_tolerates_partial_failure() {
|
async fn audit_pipeline_dispatch_batch_tolerates_partial_failure() {
|
||||||
let failing = failing_target("primary", "webhook");
|
let failing = FailingTarget::new("primary", "webhook");
|
||||||
let healthy = MockTarget::new("secondary", "webhook");
|
let healthy = TestTarget::new("secondary", "webhook");
|
||||||
let pipeline = pipeline_with_targets(vec![
|
let pipeline = pipeline_with_targets(vec![
|
||||||
("primary:webhook", Arc::new(failing)),
|
("primary:webhook", Arc::new(failing)),
|
||||||
("secondary:webhook", Arc::new(healthy)),
|
("secondary:webhook", Arc::new(healthy)),
|
||||||
@@ -146,8 +266,9 @@ async fn audit_runtime_facade_activates_empty_target_list() {
|
|||||||
async fn audit_runtime_view_upsert_and_remove_target() {
|
async fn audit_runtime_view_upsert_and_remove_target() {
|
||||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||||
let runtime_view = AuditRuntimeView::new(registry.clone());
|
let runtime_view = AuditRuntimeView::new(registry.clone());
|
||||||
let target = MockTarget::new("primary", "webhook");
|
let target = TestTarget::new("primary", "webhook");
|
||||||
let observer = target.clone();
|
let init_calls = Arc::clone(&target.init_calls);
|
||||||
|
let close_calls = Arc::clone(&target.close_calls);
|
||||||
|
|
||||||
runtime_view
|
runtime_view
|
||||||
.upsert_target("primary:webhook".to_string(), Box::new(target))
|
.upsert_target("primary:webhook".to_string(), Box::new(target))
|
||||||
@@ -155,7 +276,7 @@ async fn audit_runtime_view_upsert_and_remove_target() {
|
|||||||
.expect("upsert should succeed");
|
.expect("upsert should succeed");
|
||||||
|
|
||||||
assert_eq!(runtime_view.list_targets().await, vec!["primary:webhook".to_string()]);
|
assert_eq!(runtime_view.list_targets().await, vec!["primary:webhook".to_string()]);
|
||||||
assert_eq!(observer.init_call_count(), 1);
|
assert_eq!(init_calls.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
runtime_view
|
runtime_view
|
||||||
.remove_target("primary:webhook")
|
.remove_target("primary:webhook")
|
||||||
@@ -163,7 +284,7 @@ async fn audit_runtime_view_upsert_and_remove_target() {
|
|||||||
.expect("remove should succeed");
|
.expect("remove should succeed");
|
||||||
|
|
||||||
assert!(runtime_view.list_targets().await.is_empty());
|
assert!(runtime_view.list_targets().await.is_empty());
|
||||||
assert_eq!(observer.close_call_count(), 1);
|
assert_eq!(close_calls.load(Ordering::SeqCst), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -171,7 +292,7 @@ async fn audit_runtime_facade_replace_targets_commits_runtime_state() {
|
|||||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||||
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
||||||
let facade = AuditRuntimeFacade::new(registry.clone(), replay_workers.clone());
|
let facade = AuditRuntimeFacade::new(registry.clone(), replay_workers.clone());
|
||||||
let target = MockTarget::new("primary", "webhook");
|
let target = TestTarget::new("primary", "webhook");
|
||||||
let activation = rustfs_targets::RuntimeActivation {
|
let activation = rustfs_targets::RuntimeActivation {
|
||||||
replay_workers: rustfs_targets::ReplayWorkerManager::new(),
|
replay_workers: rustfs_targets::ReplayWorkerManager::new(),
|
||||||
targets: vec![Arc::new(target) as rustfs_targets::SharedTarget<rustfs_audit::AuditEntry>],
|
targets: vec![Arc::new(target) as rustfs_targets::SharedTarget<rustfs_audit::AuditEntry>],
|
||||||
|
|||||||
+8
-165
@@ -41,22 +41,14 @@ pub const XXHASH_64_NAME: &str = "xxhash64";
|
|||||||
pub const XXHASH_128_NAME: &str = "xxhash128";
|
pub const XXHASH_128_NAME: &str = "xxhash128";
|
||||||
pub const MD5_NAME: &str = "md5";
|
pub const MD5_NAME: &str = "md5";
|
||||||
|
|
||||||
/// The canonical checksum-algorithm registry (backlog#1833, backlog#1844):
|
/// One of three deliberately separate checksum registries (backlog#1833):
|
||||||
/// this enum owns the streaming-hash implementations and, via the exhaustive
|
/// this enum owns the **streaming-hash algorithm registry**, including the
|
||||||
/// per-algorithm metadata methods below, the wire names, header names, digest
|
/// RustFS extensions (sha512, xxhash3/64/128). The on-disk xl.meta bitset
|
||||||
/// lengths, and checksum-type capabilities — including the RustFS extensions
|
/// lives in `rustfs_rio::ChecksumType` (crates/rio/src/checksum.rs, varint
|
||||||
/// (sha512, xxhash3/64/128). The MinIO-port client's `ChecksumMode`
|
/// bits are append-only), and the MinIO-port client keeps its own
|
||||||
/// (crates/s3-client/src/checksum.rs) delegates all per-algorithm dispatch
|
/// `ChecksumMode` (crates/ecstore/src/client/checksum.rs). When adding an
|
||||||
/// here through its `algorithm()` bridge. The on-disk xl.meta bitset remains
|
/// algorithm, extend all three (or record why not) — they do not derive from
|
||||||
/// deliberately separate in `rustfs_rio::ChecksumType`
|
/// each other.
|
||||||
/// (crates/rio/src/checksum.rs, varint bits are append-only), and rio also
|
|
||||||
/// keeps its own hot-path hasher shells — equivalence with this crate's
|
|
||||||
/// hashers is enforced by both test suites pinning the same official
|
|
||||||
/// known-answer vectors (backlog#1844 PR3 verdict, recorded on
|
|
||||||
/// `rustfs_rio::ChecksumType`). When adding an algorithm: add the variant
|
|
||||||
/// here (the exhaustive matches force every metadata decision), bridge it in
|
|
||||||
/// the client, and allocate an xl.meta bit + hasher + shared vector in rio
|
|
||||||
/// (or record why not).
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ChecksumAlgorithm {
|
pub enum ChecksumAlgorithm {
|
||||||
@@ -128,84 +120,6 @@ impl ChecksumAlgorithm {
|
|||||||
Self::Xxhash128 => XXHASH_128_NAME,
|
Self::Xxhash128 => XXHASH_128_NAME,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-algorithm wire metadata. These matches are deliberately exhaustive
|
|
||||||
// (no `_` arm): adding a ChecksumAlgorithm variant without deciding its
|
|
||||||
// name, header, digest length, and checksum-type support must fail to
|
|
||||||
// compile rather than silently inherit a default (backlog#1844).
|
|
||||||
|
|
||||||
/// The canonical `x-amz-checksum-algorithm` wire value (uppercase), as
|
|
||||||
/// carried in S3 requests/responses and stored checksum maps.
|
|
||||||
pub fn s3_algorithm_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Crc32 => "CRC32",
|
|
||||||
Self::Crc32c => "CRC32C",
|
|
||||||
Self::Crc64Nvme => "CRC64NVME",
|
|
||||||
Self::Sha1 => "SHA1",
|
|
||||||
Self::Sha256 => "SHA256",
|
|
||||||
Self::Sha512 => "SHA512",
|
|
||||||
Self::Xxhash3 => "XXHASH3",
|
|
||||||
Self::Xxhash64 => "XXHASH64",
|
|
||||||
Self::Xxhash128 => "XXHASH128",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `x-amz-checksum-*` HTTP header that carries this algorithm's
|
|
||||||
/// base64-encoded digest.
|
|
||||||
pub fn http_header_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Crc32 => http::CRC_32_HEADER_NAME,
|
|
||||||
Self::Crc32c => http::CRC_32_C_HEADER_NAME,
|
|
||||||
Self::Crc64Nvme => http::CRC_64_NVME_HEADER_NAME,
|
|
||||||
Self::Sha1 => http::SHA_1_HEADER_NAME,
|
|
||||||
Self::Sha256 => http::SHA_256_HEADER_NAME,
|
|
||||||
Self::Sha512 => http::SHA_512_HEADER_NAME,
|
|
||||||
Self::Xxhash3 => http::XXHASH_3_HEADER_NAME,
|
|
||||||
Self::Xxhash64 => http::XXHASH_64_HEADER_NAME,
|
|
||||||
Self::Xxhash128 => http::XXHASH_128_HEADER_NAME,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Raw (unencoded) digest length in bytes.
|
|
||||||
pub fn raw_len(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Crc32 | Self::Crc32c => 4,
|
|
||||||
Self::Crc64Nvme => 8,
|
|
||||||
Self::Sha1 => 20,
|
|
||||||
Self::Sha256 => 32,
|
|
||||||
Self::Sha512 => 64,
|
|
||||||
Self::Xxhash3 | Self::Xxhash64 => 8,
|
|
||||||
Self::Xxhash128 => 16,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the algorithm supports the S3 COMPOSITE multipart checksum
|
|
||||||
/// type. Per the AWS registry, every algorithm does except CRC64NVME,
|
|
||||||
/// which is FULL_OBJECT-only.
|
|
||||||
pub fn supports_composite(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Crc64Nvme => false,
|
|
||||||
Self::Crc32
|
|
||||||
| Self::Crc32c
|
|
||||||
| Self::Sha1
|
|
||||||
| Self::Sha256
|
|
||||||
| Self::Sha512
|
|
||||||
| Self::Xxhash3
|
|
||||||
| Self::Xxhash64
|
|
||||||
| Self::Xxhash128 => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the algorithm supports the S3 FULL_OBJECT checksum type, i.e.
|
|
||||||
/// part digests can be linearly combined into the whole-object digest.
|
|
||||||
/// Only the CRC family has this property; the hash algorithms are
|
|
||||||
/// COMPOSITE-only.
|
|
||||||
pub fn supports_full_object(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Crc32 | Self::Crc32c | Self::Crc64Nvme => true,
|
|
||||||
Self::Sha1 | Self::Sha256 | Self::Sha512 | Self::Xxhash3 | Self::Xxhash64 | Self::Xxhash128 => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Checksum: Send + Sync {
|
pub trait Checksum: Send + Sync {
|
||||||
@@ -817,77 +731,6 @@ mod tests {
|
|||||||
assert_eq!(&raw[..], reference.digest128().to_be_bytes().as_slice());
|
assert_eq!(&raw[..], reference.digest128().to_be_bytes().as_slice());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_algorithm_metadata_is_consistent_for_every_variant() {
|
|
||||||
use crate::Checksum;
|
|
||||||
|
|
||||||
// Cross-checks the per-algorithm metadata methods against the hasher
|
|
||||||
// implementations themselves, so the registry cannot drift from the
|
|
||||||
// code that computes digests (backlog#1844). The list must cover every
|
|
||||||
// variant; the metadata methods use exhaustive matches, so a new
|
|
||||||
// variant that is missing here still fails to compile there first.
|
|
||||||
let all = [
|
|
||||||
ChecksumAlgorithm::Crc32,
|
|
||||||
ChecksumAlgorithm::Crc32c,
|
|
||||||
ChecksumAlgorithm::Crc64Nvme,
|
|
||||||
ChecksumAlgorithm::Sha1,
|
|
||||||
ChecksumAlgorithm::Sha256,
|
|
||||||
ChecksumAlgorithm::Sha512,
|
|
||||||
ChecksumAlgorithm::Xxhash3,
|
|
||||||
ChecksumAlgorithm::Xxhash64,
|
|
||||||
ChecksumAlgorithm::Xxhash128,
|
|
||||||
];
|
|
||||||
|
|
||||||
for algorithm in all {
|
|
||||||
// Digest length must match what the hasher actually produces.
|
|
||||||
let mut hasher = algorithm.into_impl();
|
|
||||||
hasher.update(b"metadata consistency probe");
|
|
||||||
assert_eq!(
|
|
||||||
algorithm.raw_len(),
|
|
||||||
Checksum::size(&*algorithm.into_impl()) as usize,
|
|
||||||
"{algorithm:?} raw_len() != hasher size()"
|
|
||||||
);
|
|
||||||
assert_eq!(hasher.finalize().len(), algorithm.raw_len(), "{algorithm:?} finalize length != raw_len()");
|
|
||||||
|
|
||||||
// Header name must match the hasher's own header binding.
|
|
||||||
assert_eq!(
|
|
||||||
algorithm.http_header_name(),
|
|
||||||
algorithm.into_impl().header_name(),
|
|
||||||
"{algorithm:?} http_header_name() != HttpChecksum::header_name()"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
algorithm.http_header_name(),
|
|
||||||
format!("x-amz-checksum-{}", algorithm.as_str()),
|
|
||||||
"{algorithm:?} header must be x-amz-checksum-<name>"
|
|
||||||
);
|
|
||||||
|
|
||||||
// The uppercase wire name and the lowercase parse name must be the
|
|
||||||
// same word, and the wire name must parse back to the variant.
|
|
||||||
assert!(
|
|
||||||
algorithm.s3_algorithm_name().eq_ignore_ascii_case(algorithm.as_str()),
|
|
||||||
"{algorithm:?} s3_algorithm_name() and as_str() diverge"
|
|
||||||
);
|
|
||||||
assert_eq!(algorithm.s3_algorithm_name().parse::<ChecksumAlgorithm>().unwrap(), algorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWS checksum-type support table: CRC64NVME is FULL_OBJECT-only, the
|
|
||||||
// CRC family supports FULL_OBJECT, everything else is COMPOSITE-only.
|
|
||||||
for algorithm in all {
|
|
||||||
let composite = algorithm.supports_composite();
|
|
||||||
let full_object = algorithm.supports_full_object();
|
|
||||||
assert!(composite || full_object, "{algorithm:?} supports no checksum type at all");
|
|
||||||
match algorithm {
|
|
||||||
ChecksumAlgorithm::Crc32 | ChecksumAlgorithm::Crc32c => {
|
|
||||||
assert!(composite && full_object, "{algorithm:?} must support both checksum types")
|
|
||||||
}
|
|
||||||
ChecksumAlgorithm::Crc64Nvme => {
|
|
||||||
assert!(!composite && full_object, "CRC64NVME must be FULL_OBJECT-only")
|
|
||||||
}
|
|
||||||
_ => assert!(composite && !full_object, "{algorithm:?} must be COMPOSITE-only"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_xxhash64_matches_direct_computation_big_endian_seed0() {
|
fn test_xxhash64_matches_direct_computation_big_endian_seed0() {
|
||||||
use crate::Xxhash64;
|
use crate::Xxhash64;
|
||||||
|
|||||||
@@ -38,9 +38,16 @@ hotpath.workspace = true
|
|||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
||||||
tonic = { workspace = true, features = ["gzip", "deflate"] }
|
tonic = { workspace = true, features = ["gzip", "deflate"] }
|
||||||
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
|
jiff = { workspace = true, features = ["serde"] }
|
||||||
metrics = { workspace = true }
|
metrics = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
smallvec = { workspace = true }
|
smallvec = { workspace = true }
|
||||||
|
rmp-serde = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
@@ -26,6 +27,8 @@ pub static GLOBAL_CONN_MAP: LazyLock<RwLock<HashMap<String, Channel>>> = LazyLoc
|
|||||||
pub static GLOBAL_ROOT_CERT: LazyLock<RwLock<Option<Vec<u8>>>> = LazyLock::new(|| RwLock::new(None));
|
pub static GLOBAL_ROOT_CERT: LazyLock<RwLock<Option<Vec<u8>>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
pub static GLOBAL_MTLS_IDENTITY: LazyLock<RwLock<Option<MtlsIdentityPem>>> = LazyLock::new(|| RwLock::new(None));
|
pub static GLOBAL_MTLS_IDENTITY: LazyLock<RwLock<Option<MtlsIdentityPem>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
pub static GLOBAL_OUTBOUND_TLS_GENERATION: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
|
pub static GLOBAL_OUTBOUND_TLS_GENERATION: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
|
||||||
|
/// Global initialization time of the RustFS node.
|
||||||
|
pub static GLOBAL_INIT_TIME: LazyLock<RwLock<Option<DateTime<Utc>>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
|
|
||||||
/// Log level to use when reporting cached gRPC connection eviction.
|
/// Log level to use when reporting cached gRPC connection eviction.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -60,6 +63,20 @@ pub fn try_get_global_local_node_name() -> Option<String> {
|
|||||||
.filter(|name| !name.is_empty())
|
.filter(|name| !name.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the global RustFS initialization time to the current UTC time.
|
||||||
|
pub async fn set_global_init_time_now() {
|
||||||
|
let now = Utc::now();
|
||||||
|
*GLOBAL_INIT_TIME.write().await = Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the global RustFS initialization time.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Option<DateTime<Utc>>` - The initialization time if set.
|
||||||
|
pub async fn get_global_init_time() -> Option<DateTime<Utc>> {
|
||||||
|
*GLOBAL_INIT_TIME.read().await
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the global RustFS address used for gRPC connections.
|
/// Set the global RustFS address used for gRPC connections.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -213,8 +213,6 @@ pub struct HealOpts {
|
|||||||
pub update_parity: bool,
|
pub update_parity: bool,
|
||||||
#[serde(rename = "nolock")]
|
#[serde(rename = "nolock")]
|
||||||
pub no_lock: bool,
|
pub no_lock: bool,
|
||||||
#[serde(rename = "readRepair", default)]
|
|
||||||
pub read_repair: bool,
|
|
||||||
#[serde(rename = "pool", default)]
|
#[serde(rename = "pool", default)]
|
||||||
pub pool: Option<usize>,
|
pub pool: Option<usize>,
|
||||||
#[serde(rename = "set", default)]
|
#[serde(rename = "set", default)]
|
||||||
@@ -347,9 +345,6 @@ pub struct HealChannelRequest {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Disk ID for heal disk/erasure set task
|
/// Disk ID for heal disk/erasure set task
|
||||||
pub disk: Option<String>,
|
pub disk: Option<String>,
|
||||||
/// Exact endpoints of replacement disks for an automatic erasure-set
|
|
||||||
/// rebuild. An empty list retains the generic erasure-set heal behavior.
|
|
||||||
pub heal_endpoints: Vec<String>,
|
|
||||||
/// Bucket name
|
/// Bucket name
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
/// Object prefix (optional)
|
/// Object prefix (optional)
|
||||||
@@ -597,7 +592,6 @@ pub fn create_heal_request(
|
|||||||
timeout_seconds: None,
|
timeout_seconds: None,
|
||||||
source: HealRequestSource::Internal,
|
source: HealRequestSource::Internal,
|
||||||
disk: None,
|
disk: None,
|
||||||
heal_endpoints: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,13 +632,12 @@ pub fn create_heal_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_auto_heal_disk_request(set_disk_id: String, priority: Option<HealChannelPriority>) -> HealChannelRequest {
|
pub async fn send_heal_disk(set_disk_id: String, priority: Option<HealChannelPriority>) -> Result<(), String> {
|
||||||
HealChannelRequest {
|
let req = HealChannelRequest {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
bucket: "".to_string(),
|
bucket: "".to_string(),
|
||||||
object_prefix: None,
|
object_prefix: None,
|
||||||
disk: Some(set_disk_id),
|
disk: Some(set_disk_id),
|
||||||
heal_endpoints: Vec::new(),
|
|
||||||
object_version_id: None,
|
object_version_id: None,
|
||||||
force_start: false,
|
force_start: false,
|
||||||
priority: priority.unwrap_or(HealChannelPriority::Low),
|
priority: priority.unwrap_or(HealChannelPriority::Low),
|
||||||
@@ -659,71 +652,8 @@ fn create_auto_heal_disk_request(set_disk_id: String, priority: Option<HealChann
|
|||||||
no_lock: None,
|
no_lock: None,
|
||||||
timeout_seconds: None,
|
timeout_seconds: None,
|
||||||
source: HealRequestSource::AutoHeal,
|
source: HealRequestSource::AutoHeal,
|
||||||
}
|
};
|
||||||
}
|
send_heal_request(req).await
|
||||||
|
|
||||||
fn create_auto_replacement_disk_request(
|
|
||||||
pool_index: usize,
|
|
||||||
set_index: usize,
|
|
||||||
replacement_endpoint: String,
|
|
||||||
priority: Option<HealChannelPriority>,
|
|
||||||
) -> HealChannelRequest {
|
|
||||||
let mut request = create_auto_heal_disk_request(format!("pool_{pool_index}_set_{set_index}"), priority);
|
|
||||||
request.heal_endpoints = vec![replacement_endpoint];
|
|
||||||
request.pool_index = Some(pool_index);
|
|
||||||
request.set_index = Some(set_index);
|
|
||||||
request
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Submit the legacy generic erasure-set auto-heal request.
|
|
||||||
pub async fn send_heal_disk(set_disk_id: String, priority: Option<HealChannelPriority>) -> Result<(), String> {
|
|
||||||
send_heal_request(create_auto_heal_disk_request(set_disk_id, priority)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Submit an automatic replacement heal for one known disk endpoint.
|
|
||||||
///
|
|
||||||
/// The endpoint makes the request eligible for the durable replacement intent
|
|
||||||
/// and completion-proof path in the heal task.
|
|
||||||
pub async fn send_heal_replacement_disk(
|
|
||||||
pool_index: usize,
|
|
||||||
set_index: usize,
|
|
||||||
replacement_endpoint: String,
|
|
||||||
priority: Option<HealChannelPriority>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
send_heal_request(create_auto_replacement_disk_request(
|
|
||||||
pool_index,
|
|
||||||
set_index,
|
|
||||||
replacement_endpoint,
|
|
||||||
priority,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod auto_heal_disk_request_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replacement_disk_request_carries_its_exact_endpoint() {
|
|
||||||
let request =
|
|
||||||
create_auto_replacement_disk_request(2, 3, "http://node2:9000/drive3".to_string(), Some(HealChannelPriority::Normal));
|
|
||||||
|
|
||||||
assert_eq!(request.disk.as_deref(), Some("pool_2_set_3"));
|
|
||||||
assert_eq!(request.heal_endpoints, ["http://node2:9000/drive3"]);
|
|
||||||
assert_eq!(request.pool_index, Some(2));
|
|
||||||
assert_eq!(request.set_index, Some(3));
|
|
||||||
assert_eq!(request.source, HealRequestSource::AutoHeal);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_auto_heal_disk_request_has_no_replacement_endpoint() {
|
|
||||||
let request = create_auto_heal_disk_request("pool_2_set_3".to_string(), None);
|
|
||||||
|
|
||||||
assert!(request.heal_endpoints.is_empty());
|
|
||||||
assert_eq!(request.pool_index, None);
|
|
||||||
assert_eq!(request.set_index, None);
|
|
||||||
assert_eq!(request.source, HealRequestSource::AutoHeal);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -742,24 +672,6 @@ mod tests {
|
|||||||
assert_eq!(request.source, HealRequestSource::Internal);
|
assert_eq!(request.source, HealRequestSource::Internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn heal_opts_deserializes_missing_read_repair_as_false() {
|
|
||||||
let opts: HealOpts = serde_json::from_str(
|
|
||||||
r#"{
|
|
||||||
"recursive": false,
|
|
||||||
"dryRun": false,
|
|
||||||
"remove": false,
|
|
||||||
"recreate": false,
|
|
||||||
"scanMode": "normal",
|
|
||||||
"updateParity": false,
|
|
||||||
"nolock": false
|
|
||||||
}"#,
|
|
||||||
)
|
|
||||||
.expect("old heal options without readRepair should decode");
|
|
||||||
|
|
||||||
assert!(!opts.read_repair);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heal_admission_result_labels_are_stable() {
|
fn heal_admission_result_labels_are_stable() {
|
||||||
assert_eq!(HealAdmissionResult::Accepted.result_label(), "accepted");
|
assert_eq!(HealAdmissionResult::Accepted.result_label(), "accepted");
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user