mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-01 00:32:21 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0913686b12 | |||
| df64242f64 | |||
| d3b75e695e | |||
| dde1aa77a1 | |||
| be01e513be | |||
| 9ab034df62 | |||
| 624463c5ba | |||
| 713920dcd6 | |||
| f219a0aba8 | |||
| 311e4306e6 | |||
| 7f0f4941cf | |||
| 655bb470b8 | |||
| 56a4099f99 | |||
| ee6de7d786 |
@@ -19,7 +19,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install quality tools
|
||||
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
|
||||
uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2
|
||||
with:
|
||||
tool: |
|
||||
ripgrep@15.2.0
|
||||
|
||||
@@ -109,7 +109,7 @@ runs:
|
||||
uses: taiki-e/install-action@96c7780c1d8a2b8723e12031def873a434d39d8d # nextest
|
||||
|
||||
- name: Setup Rust cache
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
with:
|
||||
# false is rust-cache's own default. With true, cleanup.ts returns
|
||||
# *before* pruning ~/.cargo/registry/src, and config.ts archives the
|
||||
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ripgrep
|
||||
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
|
||||
uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2
|
||||
with:
|
||||
tool: ripgrep@15.2.0
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
# default is now "false", but state it explicitly — see
|
||||
# scripts/security/check_cache_save_if.sh.
|
||||
- name: Setup Rust cache
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
with:
|
||||
# Same reasoning as the setup composite: true archives every
|
||||
# dependency's unpacked source tree.
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install cargo-deny
|
||||
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
|
||||
uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
matrix: ${{ fromJson(needs.prepare-platform-matrix.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -702,7 +702,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Upload to GitHub artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.package.outputs.package_name }}
|
||||
path: "rustfs-*.zip"
|
||||
@@ -840,7 +840,7 @@ jobs:
|
||||
release_url: ${{ steps.create.outputs.release_url }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -899,12 +899,12 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download all build artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: ./artifacts
|
||||
pattern: rustfs-*
|
||||
@@ -1138,7 +1138,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
|
||||
- name: Upload cargo timings report
|
||||
if: inputs.emit_timings
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cargo-timings-ci-dev
|
||||
path: target/cargo-timings/
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+39
-39
@@ -77,7 +77,7 @@ jobs:
|
||||
outputs:
|
||||
mode: ${{ steps.scope.outputs.mode }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
@@ -101,11 +101,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Typos check with custom config file
|
||||
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
|
||||
uses: crate-ci/typos@512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2
|
||||
|
||||
# Fail early with compile-free checks for every pull request.
|
||||
quick-checks:
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
# Checkout otherwise writes the token into .git/config, where a PR's
|
||||
# own build.rs or proc-macro could read it back out.
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
|
||||
- name: Upload test reports and diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: junit-test-and-lint-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -305,7 +305,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -344,7 +344,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
|
||||
- name: Upload ILM test diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ilm-integration-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -408,7 +408,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -521,7 +521,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -561,7 +561,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -621,7 +621,7 @@ jobs:
|
||||
PYBUILD
|
||||
|
||||
- name: Upload debug binary
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: |
|
||||
@@ -646,7 +646,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -662,7 +662,7 @@ jobs:
|
||||
run: python3 scripts/e2e_binary.py build --bins --features rio-v2,e2e-test-hooks
|
||||
|
||||
- name: Upload debug binary
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary-rio-v2
|
||||
path: |
|
||||
@@ -690,7 +690,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -754,7 +754,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -770,7 +770,7 @@ jobs:
|
||||
install-build-packaging-tools: 'false'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -785,7 +785,7 @@ jobs:
|
||||
# Download after the cache restore so the freshly built binary from the
|
||||
# build job always wins over anything restored into target/debug.
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
@@ -821,7 +821,7 @@ jobs:
|
||||
|
||||
- name: Upload e2e smoke diagnostics
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-smoke-diagnostics-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -831,7 +831,7 @@ jobs:
|
||||
|
||||
- name: Upload e2e smoke JUnit report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-smoke-junit-${{ github.run_number }}
|
||||
path: target/nextest/e2e-smoke/junit.xml
|
||||
@@ -856,7 +856,7 @@ jobs:
|
||||
|
||||
- name: Upload test logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-test-logs-${{ github.run_number }}
|
||||
path: ${{ runner.temp }}/rustfs-e2e-*/rustfs.log
|
||||
@@ -879,7 +879,7 @@ jobs:
|
||||
timeout-minutes: 55
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -903,7 +903,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -945,7 +945,7 @@ jobs:
|
||||
# Download after the cache restore so the freshly built binary from the
|
||||
# build job always wins over anything restored into target/debug.
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
@@ -1005,7 +1005,7 @@ jobs:
|
||||
|
||||
- name: Upload junit
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-full-junit-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -1015,7 +1015,7 @@ jobs:
|
||||
|
||||
- name: Upload startup CAS evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fresh-startup-cas-evidence-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -1034,7 +1034,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -1044,7 +1044,7 @@ jobs:
|
||||
rm -f /tmp/rustfs.log
|
||||
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary-rio-v2
|
||||
path: target/debug
|
||||
@@ -1077,7 +1077,7 @@ jobs:
|
||||
|
||||
- name: Upload test logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-test-logs-rio-v2-${{ github.run_number }}
|
||||
path: /tmp/rustfs.log
|
||||
@@ -1090,12 +1090,12 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
@@ -1120,7 +1120,7 @@ jobs:
|
||||
|
||||
- name: Upload s3 test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: s3tests-implemented-${{ github.run_number }}
|
||||
path: artifacts/s3tests-single/**
|
||||
@@ -1164,12 +1164,12 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
@@ -1202,7 +1202,7 @@ jobs:
|
||||
|
||||
- name: Upload s3 test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: s3tests-lifecycle-behavior-${{ github.run_number }}
|
||||
path: artifacts/s3tests-single/**
|
||||
@@ -1233,7 +1233,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Require the expected result of every CI lane
|
||||
@@ -1272,7 +1272,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
steps:
|
||||
- name: Report CLA result for merge queue
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.checks.create({
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# Copyright 2026 RustFS Team
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Connect diagnostic scheduler receipt acceptance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: Main-branch Build and Release run ID
|
||||
required: true
|
||||
type: string
|
||||
artifact_id:
|
||||
description: Linux x86_64 GNU artifact from that run
|
||||
required: true
|
||||
type: string
|
||||
source_sha:
|
||||
description: Exact RustFS source commit
|
||||
required: true
|
||||
type: string
|
||||
artifact_digest:
|
||||
description: GitHub artifact digest including sha256 prefix
|
||||
required: true
|
||||
type: string
|
||||
binary_sha256:
|
||||
description: Independently verified rustfs binary SHA-256
|
||||
required: true
|
||||
type: string
|
||||
connect_sha:
|
||||
description: Exact Connect scheduler acceptance harness commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
diagnostic-schedule:
|
||||
name: Verify production scheduler receipts over mTLS
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.connect_sha }}
|
||||
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Verify official source and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ && "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ && "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
|
||||
[[ "$BINARY_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
|
||||
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
|
||||
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
|
||||
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
|
||||
|
||||
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
|
||||
jq -e --arg source "$SOURCE_SHA" --arg repository "$GITHUB_REPOSITORY" '
|
||||
.head_sha == $source and .head_branch == "main"
|
||||
and .head_repository.full_name == $repository
|
||||
and .name == "Build and Release" and .path == ".github/workflows/build.yml"
|
||||
and .status == "completed" and .conclusion == "success"
|
||||
' <<<"$run" >/dev/null
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e '
|
||||
[.[].jobs[] | select(.name | test("^Build RustFS \\(linux-x86_64-gnu, [a-z0-9-]+, x86_64-unknown-linux-gnu, false, linux, pyroscope\\)$"))] as $matches
|
||||
| ($matches | length) == 1 and $matches[0].conclusion == "success"
|
||||
' <<<"$jobs" >/dev/null
|
||||
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
|
||||
jq -e --argjson run "$BUILD_RUN_ID" --arg source "$SOURCE_SHA" --arg digest "$ARTIFACT_DIGEST" '
|
||||
.workflow_run.id == $run and .workflow_run.head_sha == $source
|
||||
and .name == ("rustfs-linux-x86_64-gnu-dev-" + $source[0:7])
|
||||
and .digest == $digest and .expired == false
|
||||
and (.expires_at | fromdateiso8601) > now
|
||||
and .size_in_bytes > 0 and .size_in_bytes <= 2147483648
|
||||
' <<<"$artifact" >/dev/null
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" > official-artifact.zip
|
||||
printf '%s official-artifact.zip\n' "${ARTIFACT_DIGEST#sha256:}" | sha256sum --check --strict
|
||||
|
||||
- name: Extract the verified official binary
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import zipfile
|
||||
|
||||
def regular(entry):
|
||||
mode = entry.external_attr >> 16
|
||||
return not entry.is_dir() and (stat.S_IFMT(mode) in (0, stat.S_IFREG))
|
||||
|
||||
package = f"rustfs-linux-x86_64-gnu-dev-{os.environ['SOURCE_SHA'][:7]}.zip"
|
||||
with zipfile.ZipFile('official-artifact.zip') as outer:
|
||||
entries = outer.infolist()
|
||||
assert 1 <= len(entries) <= 16
|
||||
assert len({item.filename for item in entries}) == len(entries)
|
||||
assert all(regular(item) and pathlib.PurePosixPath(item.filename).name == item.filename for item in entries)
|
||||
assert sum(item.file_size for item in entries) <= 2147483648
|
||||
assert outer.testzip() is None
|
||||
with zipfile.ZipFile(io.BytesIO(outer.read(package))) as inner:
|
||||
entries = inner.infolist()
|
||||
assert {item.filename for item in entries} == {'rustfs', 'rustfs-cli'} and len(entries) == 2
|
||||
assert all(regular(item) and 0 < item.file_size <= 1073741824 for item in entries)
|
||||
assert inner.testzip() is None
|
||||
pathlib.Path('binary').mkdir()
|
||||
with inner.open('rustfs') as source, open('binary/rustfs', 'xb') as destination:
|
||||
import shutil
|
||||
shutil.copyfileobj(source, destination)
|
||||
PY
|
||||
chmod 0755 binary/rustfs
|
||||
printf '%s binary/rustfs\n' "$BINARY_SHA256" | sha256sum --check --strict
|
||||
file binary/rustfs | grep -Eq 'ELF 64-bit LSB.*x86-64'
|
||||
|
||||
- name: Run the real scheduler and receipt delivery scenario
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git -C connect-harness diff --exit-code
|
||||
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
|
||||
[[ $(git -C connect-harness diff --name-only) == tests/e2e/connected/rustfs-ref ]]
|
||||
npm --prefix connect-harness/web ci
|
||||
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
|
||||
make -C connect-harness e2e-connected-dispatch-check
|
||||
(cd connect-harness && ./tests/connectivity/diagnostic-schedule-evidence.test.sh)
|
||||
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
|
||||
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
|
||||
CONNECT_E2E_DIAGNOSTIC_SCHEDULE_EVIDENCE="$GITHUB_WORKSPACE/diagnostic-schedule-evidence.json" \
|
||||
make -C connect-harness e2e-connected E2E_SCENARIO=diagnostic-schedule
|
||||
|
||||
- name: Bind and validate sanitized evidence
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq -e --arg connect "$CONNECT_SHA" --arg source "$SOURCE_SHA" --arg binary "$BINARY_SHA256" '
|
||||
.provenance.connectSha == $connect and .provenance.sourceSha == $source
|
||||
and .provenance.binarySha256 == $binary
|
||||
' diagnostic-schedule-evidence.json >/dev/null
|
||||
jq --arg run "$GITHUB_RUN_ID" --arg build "$BUILD_RUN_ID" \
|
||||
--arg artifact "$ARTIFACT_ID" --arg digest "$ARTIFACT_DIGEST" '
|
||||
.provenance += {workflowRunId:$run,buildRunId:$build,artifactId:$artifact,artifactDigest:$digest}
|
||||
' diagnostic-schedule-evidence.json > diagnostic-schedule-evidence.bound.json
|
||||
mv diagnostic-schedule-evidence.bound.json diagnostic-schedule-evidence.json
|
||||
node connect-harness/tests/connectivity/verify-diagnostic-schedule-evidence.mjs diagnostic-schedule-evidence.json --bound
|
||||
|
||||
- name: Upload verified receipt evidence
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: connect-diagnostic-schedule-evidence-${{ github.run_id }}
|
||||
path: diagnostic-schedule-evidence.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('performance-drive-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-performance-drive-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('performance-network-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-performance-network-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('profile-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-profile-cpu-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# Copyright 2026 RustFS Team
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Connect service memory profile acceptance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: Successful main-branch Build and Release run ID
|
||||
required: true
|
||||
type: string
|
||||
artifact_id:
|
||||
description: Linux x86_64 GNU artifact from that run
|
||||
required: true
|
||||
type: string
|
||||
source_sha:
|
||||
description: Exact RustFS source commit
|
||||
required: true
|
||||
type: string
|
||||
artifact_digest:
|
||||
description: GitHub artifact digest including sha256 prefix
|
||||
required: true
|
||||
type: string
|
||||
binary_sha256:
|
||||
description: Independently verified rustfs binary SHA-256
|
||||
required: true
|
||||
type: string
|
||||
connect_sha:
|
||||
description: Exact Connect memory acceptance harness commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
profile-memory:
|
||||
name: Verify signed service memory profile over mTLS
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.connect_sha }}
|
||||
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Verify official source and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$GITHUB_REPOSITORY" == rustfs/rustfs ]]
|
||||
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ && "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ && "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
|
||||
[[ "$BINARY_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
|
||||
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
|
||||
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
|
||||
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
|
||||
run=$(gh api "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}")
|
||||
jq -e --arg source "$SOURCE_SHA" '
|
||||
.head_sha == $source and .head_branch == "main"
|
||||
and .head_repository.full_name == "rustfs/rustfs"
|
||||
and .name == "Build and Release" and .path == ".github/workflows/build.yml"
|
||||
and .status == "completed" and .conclusion == "success"
|
||||
' <<<"$run" >/dev/null
|
||||
jobs=$(gh api --paginate --slurp "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e '
|
||||
[.[].jobs[] | select(.name | test("^Build RustFS \\(linux-x86_64-gnu, [a-z0-9-]+, x86_64-unknown-linux-gnu, false, linux, pyroscope\\)$"))] as $matches
|
||||
| ($matches | length) == 1 and $matches[0].conclusion == "success"
|
||||
' <<<"$jobs" >/dev/null
|
||||
artifact=$(gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}")
|
||||
jq -e --argjson run "$BUILD_RUN_ID" --arg source "$SOURCE_SHA" --arg digest "$ARTIFACT_DIGEST" '
|
||||
.workflow_run.id == $run and .workflow_run.head_sha == $source
|
||||
and .name == ("rustfs-linux-x86_64-gnu-dev-" + $source[0:7])
|
||||
and .digest == $digest and .expired == false
|
||||
and (.expires_at | fromdateiso8601) > now
|
||||
and .size_in_bytes > 0 and .size_in_bytes <= 2147483648
|
||||
' <<<"$artifact" >/dev/null
|
||||
jq -r '.expires_at' <<<"$artifact" > artifact-expires-at
|
||||
gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}/zip" > official-artifact.zip
|
||||
[[ $(stat --format=%s official-artifact.zip) == $(jq -r '.size_in_bytes' <<<"$artifact") ]]
|
||||
printf '%s official-artifact.zip\n' "${ARTIFACT_DIGEST#sha256:}" | sha256sum --check --strict
|
||||
|
||||
- name: Extract only the verified official service binary
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import zipfile
|
||||
|
||||
def regular(entry):
|
||||
mode = entry.external_attr >> 16
|
||||
return not entry.is_dir() and stat.S_IFMT(mode) in (0, stat.S_IFREG)
|
||||
|
||||
package = f"rustfs-linux-x86_64-gnu-dev-{os.environ['SOURCE_SHA'][:7]}.zip"
|
||||
with zipfile.ZipFile('official-artifact.zip') as outer:
|
||||
entries = outer.infolist()
|
||||
assert 1 <= len(entries) <= 16
|
||||
assert len({entry.filename for entry in entries}) == len(entries)
|
||||
assert all(regular(entry) and pathlib.PurePosixPath(entry.filename).name == entry.filename for entry in entries)
|
||||
assert sum(entry.file_size for entry in entries) <= 2147483648
|
||||
assert outer.testzip() is None
|
||||
with zipfile.ZipFile(io.BytesIO(outer.read(package))) as inner:
|
||||
entries = inner.infolist()
|
||||
assert {entry.filename for entry in entries} == {'rustfs', 'rustfs-cli'} and len(entries) == 2
|
||||
assert all(regular(entry) and 0 < entry.file_size <= 1073741824 for entry in entries)
|
||||
assert inner.testzip() is None
|
||||
pathlib.Path('binary').mkdir()
|
||||
with inner.open('rustfs') as source, open('binary/rustfs', 'xb') as destination:
|
||||
shutil.copyfileobj(source, destination)
|
||||
PY
|
||||
chmod 0755 binary/rustfs
|
||||
printf '%s binary/rustfs\n' "$BINARY_SHA256" | sha256sum --check --strict
|
||||
file binary/rustfs | grep -Eq 'ELF 64-bit LSB.*x86-64'
|
||||
|
||||
- name: Run the real service job under S3 workload
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git -C connect-harness diff --exit-code
|
||||
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
|
||||
changed=$(git -C connect-harness diff --name-only)
|
||||
[[ -z "$changed" || "$changed" == tests/e2e/connected/rustfs-ref ]]
|
||||
npm --prefix connect-harness/web ci
|
||||
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
|
||||
make -C connect-harness e2e-connected-dispatch-check
|
||||
(cd connect-harness && ./tests/connectivity/profile-memory-evidence.test.sh)
|
||||
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
|
||||
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
|
||||
CONNECT_E2E_PROFILE_MEMORY_EVIDENCE="$GITHUB_WORKSPACE/profile-memory-evidence.json" \
|
||||
make -C connect-harness e2e-connected E2E_SCENARIO=profile-memory
|
||||
|
||||
- name: Bind and validate sanitized evidence
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq -e --arg connect "$CONNECT_SHA" --arg source "$SOURCE_SHA" --arg binary "$BINARY_SHA256" '
|
||||
.provenance.connectSha == $connect and .provenance.sourceSha == $source
|
||||
and .provenance.binarySha256 == $binary
|
||||
' profile-memory-evidence.json >/dev/null
|
||||
jq --arg run "$GITHUB_RUN_ID" --arg build "$BUILD_RUN_ID" \
|
||||
--arg artifact "$ARTIFACT_ID" --arg digest "$ARTIFACT_DIGEST" \
|
||||
--arg expiry "$(cat artifact-expires-at)" '
|
||||
.provenance += {workflowRunId:$run,buildRunId:$build,artifactId:$artifact,artifactDigest:$digest,artifactExpiresAt:$expiry}
|
||||
' profile-memory-evidence.json > profile-memory-evidence.bound.json
|
||||
mv profile-memory-evidence.bound.json profile-memory-evidence.json
|
||||
node connect-harness/tests/connectivity/profile-memory-evidence.mjs profile-memory-evidence.json --bound
|
||||
|
||||
- name: Upload only verified sanitized evidence
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: connect-profile-memory-evidence-${{ github.run_id }}
|
||||
path: profile-memory-evidence.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('profile-threads-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-profile-threads-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('top-api-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-top-api-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
mv top-disk-runtime-evidence.bound.json top-disk-runtime-evidence.json
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-top-disk-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('top-locks-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-top-locks-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -53,19 +53,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
if: ${{ always() && hashFiles('top-rpc-service-job-evidence.json') != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-top-rpc-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
NEXTEST_PROFILE: ci
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
install-build-packaging-tools: 'false'
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
|
||||
uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-lcov-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
source_ref: ${{ steps.check.outputs.source_ref }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
# For workflow_run events, checkout the specific commit that triggered the workflow
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.build-check.outputs.source_ref }}
|
||||
@@ -544,7 +544,7 @@ jobs:
|
||||
category: container-image-${{ matrix.variant }}
|
||||
|
||||
- name: Upload container scan report
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: container-image-scan-${{ matrix.variant }}
|
||||
path: trivy-${{ matrix.variant }}.sarif
|
||||
@@ -563,7 +563,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.build-check.outputs.source_ref }}
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
UPGRADE_SOURCE_SHA256: 7c789386bf85278f865b8e0d359bf4edb84d5aa408cc3fa54a18c25ca74cd6e7
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
|
||||
- name: Upload distributed e2e diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-distributed-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
|
||||
# The STS dual-node test requires awscurl and fails if it is unavailable.
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Upload nextest junit report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-replication-nightly-junit-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
|
||||
- name: Upload cluster fault diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-cluster-nightly-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
RUSTFS_BUILD_FEATURES: ftps,webdav,sftp
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
|
||||
- name: Upload protocol diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-protocol-nightly-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
TEST_MODE: ${{ matrix.test-mode }}
|
||||
S3_SHARD_INDEX: ${{ matrix.shard-index }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
# ship a working pip (ci-1: a bare python3 without pip is what broke the
|
||||
# scheduled sweep). Keeps the workflow correct across runner drift.
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
hostname: rustfs4
|
||||
volumes: [rustfs4-data:/data]
|
||||
lb:
|
||||
image: haproxy:2.9
|
||||
image: haproxy:3.4.4
|
||||
hostname: lb
|
||||
networks: [rustfs-net]
|
||||
ports:
|
||||
@@ -308,6 +308,11 @@ jobs:
|
||||
|
||||
docker compose -f compose.yml up -d
|
||||
|
||||
# Validate the pinned HAProxy image parses the exact config before
|
||||
# the S3 compatibility sweep starts.
|
||||
docker compose -f compose.yml exec -T lb \
|
||||
haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
|
||||
|
||||
- name: Wait for RustFS ready
|
||||
run: |
|
||||
for _ in {1..120}; do
|
||||
@@ -357,7 +362,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always() && env.ACT != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: s3tests-${{ env.TEST_MODE }}-shard-${{ matrix.shard-index }}
|
||||
path: artifacts/**
|
||||
@@ -369,12 +374,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -435,7 +440,7 @@ jobs:
|
||||
|
||||
- name: Upload canary artifacts
|
||||
if: always() && env.ACT != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: s3tests-upstream-head
|
||||
path: artifacts/s3tests-upstream-head/**
|
||||
@@ -454,7 +459,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
|
||||
- name: Upload scanner/heal G09 evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-g09-evidence-${{ github.run_number }}
|
||||
path: ${{ runner.temp }}/rustfs-upgrade-g09-evidence/${{ matrix.artifact }}
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
|
||||
- name: Upload server logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-server-logs-${{ github.run_number }}
|
||||
path: ${{ runner.temp }}/rustfs-upgrade-logs
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Collect and publish verified chain health
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_health.py --source-ref "${NIGHTLY_SOURCE_REF}" --publish --output "${RUNNER_TEMP}/chain-health.json"
|
||||
- name: Retain health observation
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-health-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-health.json
|
||||
|
||||
+10
-10
@@ -81,7 +81,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
|
||||
|
||||
- name: Install cargo-fuzz
|
||||
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
|
||||
uses: taiki-e/install-action@7623a79cdfecb99d681017af368ca353d9f49bb5 # v2
|
||||
with:
|
||||
tool: cargo-fuzz
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload prebuilt fuzz binaries
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/
|
||||
@@ -143,12 +143,12 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download prebuilt fuzz binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Upload fuzz smoke artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fuzz-smoke-${{ matrix.target }}-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -197,12 +197,12 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download prebuilt fuzz binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: fuzz-prebuilt-binaries-${{ github.run_number }}
|
||||
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
|
||||
- name: Upload nightly fuzz artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fuzz-nightly-${{ matrix.target }}-${{ github.run_number }}
|
||||
path: |
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout helm chart repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
--version "${{ steps.version.outputs.chart_version }}"
|
||||
|
||||
- name: Upload helm package as artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: helm-package
|
||||
path: helm/rustfs/*.tgz
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout helm package repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
# persist-credentials-exempt: this checkout's token IS the push credential —
|
||||
# the job git-pushes to rustfs/helm below. Clearing it breaks chart publishing.
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
token: ${{ secrets.RUSTFS_HELM_PACKAGE }}
|
||||
|
||||
- name: Download helm package
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: helm-package
|
||||
path: ./
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
INTEROP_REQUIRED_TESTS: '["reads_minio_generated_sse_s3_multipart_fixture", "reads_minio_generated_sse_kms_multipart_fixture", "rejects_minio_generated_sse_s3_fixture_with_wrong_kms_key", "rejects_minio_generated_sse_s3_fixture_with_truncated_ciphertext"]'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always() && env.ACT != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: mint
|
||||
path: artifacts/**
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
source_ref: ${{ steps.source.outputs.ref }}
|
||||
steps:
|
||||
- name: Checkout selected source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ env.NIGHTLY_BUILD_REF }}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.resolve-source.outputs.source_sha }}
|
||||
@@ -237,12 +237,12 @@ jobs:
|
||||
echo "rpm_file=$RPM_FILE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload DEB artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.deb.outputs.deb_file }}
|
||||
path: ${{ steps.deb.outputs.deb_file }}
|
||||
- name: Upload RPM artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.rpm.outputs.rpm_file }}
|
||||
path: ${{ steps.rpm.outputs.rpm_file }}
|
||||
@@ -328,7 +328,7 @@ jobs:
|
||||
|
||||
- name: Upload nightly candidate manifest
|
||||
if: ${{ steps.publish.outputs.candidate_file != '' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: nightly-candidate-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.publish.outputs.candidate_file }}
|
||||
@@ -383,7 +383,7 @@ jobs:
|
||||
NO_PROXY: 127.0.0.1,localhost
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.resolve-source.outputs.source_sha }}
|
||||
@@ -473,7 +473,7 @@ jobs:
|
||||
NO_PROXY: 127.0.0.1,localhost
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.resolve-source.outputs.source_sha }}
|
||||
@@ -501,7 +501,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -39,24 +39,24 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
# persist-credentials-exempt: update-flake-lock pushes the branch and opens
|
||||
# the PR. It passes FLAKE_UPDATE_TOKEN to create-pull-request itself rather
|
||||
# than reusing .git/config, but that is unverified — exempt until a
|
||||
# workflow_dispatch run confirms it (rustfs/backlog#1602).
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/determinate-nix-action@629b284231c2a82554b724e357e47fc6020833c8 # v3
|
||||
uses: DeterminateSystems/determinate-nix-action@8d87e8d5e5b8a8309d4281094560f127d9a265f1 # v3.22.5
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/flakehub-cache-action@1f9a51a2959d3e26c7838c6f3bf9f48acae525ea # v3.20.0
|
||||
uses: DeterminateSystems/flakehub-cache-action@83282a8aef353db1d659d3bacf4a28b6683de0b3 # v3.22.5
|
||||
|
||||
- name: Check Nix flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@3164002371bc90729c68af0e24d5aacf20d7c9f6 # v12
|
||||
uses: DeterminateSystems/flake-checker-action@786422608c7bded2bbc9741ad9f91356842bf520 # v14
|
||||
|
||||
- name: Update flake.lock
|
||||
id: update
|
||||
uses: DeterminateSystems/update-flake-lock@fd9359ac79d0e912f1b4b947a48470b3e2799b56 # main
|
||||
uses: DeterminateSystems/update-flake-lock@da03c0f078bc4b2c37ee4f7e072d34bf8f188bb3 # v29
|
||||
with:
|
||||
git-author-name: houseme
|
||||
git-author-email: housemecn@gmail.com
|
||||
|
||||
@@ -72,12 +72,12 @@ jobs:
|
||||
NIX_CURL_FLAGS: -A cargo/stable
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/determinate-nix-action@4eea0b33e3d1f02ecfe37cf16e7204c424009606 # v3.21.0
|
||||
uses: DeterminateSystems/determinate-nix-action@8d87e8d5e5b8a8309d4281094560f127d9a265f1 # v3.22.5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
extra-conf: |
|
||||
@@ -86,10 +86,10 @@ jobs:
|
||||
max-jobs = 1
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/flakehub-cache-action@c01e819d047464c3edf6ba778f075952af5a3aa7 # v3.21.0
|
||||
uses: DeterminateSystems/flakehub-cache-action@83282a8aef353db1d659d3bacf4a28b6683de0b3 # v3.22.5
|
||||
|
||||
- name: Check Nix Flake Inputs
|
||||
uses: DeterminateSystems/flake-checker-action@3164002371bc90729c68af0e24d5aacf20d7c9f6 # v12
|
||||
uses: DeterminateSystems/flake-checker-action@786422608c7bded2bbc9741ad9f91356842bf520 # v14
|
||||
with:
|
||||
fail-mode: true
|
||||
ignore-missing-flake-lock: false
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Upload service logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: oidc-keycloak-live-${{ github.run_number }}
|
||||
path: ${{ runner.temp }}/rustfs-keycloak-live-*/**/*.log
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
NEXTEST_LISTING: ${{ github.workspace }}/artifacts/odm-interop/minio/selection.json
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
|
||||
- name: Upload the MinIO interop report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: odm-interop-minio-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.credentials.outputs.present == 'true'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
|
||||
- name: Upload the ${{ matrix.provider }} interop report
|
||||
if: always() && steps.credentials.outputs.present == 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: odm-interop-${{ matrix.provider }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
#
|
||||
# Flow:
|
||||
# 1. Resolve and validate the selected Build workflow run and source identity
|
||||
# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu) from build artifacts
|
||||
# 3. Build DEB packages for amd64 and arm64
|
||||
# 4. Build RPM packages for x86_64 and aarch64
|
||||
# 2. Download Linux binaries (x86_64-gnu, aarch64-gnu, x86_64-musl,
|
||||
# aarch64-musl) from build artifacts
|
||||
# 3. Build DEB packages for amd64 and arm64, gnu and musl variants
|
||||
# 4. Build RPM packages for x86_64 and aarch64, gnu and musl variants
|
||||
# 5. Upload all packages to Cloudflare R2 and the GitHub release
|
||||
|
||||
name: Package DEB/RPM
|
||||
@@ -224,9 +225,9 @@ jobs:
|
||||
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 and libc variant
|
||||
package:
|
||||
name: Package (${{ matrix.arch }})
|
||||
name: Package (${{ matrix.arch }}-${{ matrix.libc }})
|
||||
needs: resolve
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
@@ -236,16 +237,38 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
libc: gnu
|
||||
deb_arch: amd64
|
||||
rpm_arch: x86_64
|
||||
artifact_name: "rustfs-linux-x86_64-gnu"
|
||||
# gnu binaries dynamically link glibc; musl binaries are static
|
||||
# and run without a libc dependency.
|
||||
deb_depends: "libc6 (>= 2.31)"
|
||||
rpm_depends: "glibc >= 2.31"
|
||||
- arch: aarch64
|
||||
libc: gnu
|
||||
deb_arch: arm64
|
||||
rpm_arch: aarch64
|
||||
artifact_name: "rustfs-linux-aarch64-gnu"
|
||||
deb_depends: "libc6 (>= 2.31)"
|
||||
rpm_depends: "glibc >= 2.31"
|
||||
- arch: x86_64
|
||||
libc: musl
|
||||
deb_arch: amd64
|
||||
rpm_arch: x86_64
|
||||
artifact_name: "rustfs-linux-x86_64-musl"
|
||||
deb_depends: ""
|
||||
rpm_depends: ""
|
||||
- arch: aarch64
|
||||
libc: musl
|
||||
deb_arch: arm64
|
||||
rpm_arch: aarch64
|
||||
artifact_name: "rustfs-linux-aarch64-musl"
|
||||
deb_depends: ""
|
||||
rpm_depends: ""
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -256,17 +279,17 @@ jobs:
|
||||
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 }}
|
||||
ARCH: ${{ matrix.rpm_arch }}
|
||||
LIBC: ${{ matrix.libc }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
normalized=$(./scripts/release/package_versions.sh \
|
||||
"$BUILD_TYPE" "$SOURCE_VERSION" "$DEV_SEQUENCE" "$DEB_ARCH" "$RPM_ARCH")
|
||||
"$BUILD_TYPE" "$SOURCE_VERSION" "$DEV_SEQUENCE" "$ARCH" "$LIBC")
|
||||
printf '%s\n' "$normalized" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download binary artifact from build run
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: ${{ matrix.artifact_name }}*
|
||||
path: ./binary-artifact
|
||||
@@ -308,6 +331,8 @@ jobs:
|
||||
DEB_VERSION: ${{ steps.versions.outputs.deb_version }}
|
||||
DEB_ARCH: ${{ matrix.deb_arch }}
|
||||
DEB_FILE: ${{ steps.versions.outputs.deb_file }}
|
||||
DEB_DEPENDS: ${{ matrix.deb_depends }}
|
||||
LIBC: ${{ matrix.libc }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -345,9 +370,17 @@ jobs:
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: ${DEB_ARCH}
|
||||
Depends: libc6 (>= 2.31)
|
||||
EOF
|
||||
|
||||
# gnu binaries dynamically link glibc and must declare it; musl
|
||||
# binaries are statically linked and need no libc dependency.
|
||||
if [[ -n "$DEB_DEPENDS" ]]; then
|
||||
echo "Depends: ${DEB_DEPENDS}" >> "${PKG_DIR}/DEBIAN/control"
|
||||
fi
|
||||
|
||||
cat >> "${PKG_DIR}/DEBIAN/control" << EOF
|
||||
Maintainer: RustFS Team <support@rustfs.com>
|
||||
Description: High-performance distributed object storage
|
||||
Description: High-performance distributed object storage (${LIBC} build)
|
||||
RustFS is a high-performance distributed object storage software
|
||||
built using Rust. It is compatible with MinIO and S3 API.
|
||||
Homepage: https://rustfs.com
|
||||
@@ -389,15 +422,24 @@ jobs:
|
||||
RPM_RELEASE: ${{ steps.versions.outputs.rpm_release }}
|
||||
RPM_ARCH: ${{ matrix.rpm_arch }}
|
||||
RPM_FILE: ${{ steps.versions.outputs.rpm_file }}
|
||||
RPM_DEPENDS: ${{ matrix.rpm_depends }}
|
||||
LIBC: ${{ matrix.libc }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Building RPM for ${RPM_ARCH}"
|
||||
echo "Building RPM for ${RPM_ARCH} (${LIBC})"
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y ruby ruby-dev build-essential rpm
|
||||
sudo gem install fpm
|
||||
./scripts/test_package_versions.sh --require-package-managers
|
||||
|
||||
# gnu binaries dynamically link glibc and must declare it; musl
|
||||
# binaries are statically linked and need no libc dependency.
|
||||
depends_args=()
|
||||
if [[ -n "$RPM_DEPENDS" ]]; then
|
||||
depends_args+=(--depends "$RPM_DEPENDS")
|
||||
fi
|
||||
|
||||
# Create config file for fpm (DEB build creates it in its package dir structure,
|
||||
# but fpm needs the file to exist before packaging)
|
||||
mkdir -p ./tmp-pkg/etc/default
|
||||
@@ -420,9 +462,9 @@ jobs:
|
||||
--iteration "$RPM_RELEASE" \
|
||||
--architecture "$RPM_ARCH" \
|
||||
--package "$RPM_FILE" \
|
||||
--depends "glibc >= 2.31" \
|
||||
"${depends_args[@]}" \
|
||||
--description "High-performance distributed object storage (${LIBC} build)" \
|
||||
--maintainer "RustFS Team <support@rustfs.com>" \
|
||||
--description "High-performance distributed object storage" \
|
||||
--url "https://rustfs.com" \
|
||||
--license "Apache-2.0" \
|
||||
--before-install <(./scripts/release/package_service_scripts.sh rpm-before-install) \
|
||||
@@ -453,9 +495,9 @@ jobs:
|
||||
echo "✅ RPM built: $RPM_FILE"
|
||||
|
||||
- name: Upload packages to artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: packages-${{ matrix.arch }}
|
||||
name: packages-${{ matrix.arch }}-${{ matrix.libc }}
|
||||
path: |
|
||||
*.deb
|
||||
*.rpm
|
||||
@@ -551,10 +593,11 @@ jobs:
|
||||
for f in "$DEB_FILE" "$RPM_FILE"; do
|
||||
if [[ -n "$f" && -f "$f" ]]; then
|
||||
base="$(basename "$f")"
|
||||
# GitHub stores release asset names with '~' normalized to '.'
|
||||
# (e.g. rustfs_1.0.0~rc.2_amd64.deb is stored as
|
||||
# rustfs_1.0.0.rc.2_amd64.deb), so checksum entries must
|
||||
# reference the name as stored on the release.
|
||||
# GitHub stores release asset names with '~' normalized to '.'.
|
||||
# Current package file names embed the raw tag and contain no
|
||||
# '~', but older releases may still ship such assets, so the
|
||||
# checksum entries must reference the name as stored on the
|
||||
# release.
|
||||
github_base="${base//\~/.}"
|
||||
# Remove any stale entry (both naming variants), then append
|
||||
grep -Fv -- "$base" "$checksum_file" > "${checksum_file}.tmp" || true
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # baseline may be an earlier successful scheduled head
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
- name: Find last successful scheduled baseline
|
||||
id: scheduled_baseline
|
||||
if: github.event_name == 'schedule'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
|
||||
- name: Upload A/B results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: hotpath-warp-ab-${{ github.run_number }}
|
||||
# Includes per-cell median_summary.csv / baseline_compare.csv, both gates,
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
timeout-minutes: 5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ inputs.auto_testing_ref }}
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Upload test logs & evidence
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-fault-tolerance-matrix-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
timeout-minutes: 5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || inputs.auto_testing_ref || 'main' }}
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
- name: Upload test logs & evidence
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-fault-tolerance-${{ github.run_id }}-${{ github.run_attempt }}/fault-tolerance.json
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
manifest: ${{ steps.candidate.outputs.manifest }}
|
||||
performance_ready: ${{ steps.perf_probe.outputs.performance_ready }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Resolve published candidate
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
CHAIN_OUTPUT: ${{ runner.temp }}/chain-candidate.json
|
||||
run: python3 scripts/resolve_functional_candidate.py
|
||||
- name: Retain candidate identity
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-candidate-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-candidate.json
|
||||
@@ -191,13 +191,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download suite evidence
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: functional-chain-*-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-evidence
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
--output "${RUNNER_TEMP}/chain-report.json"
|
||||
- name: Retain chain report regardless of test verdict
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-report-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
--directory "${RUNNER_TEMP}/chain-evidence"
|
||||
--output "${RUNNER_TEMP}/chain-complete.json"
|
||||
- name: Upload complete-chain evidence
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-complete-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-complete.json
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout chain tooling
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
- name: Upload test logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-heal-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -467,7 +467,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-heal-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-heal-${{ github.run_id }}-${{ github.run_attempt }}/heal.json
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-kms-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -414,7 +414,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-kms-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-kms-${{ github.run_id }}-${{ github.run_attempt }}/kms.json
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check performance runner before entering its queue
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout chain tooling
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
|
||||
- name: Upload test logs & results
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-perf-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -349,7 +349,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-performance-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-performance-${{ github.run_id }}-${{ github.run_attempt }}/performance.json
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
RUSTFS_POOL_NODE_ENDPOINTS: ${{ secrets.RUSTFS_POOL_NODE_ENDPOINTS || vars.RUSTFS_POOL_NODE_ENDPOINTS || 'http://rustfs-node1:9000 http://rustfs-node2:9000 http://rustfs-node3:9000 http://rustfs-node4:9000' }}
|
||||
steps:
|
||||
- name: Checkout chain tooling
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -656,7 +656,7 @@ jobs:
|
||||
- name: Upload test logs
|
||||
timeout-minutes: 2
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-pool-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/rustfs-pool-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
@@ -764,7 +764,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-pool-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-pool-${{ github.run_id }}-${{ github.run_attempt }}/pool.json
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-replication-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -419,7 +419,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-replication-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-replication-${{ github.run_id }}-${{ github.run_attempt }}/replication.json
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-s3-compat-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-s3-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-s3-${{ github.run_id }}-${{ github.run_attempt }}/s3.json
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
# 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
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
path: rustfs-repo
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-security-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-security-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-security-${{ github.run_id }}-${{ github.run_attempt }}/security.json
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-storage-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -406,7 +406,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-storage-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-storage-${{ github.run_id }}-${{ github.run_attempt }}/storage.json
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
timeout-minutes: 5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
if [ -n "${PACKAGE_URL}" ]; then
|
||||
ARGS+=(--package-url "${PACKAGE_URL}")
|
||||
elif [ -n "${RUSTFS_VERSION}" ] && [ "${RUSTFS_VERSION}" != "null" ]; then
|
||||
ARGS+=(--package-url "https://github.com/rustfs/rustfs/releases/download/${RUSTFS_VERSION}/rustfs_${RUSTFS_VERSION}_amd64.deb")
|
||||
ARGS+=(--package-url "https://github.com/rustfs/rustfs/releases/download/${RUSTFS_VERSION}/rustfs-linux-x86_64-gnu-v${RUSTFS_VERSION}.deb")
|
||||
else
|
||||
ARGS+=(--package-url "${RUSTFS_NIGHTLY_PACKAGE_URL}")
|
||||
fi
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-table-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-table-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-table-${{ github.run_id }}-${{ github.run_attempt }}/table.json
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout chain tooling
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -460,7 +460,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-tier-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ env.TIER_ARTIFACTS_DIR }}/
|
||||
@@ -734,7 +734,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-tier-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-tier-${{ github.run_id }}-${{ github.run_attempt }}/tier.json
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout repository (for report parser)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
run: python3 scripts/functional_chain_evidence.py consume
|
||||
|
||||
- name: Checkout auto-testing scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/auto-testing
|
||||
ref: ${{ steps.chain.outputs.testing_sha || 'main' }}
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
local version="$1" tag asset url
|
||||
[ -n "${version}" ] && [ "${version}" != "null" ] || return 0
|
||||
tag="${version#v}"
|
||||
asset="rustfs_${tag//-/.}_amd64.deb"
|
||||
asset="rustfs-linux-x86_64-gnu-v${tag}.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
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
- name: Upload report and logs
|
||||
timeout-minutes: 2
|
||||
if: ${{ always() && steps.evidence.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustfs-upgrade-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
@@ -503,7 +503,7 @@ jobs:
|
||||
|
||||
- name: Upload chain evidence
|
||||
if: ${{ always() && steps.chain_record.outputs.written == 'true' }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: functional-chain-upgrade-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/chain-upgrade-${{ github.run_id }}-${{ github.run_attempt }}/upgrade.json
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check latest scheduled runs
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update incomplete-run issue
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.'
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
RUSTFS_TEST_NATS_URL: nats://127.0.0.1:4222
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
|
||||
- name: Upload target integration diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: targets-integration-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: artifacts/targets-live
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Open or update failure-tracking issue
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
|
||||
Copyright 2024-2026 RustFS, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -298,7 +298,11 @@ mod tests {
|
||||
assert_eq!(empty_head_resp.content_disposition(), None);
|
||||
assert_eq!(empty_head_resp.content_encoding(), None);
|
||||
assert_eq!(empty_head_resp.content_language(), None);
|
||||
assert_eq!(empty_head_resp.content_type(), None);
|
||||
assert_eq!(
|
||||
empty_head_resp.content_type(),
|
||||
Some("binary/octet-stream"),
|
||||
"REPLACE without an explicit content type must default to binary/octet-stream, matching S3"
|
||||
);
|
||||
assert_eq!(empty_head_resp.expires_string(), None);
|
||||
assert_eq!(empty_head_resp.website_redirect_location(), None);
|
||||
|
||||
@@ -394,7 +398,15 @@ mod tests {
|
||||
assert_eq!(head.content_disposition(), (field == "content-disposition").then_some("attachment"));
|
||||
assert_eq!(head.content_encoding(), (field == "content-encoding").then_some("gzip"));
|
||||
assert_eq!(head.content_language(), (field == "content-language").then_some("de"));
|
||||
assert_eq!(head.content_type(), (field == "content-type").then_some("text/field"));
|
||||
assert_eq!(
|
||||
head.content_type(),
|
||||
Some(if field == "content-type" {
|
||||
"text/field"
|
||||
} else {
|
||||
"binary/octet-stream"
|
||||
}),
|
||||
"REPLACE without an explicit content type must default to binary/octet-stream, matching S3"
|
||||
);
|
||||
assert_eq!(
|
||||
head.expires_string(),
|
||||
(field == "expires").then_some(replacement_expires_http_date.as_str())
|
||||
@@ -420,7 +432,11 @@ mod tests {
|
||||
.send()
|
||||
.await
|
||||
.expect("HEAD failed for metadata collision case");
|
||||
assert_eq!(collision_head.content_type(), None);
|
||||
assert_eq!(
|
||||
collision_head.content_type(),
|
||||
Some("binary/octet-stream"),
|
||||
"user-metadata under the reserved 'content-type' key must not satisfy the system content-type default"
|
||||
);
|
||||
assert_eq!(collision_head.content_encoding(), None);
|
||||
assert_eq!(
|
||||
collision_head.metadata().and_then(|metadata| metadata.get("content-type")),
|
||||
|
||||
@@ -282,5 +282,9 @@ harness = false
|
||||
name = "single_block_non_inline_benchmark"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "multipart_read_parts_benchmark"
|
||||
harness = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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.
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use rustfs_filemeta::ObjectPartInfo;
|
||||
use std::hint::black_box;
|
||||
use std::time::Duration;
|
||||
|
||||
mod storage_api;
|
||||
use storage_api::multipart::{DiskAPI, DiskOption, Endpoint, new_disk};
|
||||
|
||||
fn bench_multipart_read_parts(c: &mut Criterion) {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("benchmark runtime");
|
||||
let root = tempfile::tempdir().expect("benchmark disk");
|
||||
let mut endpoint = Endpoint::try_from(root.path().to_str().expect("UTF-8 path")).expect("endpoint");
|
||||
endpoint.set_pool_index(0);
|
||||
endpoint.set_set_index(0);
|
||||
endpoint.set_disk_index(0);
|
||||
let disk = runtime
|
||||
.block_on(new_disk(
|
||||
&endpoint,
|
||||
&DiskOption {
|
||||
cleanup: false,
|
||||
health_check: false,
|
||||
},
|
||||
))
|
||||
.expect("local disk");
|
||||
let bucket = "multipart-bench";
|
||||
runtime.block_on(disk.make_volume(bucket)).expect("benchmark volume");
|
||||
let upload = root.path().join(bucket).join("upload");
|
||||
std::fs::create_dir_all(&upload).expect("upload directory");
|
||||
let mut paths = Vec::with_capacity(1024);
|
||||
for number in 1..=1024 {
|
||||
let part = ObjectPartInfo {
|
||||
number,
|
||||
etag: format!("{number:032x}"),
|
||||
size: 1024,
|
||||
actual_size: 1024,
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(upload.join(format!("part.{number}")), b"data").expect("part data");
|
||||
std::fs::write(upload.join(format!("part.{number}.meta")), part.marshal_msg().expect("metadata")).expect("part metadata");
|
||||
paths.push(format!("upload/part.{number}.meta"));
|
||||
}
|
||||
|
||||
// Measures the real DiskAPI path against warm local files. Fixture creation
|
||||
// and correctness checks stay outside the timed region; this is not a cold-disk benchmark.
|
||||
let mut group = c.benchmark_group("multipart_read_parts");
|
||||
group.sample_size(20);
|
||||
group.warm_up_time(Duration::from_secs(1));
|
||||
group.measurement_time(Duration::from_secs(2));
|
||||
for count in [1, 32, 256, 1024] {
|
||||
let paths = &paths[..count];
|
||||
let parts = runtime.block_on(disk.read_parts(bucket, paths)).expect("read parts");
|
||||
assert_eq!(parts.len(), count);
|
||||
assert!(
|
||||
parts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, part)| part.number == i + 1 && part.error.is_none())
|
||||
);
|
||||
group.throughput(Throughput::Elements(u64::try_from(count).expect("part count")));
|
||||
group.bench_with_input(BenchmarkId::from_parameter(count), &paths, |b, paths| {
|
||||
b.iter(|| {
|
||||
black_box(
|
||||
runtime
|
||||
.block_on(disk.read_parts(bucket, black_box(paths)))
|
||||
.expect("read parts"),
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_multipart_read_parts);
|
||||
criterion_main!(benches);
|
||||
@@ -29,3 +29,7 @@ pub(crate) mod erasure {
|
||||
pub(crate) mod single_block_non_inline {
|
||||
pub(crate) use super::{BitrotWriterWrapper, CustomWriter, Erasure};
|
||||
}
|
||||
|
||||
pub(crate) mod multipart {
|
||||
pub(crate) use rustfs_ecstore::api::disk::{DiskAPI, DiskOption, Endpoint, new_disk};
|
||||
}
|
||||
|
||||
@@ -1185,14 +1185,18 @@ mod decommission_lock_order_tests {
|
||||
barrier.wait_until_paused().await;
|
||||
|
||||
let probe_store = Arc::clone(&other_store);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), async move {
|
||||
probe_store
|
||||
.save_current_pool_meta_for_test(&[0])
|
||||
.await
|
||||
.expect("pool metadata mutation probe should commit before UploadPart admission");
|
||||
})
|
||||
let pool_meta_lock = probe_store
|
||||
.new_ns_lock(RUSTFS_META_BUCKET, POOL_META_NAME)
|
||||
.await
|
||||
.expect("create the public UploadPart capacity guard probe");
|
||||
let pool_meta_guard = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
pool_meta_lock.get_write_lock(std::time::Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
.expect("public UploadPart must not hold a capacity read guard during staging");
|
||||
.expect("public UploadPart must not hold a capacity read guard during staging")
|
||||
.expect("public UploadPart capacity guard probe should acquire");
|
||||
drop(pool_meta_guard);
|
||||
|
||||
barrier.release();
|
||||
let part = tokio::time::timeout(std::time::Duration::from_secs(30), put)
|
||||
|
||||
@@ -53,6 +53,7 @@ use crate::disk::{
|
||||
use crate::erasure::coding::{self, bitrot_verify};
|
||||
use crate::runtime::sources as runtime_sources;
|
||||
use bytes::Bytes;
|
||||
use futures::{StreamExt, TryStreamExt, stream};
|
||||
use metrics::counter;
|
||||
#[cfg(target_os = "linux")]
|
||||
use metrics::gauge;
|
||||
@@ -88,6 +89,9 @@ use tokio::time::{Instant, Sleep, interval_at, timeout};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Bound outstanding filesystem jobs and metadata buffers per disk request.
|
||||
const PART_METADATA_READ_CONCURRENCY: usize = 8;
|
||||
|
||||
const DELETED_OBJECTS_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 5);
|
||||
const STALE_TMP_OBJECT_EXPIRY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
@@ -6679,6 +6683,53 @@ impl LocalDisk {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn read_part_metadata(&self, bucket: &str, volume_dir: &Path, path_str: &str) -> Result<ObjectPartInfo> {
|
||||
let path = Path::new(path_str);
|
||||
let num = path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or_default()
|
||||
.strip_prefix("part.")
|
||||
.and_then(|v| v.strip_suffix(".meta"))
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or_default();
|
||||
let data_path = self.io_get_object_path(
|
||||
bucket,
|
||||
&path_join_buf(&[
|
||||
path.parent().unwrap_or_else(|| Path::new("")).to_string_lossy().as_ref(),
|
||||
&format!("part.{num}"),
|
||||
]),
|
||||
)?;
|
||||
let metadata_path = self.io_get_object_path(bucket, path.to_string_lossy().as_ref());
|
||||
// A part's existence check, metadata read and decode share one dispatch.
|
||||
// Keep open errors unmapped for the existing missing-volume fallback.
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<_> {
|
||||
let part_error = |error: String| ObjectPartInfo {
|
||||
number: num,
|
||||
error: Some(error),
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(err) = std::fs::metadata(data_path) {
|
||||
return Ok(Ok(part_error(err.to_string())));
|
||||
}
|
||||
// Invalid metadata paths remain request errors, but missing data wins
|
||||
// first, as it does in the serial reader.
|
||||
let metadata_path = metadata_path?;
|
||||
Ok(read_all_data_std(&metadata_path)
|
||||
.map(|(data, _)| ObjectPartInfo::unmarshal(&data).unwrap_or_else(|err| part_error(err.to_string()))))
|
||||
})
|
||||
.await;
|
||||
let result = match result {
|
||||
Ok(result) => self.resolve_read_all_result(bucket, volume_dir, result?).await,
|
||||
Err(err) => Err(DiskError::from(err)),
|
||||
};
|
||||
Ok(result.unwrap_or_else(|err| ObjectPartInfo {
|
||||
number: num,
|
||||
error: Some(err.to_string()),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
async fn read_listing_metadata(&self, volume: &str, object_name: &str) -> Result<ListingMetadataRead> {
|
||||
let object_dir = self.io_get_object_path(volume, object_name)?;
|
||||
let metadata_path = object_dir.join(STORAGE_FORMAT_FILE);
|
||||
@@ -7615,17 +7666,21 @@ impl LocalDisk {
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
// The part of forward_to below this directory, taken before `current`
|
||||
// loses its trailing slash.
|
||||
let forward_rest = opts
|
||||
.forward_to
|
||||
.as_ref()
|
||||
.and_then(|v| v.strip_prefix(¤t))
|
||||
.map(str::to_owned);
|
||||
let forward = {
|
||||
opts.forward_to
|
||||
.as_ref()
|
||||
.and_then(|v| v.strip_prefix(¤t))
|
||||
.map(|forward| {
|
||||
if let Some(idx) = forward.find('/') {
|
||||
forward[..idx].to_owned()
|
||||
} else {
|
||||
forward.to_owned()
|
||||
}
|
||||
})
|
||||
forward_rest.as_deref().map(|forward| {
|
||||
if let Some(idx) = forward.find('/') {
|
||||
forward[..idx].to_owned()
|
||||
} else {
|
||||
forward.to_owned()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if opts.limit > 0 && *objs_returned >= opts.limit {
|
||||
@@ -7778,13 +7833,22 @@ impl LocalDisk {
|
||||
|
||||
entries.sort();
|
||||
|
||||
if let Some(forward) = &forward {
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
if entry >= forward || forward.starts_with(entry.as_str()) {
|
||||
entries.drain(..i);
|
||||
break;
|
||||
// Every entry left here is a directory. Compare it as the key prefix it
|
||||
// stands for, slash included: "s-x" sorts after "s" as a name, but all
|
||||
// of "s-x/..." sorts before "s/..." ('-' < '/'), so a scan resuming
|
||||
// inside "s/" has to leave "s-x" out even though it comes later.
|
||||
if let Some(forward) = forward_rest.as_deref() {
|
||||
entries.retain(|entry| {
|
||||
if entry.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let key = if entry.ends_with(SLASH_SEPARATOR) {
|
||||
std::borrow::Cow::Borrowed(entry.as_str())
|
||||
} else {
|
||||
std::borrow::Cow::Owned(format!("{entry}{SLASH_SEPARATOR}"))
|
||||
};
|
||||
key.as_ref() >= forward || forward.starts_with(key.as_ref())
|
||||
});
|
||||
}
|
||||
|
||||
let mut dir_stack: Vec<(String, bool, Option<HashSet<String>>, bool)> = Vec::with_capacity(5);
|
||||
@@ -9340,72 +9404,14 @@ impl DiskAPI for LocalDisk {
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn read_parts(&self, bucket: &str, paths: &[String]) -> Result<Vec<ObjectPartInfo>> {
|
||||
let volume_dir = self.io_get_bucket_path(bucket)?;
|
||||
|
||||
let mut ret = vec![ObjectPartInfo::default(); paths.len()];
|
||||
|
||||
for (i, path_str) in paths.iter().enumerate() {
|
||||
let path = Path::new(path_str);
|
||||
let file_name = path.file_name().and_then(|v| v.to_str()).unwrap_or_default();
|
||||
let num = file_name
|
||||
.strip_prefix("part.")
|
||||
.and_then(|v| v.strip_suffix(".meta"))
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Err(err) = access(
|
||||
self.io_get_object_path(
|
||||
bucket,
|
||||
path_join_buf(&[
|
||||
path.parent().unwrap_or_else(|| Path::new("")).to_string_lossy().as_ref(),
|
||||
&format!("part.{num}"),
|
||||
])
|
||||
.as_str(),
|
||||
)?,
|
||||
)
|
||||
stream::iter(0..paths.len())
|
||||
.map(|index| self.read_part_metadata(bucket, &volume_dir, &paths[index]))
|
||||
.buffered(PART_METADATA_READ_CONCURRENCY)
|
||||
.try_fold(Vec::with_capacity(paths.len()), |mut parts, part| async move {
|
||||
parts.push(part);
|
||||
Ok(parts)
|
||||
})
|
||||
.await
|
||||
{
|
||||
ret[i] = ObjectPartInfo {
|
||||
number: num,
|
||||
error: Some(err.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
let data = match self
|
||||
.read_all_data(
|
||||
bucket,
|
||||
volume_dir.clone(),
|
||||
self.io_get_object_path(bucket, path.to_string_lossy().as_ref())?,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
ret[i] = ObjectPartInfo {
|
||||
number: num,
|
||||
error: Some(err.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match ObjectPartInfo::unmarshal(&data) {
|
||||
Ok(meta) => {
|
||||
ret[i] = meta;
|
||||
}
|
||||
Err(err) => {
|
||||
ret[i] = ObjectPartInfo {
|
||||
number: num,
|
||||
error: Some(err.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn check_parts(&self, volume: &str, path: &str, fi: &FileInfo) -> Result<CheckPartsResp> {
|
||||
@@ -12460,6 +12466,79 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_parts_preserves_order_across_concurrency_windows() {
|
||||
let dir = tempfile::tempdir().expect("temporary disk");
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().expect("UTF-8 path")).expect("endpoint");
|
||||
let disk = LocalDisk::new(&endpoint, false).await.expect("local disk");
|
||||
let bucket = "bucket";
|
||||
ensure_test_volume(&disk, bucket).await;
|
||||
let count = PART_METADATA_READ_CONCURRENCY * 3 + 1;
|
||||
let mut paths = Vec::with_capacity(count);
|
||||
let mut expected = Vec::with_capacity(count);
|
||||
for number in (1..=count).rev() {
|
||||
let part = ObjectPartInfo {
|
||||
number,
|
||||
etag: format!("etag-{number}"),
|
||||
size: number,
|
||||
actual_size: i64::try_from(number).expect("small part"),
|
||||
..Default::default()
|
||||
};
|
||||
let data_path = format!("upload/part.{number}");
|
||||
let meta_path = format!("{data_path}.meta");
|
||||
disk.write_all(bucket, &data_path, Bytes::from_static(b"data"))
|
||||
.await
|
||||
.expect("part data");
|
||||
disk.write_all(bucket, &meta_path, Bytes::from(part.marshal_msg().expect("metadata")))
|
||||
.await
|
||||
.expect("part metadata");
|
||||
paths.push(meta_path);
|
||||
expected.push(part);
|
||||
}
|
||||
assert_eq!(disk.read_parts(bucket, &paths).await.expect("read parts"), expected);
|
||||
assert!(disk.read_parts(bucket, &[]).await.expect("empty batch").is_empty());
|
||||
let missing_meta = "upload/part.99.meta".to_owned();
|
||||
disk.write_all(bucket, "upload/part.99", Bytes::from_static(b"data"))
|
||||
.await
|
||||
.expect("data without metadata");
|
||||
paths.insert(PART_METADATA_READ_CONCURRENCY, missing_meta.clone());
|
||||
let parts = disk.read_parts(bucket, &paths).await.expect("per-part failures");
|
||||
let error = disk
|
||||
.read_all_data(
|
||||
bucket,
|
||||
disk.io_get_bucket_path(bucket).expect("bucket path"),
|
||||
disk.io_get_object_path(bucket, &missing_meta).expect("metadata path"),
|
||||
)
|
||||
.await
|
||||
.expect_err("missing metadata");
|
||||
assert_eq!(parts[PART_METADATA_READ_CONCURRENCY].number, 99);
|
||||
assert_eq!(parts[PART_METADATA_READ_CONCURRENCY].error.as_deref(), Some(error.to_string().as_str()));
|
||||
let successful: Vec<_> = parts.into_iter().filter(|part| part.error.is_none()).collect();
|
||||
assert_eq!(successful, expected, "later windows must survive an earlier part error");
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let meta_path = disk
|
||||
.io_get_object_path(bucket, "upload")
|
||||
.expect("upload path")
|
||||
.join("part.100.meta");
|
||||
std::os::unix::fs::symlink(dir.path(), meta_path).expect("invalid metadata symlink");
|
||||
let paths = ["upload/part.100.meta".to_owned()];
|
||||
let parts = disk
|
||||
.read_parts(bucket, &paths)
|
||||
.await
|
||||
.expect("missing data remains a per-part error");
|
||||
assert!(parts[0].error.is_some());
|
||||
disk.write_all(bucket, "upload/part.100", Bytes::from_static(b"data"))
|
||||
.await
|
||||
.expect("data for invalid metadata path");
|
||||
assert_eq!(
|
||||
disk.read_parts(bucket, &paths).await.expect_err("reject metadata symlink"),
|
||||
DiskError::InvalidPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_parts_reports_bad_metadata_and_missing_data_part() {
|
||||
use tempfile::tempdir;
|
||||
@@ -18646,6 +18725,95 @@ mod test {
|
||||
assert_eq!(double_count as usize, double_names.len());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scan_dir_orders_directory_before_its_dash_suffixed_sibling() {
|
||||
use rustfs_filemeta::MetacacheReader;
|
||||
use tempfile::tempdir;
|
||||
|
||||
// "s-x/..." sorts before "s/..." because '-' (0x2d) sorts before '/'
|
||||
// (0x2f), while the directory names alone sort the other way round
|
||||
// ("s" < "s-x"). Backup tools keep "<name>" and "<name>-rollbacks" side
|
||||
// by side this way, which is how flat listings of real buckets ended
|
||||
// early without being reported as truncated.
|
||||
let dir = tempdir().expect("operation should succeed");
|
||||
let bucket = "test-bucket";
|
||||
let bucket_dir = dir.path().join(bucket);
|
||||
|
||||
let mut expected = Vec::new();
|
||||
for parent in ["s", "s-x", "db/backup/s", "db/backup/s-x"] {
|
||||
for n in 0..3 {
|
||||
let name = format!("{parent}/l/l/{n:05}");
|
||||
let object_dir = bucket_dir.join(&name);
|
||||
fs::create_dir_all(&object_dir).await.expect("operation should succeed");
|
||||
fs::write(object_dir.join(STORAGE_FORMAT_FILE), b"meta")
|
||||
.await
|
||||
.expect("operation should succeed");
|
||||
expected.push(name);
|
||||
}
|
||||
}
|
||||
expected.sort();
|
||||
|
||||
let endpoint =
|
||||
Endpoint::try_from(dir.path().to_str().expect("operation should succeed")).expect("operation should succeed");
|
||||
let disk = LocalDisk::new(&endpoint, false).await.expect("operation should succeed");
|
||||
|
||||
async fn scan_names(disk: &LocalDisk, bucket: &str, forward_to: Option<&str>, limit: i32) -> Vec<String> {
|
||||
let (reader, mut writer) = tokio::io::duplex(1 << 16);
|
||||
let mut out = MetacacheWriter::new(&mut writer);
|
||||
let opts = WalkDirOptions {
|
||||
bucket: bucket.to_string(),
|
||||
base_dir: "".to_string(),
|
||||
recursive: true,
|
||||
forward_to: forward_to.map(str::to_string),
|
||||
limit,
|
||||
..Default::default()
|
||||
};
|
||||
let mut objs_returned = 0;
|
||||
|
||||
disk.scan_dir("".to_string(), "".to_string(), &opts, &mut out, &mut objs_returned, false, None)
|
||||
.await
|
||||
.expect("operation should succeed");
|
||||
out.close().await.expect("operation should succeed");
|
||||
drop(out);
|
||||
drop(writer);
|
||||
|
||||
let mut reader = MetacacheReader::new(reader);
|
||||
let entries = reader.read_all().await.expect("operation should succeed");
|
||||
entries
|
||||
.into_iter()
|
||||
.filter(|entry| !entry.metadata.is_empty())
|
||||
.map(|entry| entry.name)
|
||||
.collect()
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
scan_names(&disk, bucket, None, 0).await,
|
||||
expected,
|
||||
"a full recursive scan must emit keys in lexicographic order"
|
||||
);
|
||||
|
||||
for (i, forward_to) in expected.iter().enumerate() {
|
||||
assert_eq!(
|
||||
scan_names(&disk, bucket, Some(forward_to), 0).await,
|
||||
expected[i..].to_vec(),
|
||||
"resuming at {forward_to} must return every later key"
|
||||
);
|
||||
}
|
||||
|
||||
let mut paged = Vec::new();
|
||||
let mut forward_to: Option<String> = None;
|
||||
while paged.len() < expected.len() {
|
||||
let page = scan_names(&disk, bucket, forward_to.as_deref(), 2).await;
|
||||
let page: Vec<String> = page.into_iter().filter(|name| Some(name) != forward_to.as_ref()).collect();
|
||||
if page.is_empty() {
|
||||
break;
|
||||
}
|
||||
forward_to = page.last().cloned();
|
||||
paged.extend(page);
|
||||
}
|
||||
assert_eq!(paged, expected, "paging through the tree must not lose keys");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scan_dir_hidden_delete_markers_do_not_exhaust_limit() {
|
||||
use rustfs_filemeta::MetacacheReader;
|
||||
|
||||
@@ -2490,15 +2490,15 @@ impl SetDisks {
|
||||
part_numbers: &[usize],
|
||||
read_quorum: usize,
|
||||
) -> disk::error::Result<Vec<ObjectPartInfo>> {
|
||||
let bucket = bucket.to_string();
|
||||
let part_meta_paths = part_meta_paths.to_vec();
|
||||
let bucket: Arc<str> = Arc::from(bucket);
|
||||
let part_meta_paths: Arc<[String]> = Arc::from(part_meta_paths);
|
||||
|
||||
let tasks: Vec<_> = disks
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
let disk = disk.clone();
|
||||
let bucket = bucket.clone();
|
||||
let part_meta_paths = part_meta_paths.clone();
|
||||
let bucket = Arc::clone(&bucket);
|
||||
let part_meta_paths = Arc::clone(&part_meta_paths);
|
||||
|
||||
async move {
|
||||
if let Some(disk) = disk {
|
||||
|
||||
@@ -6809,7 +6809,10 @@ pub fn should_heal_object_on_disk(
|
||||
latest_meta: &FileInfo,
|
||||
) -> (bool, bool, Option<DiskError>) {
|
||||
if let Some(err) = err
|
||||
&& (err == &DiskError::FileNotFound || err == &DiskError::FileVersionNotFound || err == &DiskError::FileCorrupt)
|
||||
&& (err == &DiskError::FileNotFound
|
||||
|| err == &DiskError::FileVersionNotFound
|
||||
|| err == &DiskError::FileCorrupt
|
||||
|| err == &DiskError::VolumeNotFound)
|
||||
{
|
||||
return (true, true, Some(err.clone()));
|
||||
}
|
||||
@@ -6818,7 +6821,12 @@ pub fn should_heal_object_on_disk(
|
||||
return (false, false, err.clone());
|
||||
}
|
||||
|
||||
if !meta.equals(latest_meta) {
|
||||
// `FileInfo::equals` intentionally compares the erasure payload shape and
|
||||
// modification time, but it does not compare the selected version or the
|
||||
// delete-marker bit. Heal uses this decision for versioned metadata, so a
|
||||
// stale historical version that is still marked latest must be treated as
|
||||
// outdated even when those storage-level fields happen to match.
|
||||
if !meta.equals(latest_meta) || !heal_metadata_identity_matches(meta, latest_meta) {
|
||||
debug!(
|
||||
event = EVENT_SET_DISK_HEAL,
|
||||
component = LOG_COMPONENT_ECSTORE,
|
||||
@@ -6841,6 +6849,11 @@ pub fn should_heal_object_on_disk(
|
||||
(false, false, None)
|
||||
}
|
||||
|
||||
fn heal_metadata_identity_matches(meta: &FileInfo, latest_meta: &FileInfo) -> bool {
|
||||
meta.deleted == latest_meta.deleted
|
||||
&& meta.version_id.filter(|version| !version.is_nil()) == latest_meta.version_id.filter(|version| !version.is_nil())
|
||||
}
|
||||
|
||||
/// Probe every drive of the set at once. Each live probe is bounded by the
|
||||
/// drive `disk_info` timeout, and the admin peer probe budget only covers one
|
||||
/// such timeout; a sequential walk over several stalled drives after a power
|
||||
@@ -11263,6 +11276,12 @@ mod tests {
|
||||
let (should_heal, _, _) = should_heal_object_on_disk(&err, &[], &meta, &latest_meta);
|
||||
assert!(should_heal);
|
||||
|
||||
let err = Some(DiskError::VolumeNotFound);
|
||||
let (should_heal, is_meta, reason) = should_heal_object_on_disk(&err, &[], &meta, &latest_meta);
|
||||
assert!(should_heal);
|
||||
assert!(is_meta);
|
||||
assert_eq!(reason, Some(DiskError::VolumeNotFound));
|
||||
|
||||
let err = Some(DiskError::FileCorrupt);
|
||||
let (should_heal, is_meta, reason) = should_heal_object_on_disk(&err, &[], &meta, &latest_meta);
|
||||
assert!(should_heal);
|
||||
@@ -11279,6 +11298,29 @@ mod tests {
|
||||
assert_eq!(reason, Some(DiskError::FileCorrupt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_delete_marker_identity_requires_metadata_heal() {
|
||||
let historical_version = Uuid::new_v4();
|
||||
let marker_version = Uuid::new_v4();
|
||||
let mut stale = FileInfo {
|
||||
version_id: Some(historical_version),
|
||||
..FileInfo::default()
|
||||
};
|
||||
let mut latest = stale.clone();
|
||||
stale.deleted = false;
|
||||
latest.deleted = true;
|
||||
latest.version_id = Some(marker_version);
|
||||
|
||||
// The generic equality helper intentionally considers these records
|
||||
// equal when their erasure shape and mod-time match. Heal must still
|
||||
// repair the version identity and delete-marker state.
|
||||
assert!(stale.equals(&latest));
|
||||
let (should_heal, metadata, reason) = should_heal_object_on_disk(&None, &[], &stale, &latest);
|
||||
assert!(should_heal);
|
||||
assert!(metadata);
|
||||
assert_eq!(reason, Some(DiskError::OutdatedXLMeta));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_io_failures_never_authorize_heal_overwrite() {
|
||||
let meta = FileInfo::default();
|
||||
|
||||
@@ -502,6 +502,22 @@ fn heal_writer_error_summary(error: &DiskError) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a writer failure can be retried after the target disk or peer
|
||||
/// becomes available again. Keep this decision typed: the aggregate
|
||||
/// `all_targets_unavailable` path must not turn permanent errors such as
|
||||
/// `DiskFull` into retryable failures.
|
||||
fn is_retryable_heal_writer_error(error: &DiskError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
DiskError::DiskNotFound
|
||||
| DiskError::Timeout
|
||||
| DiskError::SourceStalled
|
||||
| DiskError::FaultyRemoteDisk
|
||||
| DiskError::FaultyDisk
|
||||
| DiskError::RemoteClientUnavailable(_)
|
||||
) || error.is_retryable_internode_write_failure()
|
||||
}
|
||||
|
||||
fn warn_heal_writer_failures(
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
@@ -1398,6 +1414,26 @@ impl SetDisks {
|
||||
));
|
||||
}
|
||||
|
||||
// `errs` is reported in physical-disk order while the
|
||||
// reconstruction loop below uses the shard order from
|
||||
// the selected metadata. Preserve which targets are
|
||||
// missing their bucket volume across that permutation.
|
||||
let mut missing_bucket_volumes = vec![false; out_dated_disks.len()];
|
||||
for (physical_index, error) in errs.iter().enumerate() {
|
||||
if error.as_ref() != Some(&DiskError::VolumeNotFound) {
|
||||
continue;
|
||||
}
|
||||
let shard_index = latest_meta
|
||||
.erasure
|
||||
.distribution
|
||||
.get(physical_index)
|
||||
.and_then(|index| index.checked_sub(1))
|
||||
.unwrap_or(physical_index);
|
||||
if let Some(target) = missing_bucket_volumes.get_mut(shard_index) {
|
||||
*target = true;
|
||||
}
|
||||
}
|
||||
|
||||
out_dated_disks = Self::shuffle_disks(&out_dated_disks, &latest_meta.erasure.distribution);
|
||||
let mut parts_metadata = Self::shuffle_parts_metadata(&parts_metadata, &latest_meta.erasure.distribution);
|
||||
let mut copy_parts_metadata = vec![None; parts_metadata.len()];
|
||||
@@ -1424,6 +1460,58 @@ impl SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
// A missing bucket volume is a repairable object target,
|
||||
// but the final rename cannot create its parent volume.
|
||||
// Recreate only the affected volumes after quorum and
|
||||
// lifecycle checks have passed. Keep failures visible so
|
||||
// a partial repair cannot be reported as successful.
|
||||
let mut volume_creation_error = None;
|
||||
for (index, outdated_disk) in out_dated_disks.iter_mut().enumerate() {
|
||||
if !missing_bucket_volumes.get(index).copied().unwrap_or(false) || outdated_disk.is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(scope) = &bucket_heal_scope {
|
||||
scope.check()?;
|
||||
}
|
||||
let disk = outdated_disk.as_ref().expect("checked above").clone();
|
||||
let endpoint = disk.endpoint().to_string();
|
||||
match disk.make_volume(bucket).await {
|
||||
Ok(()) | Err(DiskError::VolumeExists) => {}
|
||||
Err(error) => {
|
||||
warn!(
|
||||
event = EVENT_SET_DISK_HEAL,
|
||||
component = LOG_COMPONENT_ECSTORE,
|
||||
subsystem = LOG_SUBSYSTEM_HEAL,
|
||||
bucket,
|
||||
object,
|
||||
version_id,
|
||||
disk_index = index,
|
||||
endpoint = %endpoint,
|
||||
error = %error,
|
||||
state = "bucket_volume_recreate_failed",
|
||||
"Heal object could not recreate the target bucket volume"
|
||||
);
|
||||
if volume_creation_error.is_none() {
|
||||
volume_creation_error = Some(error.clone());
|
||||
}
|
||||
*outdated_disk = None;
|
||||
disks_to_heal_count = disks_to_heal_count.saturating_sub(1);
|
||||
for drive in &mut result.after.drives {
|
||||
if drive.endpoint == endpoint {
|
||||
drive.state = heal_drive_state_for_error(&error).to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scope) = &bucket_heal_scope {
|
||||
scope.check()?;
|
||||
}
|
||||
if disks_to_heal_count == 0 {
|
||||
return Ok((result, volume_creation_error.or(Some(DiskError::ErasureWriteQuorum))));
|
||||
}
|
||||
|
||||
// We write at temporary location and then rename to final location.
|
||||
let tmp_id = Uuid::new_v4().to_string();
|
||||
// Delete markers and remote (transitioned) objects carry no data_dir and
|
||||
@@ -1458,6 +1546,8 @@ impl SetDisks {
|
||||
let erasure_info = latest_meta.erasure.clone();
|
||||
let mut writer_failure_count = 0usize;
|
||||
let mut first_writer_failure = None;
|
||||
let mut first_writer_error = None;
|
||||
let mut all_writer_failures_retryable = true;
|
||||
let mut writer_failure_warned = false;
|
||||
|
||||
for (part_index, part) in latest_meta.parts.iter().enumerate() {
|
||||
@@ -1584,9 +1674,11 @@ impl SetDisks {
|
||||
Ok(writer) => writer,
|
||||
Err(err) => {
|
||||
writer_failure_count += 1;
|
||||
all_writer_failures_retryable &= is_retryable_heal_writer_error(&err);
|
||||
if first_writer_failure.is_none() {
|
||||
first_writer_failure =
|
||||
Some((part.number, index, heal_writer_error_summary(&err)));
|
||||
first_writer_error = Some(err);
|
||||
}
|
||||
writers.push(None);
|
||||
continue;
|
||||
@@ -1714,12 +1806,12 @@ impl SetDisks {
|
||||
}
|
||||
// Clean up healed shards written to .rustfs/tmp before bailing (B20).
|
||||
let _ = self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_id).await;
|
||||
return Ok((
|
||||
result,
|
||||
Some(DiskError::other(format!(
|
||||
"all drives had write errors, unable to heal {bucket}/{object}"
|
||||
))),
|
||||
));
|
||||
let error = if all_writer_failures_retryable {
|
||||
first_writer_error.take().unwrap_or(DiskError::FaultyDisk)
|
||||
} else {
|
||||
DiskError::other(format!("all drives had write errors, unable to heal {bucket}/{object}"))
|
||||
};
|
||||
return Ok((result, Some(error)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1865,6 +1957,10 @@ impl SetDisks {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(error) = volume_creation_error {
|
||||
return Ok((result, Some(error)));
|
||||
}
|
||||
|
||||
if result.integrity_verified
|
||||
&& let Err(error) =
|
||||
crate::io_support::shard_integrity::restore_proof_replicas(&latest_meta, &disks, bucket, object)
|
||||
@@ -3430,7 +3526,7 @@ impl SetDisks {
|
||||
mod heal_result_report_tests {
|
||||
use super::{
|
||||
AbsenceProofRequest, DanglingCheckPartsFailure, DanglingDeleteFailure, DanglingDeleteSafety, ReadRepairCommitFingerprint,
|
||||
ReadRepairPauseScope, SetDisks, heal_writer_error_summary,
|
||||
ReadRepairPauseScope, SetDisks, heal_writer_error_summary, is_retryable_heal_writer_error,
|
||||
};
|
||||
use super::{HEAL_RENAME_INCOMPLETE, HealRenameFailureScope, HealWriterFailureScope};
|
||||
use crate::disk::endpoint::Endpoint;
|
||||
@@ -3663,6 +3759,24 @@ mod heal_result_report_tests {
|
||||
assert!(!summary.contains("sensitive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryable_heal_writer_error_keeps_permanent_errors_out() {
|
||||
for error in [
|
||||
DiskError::DiskNotFound,
|
||||
DiskError::Timeout,
|
||||
DiskError::SourceStalled,
|
||||
DiskError::FaultyRemoteDisk,
|
||||
DiskError::FaultyDisk,
|
||||
DiskError::RemoteClientUnavailable("peer restarting".to_string()),
|
||||
] {
|
||||
assert!(is_retryable_heal_writer_error(&error), "{error:?} should be retryable");
|
||||
}
|
||||
|
||||
for error in [DiskError::DiskFull, DiskError::DiskAccessDenied, DiskError::FileCorrupt] {
|
||||
assert!(!is_retryable_heal_writer_error(&error), "{error:?} should remain permanent");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[serial_test::serial]
|
||||
async fn heal_writer_failures_emit_one_aggregate_warning_per_object() {
|
||||
@@ -4223,6 +4337,88 @@ mod heal_result_report_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deep_heal_recreates_missing_bucket_volume_before_rebuilding_shard() {
|
||||
let (temp_dirs, disks, set) = hermetic_set_disks_isolated(4).await;
|
||||
let bucket = "deep-heal-missing-bucket-volume";
|
||||
let object = "object.bin";
|
||||
for disk in &disks {
|
||||
disk.make_volume(bucket).await.expect("bucket volume should be created");
|
||||
}
|
||||
|
||||
let mut reader = PutObjReader::from_vec(vec![0x5c; 1024 * 1024]);
|
||||
set.put_object(
|
||||
bucket,
|
||||
object,
|
||||
&mut reader,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("source object should be written before volume loss");
|
||||
let source = disks[2]
|
||||
.read_version("", bucket, object, "", &ReadOptions::default())
|
||||
.await
|
||||
.expect("source metadata should be readable");
|
||||
let data_dir = source.data_dir.expect("non-inline source should have a data directory");
|
||||
|
||||
disks[1]
|
||||
.delete_volume(bucket, true)
|
||||
.await
|
||||
.expect("target bucket volume should be removable for the regression setup");
|
||||
assert!(matches!(disks[1].stat_volume(bucket).await, Err(DiskError::VolumeNotFound)));
|
||||
|
||||
let (result, error) = set
|
||||
.heal_object(
|
||||
bucket,
|
||||
object,
|
||||
"",
|
||||
&HealOpts {
|
||||
no_lock: true,
|
||||
scan_mode: HealScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("deep heal should finish after bucket volume loss");
|
||||
|
||||
assert!(error.is_none(), "deep heal should recreate the missing bucket volume: {error:?}");
|
||||
assert!(disks[1].stat_volume(bucket).await.is_ok(), "target bucket volume should be restored");
|
||||
let healed = disks[1]
|
||||
.read_version("", bucket, object, "", &ReadOptions::default())
|
||||
.await
|
||||
.expect("healed target metadata should be readable");
|
||||
assert_eq!(healed.data_dir, Some(data_dir));
|
||||
assert_eq!(result.after.drives[1].state, DriveState::Ok.to_string());
|
||||
assert!(
|
||||
temp_dirs[1]
|
||||
.path()
|
||||
.join(bucket)
|
||||
.join(object)
|
||||
.join(data_dir.to_string())
|
||||
.join("part.1")
|
||||
.exists(),
|
||||
"deep heal should rebuild the object shard after recreating its volume"
|
||||
);
|
||||
|
||||
let (_, retry_error) = set
|
||||
.heal_object(
|
||||
bucket,
|
||||
object,
|
||||
"",
|
||||
&HealOpts {
|
||||
no_lock: true,
|
||||
scan_mode: HealScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("repeat heal should complete");
|
||||
assert!(retry_error.is_none(), "repeat heal should be idempotent: {retry_error:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deep_heal_rebuilds_a_stale_current_delete_marker_metadata_replica() {
|
||||
let (_temp_dirs, disks, set) = hermetic_set_disks_isolated(4).await;
|
||||
|
||||
@@ -66,6 +66,7 @@ use crate::disk::DiskOption;
|
||||
use crate::disk::STORAGE_FORMAT_FILE;
|
||||
#[cfg(test)]
|
||||
use crate::disk::new_disk;
|
||||
use crate::erasure::coding::BitrotWriterWrapper;
|
||||
use crate::multipart_listing::paginate_multipart_listing;
|
||||
#[cfg(test)]
|
||||
use crate::object_api::ObjectLockConfigSnapshot;
|
||||
@@ -77,7 +78,7 @@ use crate::storage_api_contracts::multipart::MultipartOperations;
|
||||
#[cfg(test)]
|
||||
use crate::storage_api_contracts::object::HTTPPreconditions;
|
||||
use crate::storage_api_contracts::object::ObjectOperations;
|
||||
use futures::{StreamExt, stream};
|
||||
use futures::{StreamExt, future::join_all, stream};
|
||||
#[cfg(test)]
|
||||
use http::HeaderMap;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
@@ -100,6 +101,49 @@ use std::time::Duration;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
// The erasure-set width bounds fan-out. Await every opener so errors retain
|
||||
// their disk slots and every successful writer remains owned until quorum is checked.
|
||||
async fn create_part_writers(
|
||||
disks: &[Option<DiskStore>],
|
||||
path: &str,
|
||||
length: i64,
|
||||
shard_size: usize,
|
||||
) -> (Vec<Option<BitrotWriterWrapper>>, Vec<Option<DiskError>>) {
|
||||
join_all(disks.iter().map(|disk| async move {
|
||||
let Some(disk) = disk else {
|
||||
return (None, Some(DiskError::DiskNotFound));
|
||||
};
|
||||
match create_bitrot_writer(
|
||||
false,
|
||||
Some(disk),
|
||||
RUSTFS_META_TMP_BUCKET,
|
||||
path,
|
||||
length,
|
||||
shard_size,
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(writer) => (Some(writer), None),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event = EVENT_SET_DISK_MULTIPART,
|
||||
component = LOG_COMPONENT_ECSTORE,
|
||||
subsystem = LOG_SUBSYSTEM_SET_DISK,
|
||||
disk = ?disk,
|
||||
state = "bitrot_writer_skipped",
|
||||
error = ?err,
|
||||
"Set disk multipart bitrot writer skipped"
|
||||
);
|
||||
(None, Some(err))
|
||||
}
|
||||
}
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.unzip()
|
||||
}
|
||||
|
||||
const MULTIPART_LIST_IO_CONCURRENCY: usize = 16;
|
||||
|
||||
static CAPPED_MULTIPART_STAGING: OnceLock<Mutex<HashMap<String, Arc<tokio::sync::Semaphore>>>> = OnceLock::new();
|
||||
@@ -1542,45 +1586,13 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
.map_err(Error::from)?);
|
||||
let writer_setup_stage_start = rustfs_io_metrics::put_stage_metrics_enabled().then(Instant::now);
|
||||
|
||||
let mut writers = Vec::with_capacity(shuffle_disks.len());
|
||||
let mut errors = Vec::with_capacity(shuffle_disks.len());
|
||||
for disk_op in shuffle_disks.iter() {
|
||||
if let Some(disk) = disk_op {
|
||||
let writer = match create_bitrot_writer(
|
||||
false,
|
||||
Some(disk),
|
||||
RUSTFS_META_TMP_BUCKET,
|
||||
&tmp_part_path,
|
||||
erasure.shard_file_size(data.size()),
|
||||
erasure.shard_size(),
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(writer) => writer,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event = EVENT_SET_DISK_MULTIPART,
|
||||
component = LOG_COMPONENT_ECSTORE,
|
||||
subsystem = LOG_SUBSYSTEM_SET_DISK,
|
||||
disk = ?disk,
|
||||
state = "bitrot_writer_skipped",
|
||||
error = ?err,
|
||||
"Set disk multipart bitrot writer skipped"
|
||||
);
|
||||
errors.push(Some(err));
|
||||
writers.push(None);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
writers.push(Some(writer));
|
||||
errors.push(None);
|
||||
} else {
|
||||
errors.push(Some(DiskError::DiskNotFound));
|
||||
writers.push(None);
|
||||
}
|
||||
}
|
||||
let (mut writers, errors) = create_part_writers(
|
||||
&shuffle_disks,
|
||||
&tmp_part_path,
|
||||
erasure.shard_file_size(data.size()),
|
||||
erasure.shard_size(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(stage_start) = writer_setup_stage_start {
|
||||
rustfs_io_metrics::record_put_object_stage_duration(
|
||||
@@ -3594,6 +3606,82 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
|
||||
#[tokio::test]
|
||||
async fn multipart_writer_setup_opens_disks_concurrently_and_preserves_error_slots() {
|
||||
use crate::cluster::rpc::internode_data_transport::{
|
||||
InternodeDataTransport, InternodeDataTransportCapabilities, ReadStreamRequest, WalkDirStreamRequest,
|
||||
WriteStreamRequest,
|
||||
};
|
||||
use crate::cluster::rpc::remote_disk::RemoteDisk;
|
||||
use crate::disk::{Disk, FileReader, FileWriter};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BarrierTransport {
|
||||
barrier: Arc<tokio::sync::Barrier>,
|
||||
fail: bool,
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl InternodeDataTransport for BarrierTransport {
|
||||
async fn open_read(&self, _: ReadStreamRequest) -> disk::error::Result<FileReader> {
|
||||
unreachable!("writer setup must not read")
|
||||
}
|
||||
async fn open_walk_dir(&self, _: WalkDirStreamRequest) -> disk::error::Result<FileReader> {
|
||||
unreachable!("writer setup must not list")
|
||||
}
|
||||
async fn open_write(&self, request: WriteStreamRequest) -> disk::error::Result<FileWriter> {
|
||||
assert_eq!(request.volume, RUSTFS_META_TMP_BUCKET);
|
||||
assert_eq!(request.path, "upload/part.1");
|
||||
self.barrier.wait().await;
|
||||
if self.fail {
|
||||
Err(DiskError::FileAccessDenied)
|
||||
} else {
|
||||
Ok(Box::new(tokio::io::sink()))
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"multipart-writer-test"
|
||||
}
|
||||
fn capabilities(&self) -> InternodeDataTransportCapabilities {
|
||||
InternodeDataTransportCapabilities::tcp_http()
|
||||
}
|
||||
}
|
||||
|
||||
let barrier = Arc::new(tokio::sync::Barrier::new(3));
|
||||
let mut disks = Vec::new();
|
||||
for i in 0..3 {
|
||||
let endpoint = Endpoint {
|
||||
url: url::Url::parse(&format!("http://multipart-test.invalid:9000/disk{i}")).expect("endpoint"),
|
||||
is_local: false,
|
||||
pool_idx: 0,
|
||||
set_idx: 0,
|
||||
disk_idx: i,
|
||||
};
|
||||
let disk = RemoteDisk::new(
|
||||
&endpoint,
|
||||
&DiskOption {
|
||||
cleanup: false,
|
||||
health_check: false,
|
||||
},
|
||||
Arc::new(BarrierTransport {
|
||||
barrier: Arc::clone(&barrier),
|
||||
fail: i == 1,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("remote disk");
|
||||
disks.push(Some(Arc::new(Disk::Remote(Box::new(disk)))));
|
||||
}
|
||||
disks.insert(1, None);
|
||||
// A serial opener cannot cross the barrier. The timeout only bounds failures;
|
||||
// the assertion depends on all three independent openers making progress.
|
||||
let (writers, errors) =
|
||||
tokio::time::timeout(Duration::from_secs(10), create_part_writers(&disks, "upload/part.1", 1024, 256))
|
||||
.await
|
||||
.expect("all disk openers must be polled concurrently");
|
||||
assert_eq!(writers.iter().map(Option::is_some).collect::<Vec<_>>(), [true, false, false, true]);
|
||||
assert_eq!(errors, [None, Some(DiskError::DiskNotFound), Some(DiskError::FileAccessDenied), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_bucket_incarnation_metadata_is_consistent_and_non_nil() {
|
||||
let incarnation = Uuid::new_v4();
|
||||
|
||||
@@ -857,11 +857,10 @@ impl ECStore {
|
||||
opts: &HealOpts,
|
||||
allow_unversioned_absence: bool,
|
||||
) -> Result<HealObjectStorageResult> {
|
||||
if opts.dry_run
|
||||
|| opts.no_lock
|
||||
|| (!allow_unversioned_absence && version_id.is_empty())
|
||||
|| super::utils::is_reserved_or_invalid_bucket(bucket, false)
|
||||
{
|
||||
// Object heal may recreate a missing bucket volume before committing a
|
||||
// shard. Keep the current bucket generation fenced for the whole
|
||||
// operation, including the ordinary unversioned path.
|
||||
if opts.dry_run || opts.no_lock || super::utils::is_reserved_or_invalid_bucket(bucket, false) {
|
||||
let (item, error) = self.handle_heal_object(bucket, object, version_id, opts).await?;
|
||||
return Ok(HealObjectStorageResult {
|
||||
item,
|
||||
|
||||
@@ -143,6 +143,8 @@ impl Error {
|
||||
err,
|
||||
EcstoreError::DiskNotFound
|
||||
| EcstoreError::VolumeNotFound
|
||||
| EcstoreError::FaultyDisk
|
||||
| EcstoreError::FaultyRemoteDisk
|
||||
| EcstoreError::SlowDown
|
||||
| EcstoreError::OperationCanceled
|
||||
| EcstoreError::RemoteClientUnavailable(_)
|
||||
@@ -348,6 +350,8 @@ mod tests {
|
||||
assert!(Error::Disk(DiskError::DiskNotFound).is_recoverable_heal());
|
||||
assert!(Error::Storage(EcstoreError::DiskNotFound).is_recoverable_heal());
|
||||
assert!(Error::Storage(EcstoreError::VolumeNotFound).is_recoverable_heal());
|
||||
assert!(Error::Storage(EcstoreError::FaultyDisk).is_recoverable_heal());
|
||||
assert!(Error::Storage(EcstoreError::FaultyRemoteDisk).is_recoverable_heal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -341,6 +341,8 @@ impl ErasureSetHealer {
|
||||
se,
|
||||
EcstoreError::DiskNotFound
|
||||
| EcstoreError::VolumeNotFound
|
||||
| EcstoreError::FaultyDisk
|
||||
| EcstoreError::FaultyRemoteDisk
|
||||
| EcstoreError::SlowDown
|
||||
| EcstoreError::OperationCanceled
|
||||
)
|
||||
@@ -2055,6 +2057,8 @@ mod tests {
|
||||
fn disk_not_found_is_transient_not_absent() {
|
||||
assert!(matches!(classify(EcstoreError::DiskNotFound), HealObjectOutcome::Transient));
|
||||
assert!(matches!(classify(EcstoreError::VolumeNotFound), HealObjectOutcome::Transient));
|
||||
assert!(matches!(classify(EcstoreError::FaultyDisk), HealObjectOutcome::Transient));
|
||||
assert!(matches!(classify(EcstoreError::FaultyRemoteDisk), HealObjectOutcome::Transient));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -407,6 +407,64 @@ mod canonical_outcome {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn faulty_storage_disks_recover_or_exhaust_without_losing_objects() {
|
||||
for remote in [false, true] {
|
||||
for failures in [1, 4] {
|
||||
let incarnation = Uuid::new_v4();
|
||||
let storage = Arc::new(MockStorage {
|
||||
bucket_incarnation_id: Mutex::new(Some(incarnation)),
|
||||
heal_object_receipts: Mutex::new(HashMap::from([
|
||||
(
|
||||
"object-a".to_string(),
|
||||
VecDeque::from([object_receipt("object-a", None, HealObjectDisposition::Repaired, incarnation)]),
|
||||
),
|
||||
(
|
||||
"object-b".to_string(),
|
||||
VecDeque::from([object_receipt("object-b", None, HealObjectDisposition::Repaired, incarnation)]),
|
||||
),
|
||||
])),
|
||||
..Default::default()
|
||||
});
|
||||
storage.heal_object_outcomes.lock().expect("outcomes").insert(
|
||||
"object-a".to_string(),
|
||||
(0..failures)
|
||||
.map(|_| MockHealObjectOutcome::FaultyStorageDisk(remote))
|
||||
.collect(),
|
||||
);
|
||||
let task = bucket_task(storage.clone());
|
||||
let result = task.execute().await;
|
||||
let outcome = task.get_outcome().await;
|
||||
assert_eq!(outcome.coverage, HealTraversalCoverage::Complete);
|
||||
assert_eq!(outcome.counters.processed, 2);
|
||||
assert_eq!(outcome.counters.attempt_failures, failures);
|
||||
assert_eq!(outcome.counters.unknown, 0);
|
||||
let object = outcome
|
||||
.objects
|
||||
.iter()
|
||||
.find(|item| item.identity.object == "object-a")
|
||||
.expect("object outcome");
|
||||
if failures == 1 {
|
||||
result.expect("faulty disk recovered within retry budget");
|
||||
assert_eq!(outcome.execution, HealExecutionOutcome::Completed);
|
||||
assert_eq!((outcome.counters.healed, outcome.counters.failed), (2, 0));
|
||||
assert_eq!(object.disposition, HealObjectDisposition::Repaired);
|
||||
} else {
|
||||
result.expect_err("persistent faulty disk must fail");
|
||||
assert_eq!(outcome.execution, HealExecutionOutcome::CompletedWithErrors);
|
||||
assert_eq!((outcome.counters.healed, outcome.counters.failed), (1, 1));
|
||||
assert_eq!(object.disposition, HealObjectDisposition::Failed(HealFailureClass::RetryExhausted));
|
||||
}
|
||||
let calls = storage.heal_object_calls.lock().expect("calls");
|
||||
assert_eq!(
|
||||
calls.iter().filter(|name| name.as_str() == "object-a").count(),
|
||||
if failures == 1 { 2 } else { 4 }
|
||||
);
|
||||
assert_eq!(calls.iter().filter(|name| name.as_str() == "object-b").count(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mixed_grace_and_legacy_success_keep_distinct_dispositions() {
|
||||
let storage = Arc::new(MockStorage::default());
|
||||
@@ -1854,6 +1912,7 @@ enum MockHealObjectOutcome {
|
||||
RetiredMarkerDeferred,
|
||||
UnavailableDrive(DriveState),
|
||||
RetryableReadQuorum,
|
||||
FaultyStorageDisk(bool),
|
||||
InternodeHttp(http::StatusCode),
|
||||
RetryableSlowDown,
|
||||
PermanentOther(&'static str),
|
||||
@@ -2016,6 +2075,11 @@ impl HealStorageAPI for MockStorage {
|
||||
))),
|
||||
)),
|
||||
MockHealObjectOutcome::UnavailableDrive(state) => Ok(unavailable_drive_heal_result(state)),
|
||||
MockHealObjectOutcome::FaultyStorageDisk(remote) => Err(Error::Storage(if remote {
|
||||
EcstoreError::FaultyRemoteDisk
|
||||
} else {
|
||||
EcstoreError::FaultyDisk
|
||||
})),
|
||||
MockHealObjectOutcome::InternodeHttp(status) => Err(Error::Storage(EcstoreError::from(DiskError::from(
|
||||
rustfs_rio::new_test_internode_http_io_error(rustfs_rio::InternodeHttpErrorKind::HttpStatus(status)),
|
||||
)))),
|
||||
@@ -2071,6 +2135,11 @@ impl HealStorageAPI for MockStorage {
|
||||
))),
|
||||
)),
|
||||
MockHealObjectOutcome::UnavailableDrive(state) => Ok(unavailable_drive_heal_result(state)),
|
||||
MockHealObjectOutcome::FaultyStorageDisk(remote) => Err(Error::Storage(if remote {
|
||||
EcstoreError::FaultyRemoteDisk
|
||||
} else {
|
||||
EcstoreError::FaultyDisk
|
||||
})),
|
||||
MockHealObjectOutcome::OkWithOtherError(message) => Ok((HealResultItem::default(), Some(Error::other(message)))),
|
||||
MockHealObjectOutcome::ErrOther(message) | MockHealObjectOutcome::PermanentOther(message) => {
|
||||
Err(Error::other(message))
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
## Open Items
|
||||
|
||||
- `connect-894` pending Connect heartbeats: replay the exact preceding producer-capability list, with or without jobs, when upgrading to the memory-service capability. Preserve request ID, sequence, and persisted body rather than inserting the new capability into a retry. Remove after upgrades from the pre-service-memory capability set are unsupported.
|
||||
|
||||
- `backlog-2539` administrator erasure-set scope: decode historical pending intents whose empty bucket list omitted the all-buckets marker. Normalize that representation to the explicit scope before replay or checkpoint comparison. Remove after all pre-marker administrator ErasureSet intents are retired; deployment verification must confirm that no such pending records remain on coordinator disks.
|
||||
|
||||
- `backlog-2519` retained admin heal reports: keep the schema-1 terminal as the commit and replay fence, with a bounded versioned report in a separate namespace on the same disk. Older rollback readers can still query the terminal and suppress replay, but cannot expose its outcome; newer readers mark missing outcomes as unavailable. Remove the legacy marker and missing-report adapter only after all supported direct-upgrade and rollback readers understand the report format and retained schema-1-only receipts have expired.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
975c1ca53eefeef6766a6fc0b3d3281f7408255342b0686e5e2aee5ad055414c duplicate.json
|
||||
963529a38a02849c6c2acc6d72668dca9f63218b49c89fae41a451b584850411 overflow.json
|
||||
6afec3806c98fb55d477455b43935798d8fdb7f9ae39bd9f6637465539e5c02b producer-capabilities.json
|
||||
60300d053dd3746a3aceb327315e8c5abe292fb6505dd02a64b8d6279fb56c09 producer-capabilities.json
|
||||
e3adeee1c8a19aa17e70894896fb79c072e3785bea3611b93c11e79f039ed5af stale.json
|
||||
22cc7337f545271ebf11ca6b76e7e479d62c51278ac001a8d870b07d0fa9884e unknown.json
|
||||
b06a72ff1d964efe65e996797705ec2ef72574a813aec5d8e9aff26c899f1352 valid.json
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"logs.capture@1",
|
||||
"profile.cpu@1",
|
||||
"profile.memory@1",
|
||||
"profile.memory.service@1",
|
||||
"profile.threads@1",
|
||||
"telemetry.record@1",
|
||||
"telemetry.otlp@1",
|
||||
@@ -40,6 +41,7 @@
|
||||
"logs.capture@1",
|
||||
"profile.cpu@1",
|
||||
"profile.memory@1",
|
||||
"profile.memory.service@1",
|
||||
"profile.threads@1",
|
||||
"telemetry.record@1",
|
||||
"telemetry.otlp@1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
a499742c06046e9cf2f781fc17d8b4cf95e018ab45fa41ee5944ba15b0e2902c auth
|
||||
9b54e39584a442270ae486e8378df1d6f168c45d0c59baba9459b46bb212d2df version
|
||||
159d116e965b51771f1687fc6ec9bdfbf2a4a0e6d6aa26d3f1046b3dac2697dd registration
|
||||
fe1287eac16832bed88320254245ba07a985e6ae7e66e1d1570e324ffa972873 heartbeat
|
||||
9b3862f5160968a2ff281afb3ea519a3d49ae1bc3d767153d70f309d2a7457a7 heartbeat
|
||||
cc6c3f3bf5e6a938f2e00b8ce42d8fa7bfb974f4eb2927101d4ede4ebdc10dd5 inventory
|
||||
0207458ec9b97b368e2f347da112511a3aabd27d752890cacda4fa2408e7f19e object-performance
|
||||
a464d4c695d76dd985c0f2abf68c2135c080221098ad2427022f85243a7bf6ae network-performance
|
||||
|
||||
@@ -607,9 +607,16 @@ impl DefaultObjectUsecase {
|
||||
// Handle MetadataDirective REPLACE: replace user metadata while preserving system metadata.
|
||||
// System metadata (compression, encryption) is added after this block to ensure
|
||||
// it's not cleared by the REPLACE operation.
|
||||
if let Some(replacement_metadata) = replacement_metadata {
|
||||
if let Some(mut replacement_metadata) = replacement_metadata {
|
||||
// S3 defaults an object's content type to `binary/octet-stream` when none is
|
||||
// supplied. REPLACE must apply that same default instead of leaving
|
||||
// content-type unset.
|
||||
if !replacement_metadata.contains_key("content-type") {
|
||||
replacement_metadata.insert("content-type".to_string(), "binary/octet-stream".to_string());
|
||||
}
|
||||
let effective_content_type = replacement_metadata.get("content-type").cloned();
|
||||
user_defined = replacement_metadata;
|
||||
src_info.content_type = content_type.clone();
|
||||
src_info.content_type = effective_content_type;
|
||||
src_info.content_encoding = content_encoding.as_deref().and_then(normalize_content_encoding_for_storage);
|
||||
src_info.expires = expires_timestamp.map(OffsetDateTime::from);
|
||||
} else if metadata_directive.is_some() || website_redirect_location.is_some() {
|
||||
|
||||
@@ -29,6 +29,7 @@ use thiserror::Error;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::{Uuid, Variant, Version};
|
||||
|
||||
use super::profile_memory::MEMORY_PROFILE_SERVICE_CAPABILITY;
|
||||
use super::{
|
||||
CPU_PROFILE_CAPABILITY, DRIVE_CAPABILITY, DRIVE_SCHEMA_VERSION, DriveOutcome, DrivePerformanceError, DrivePerformanceRequest,
|
||||
DriveProvenance, LocalDriveConsent, LocalNetworkConsent, LocalProfileConsent, LocalTopConsent, MAX_NETWORK_TRAFFIC_BYTES,
|
||||
@@ -45,6 +46,7 @@ use crate::connect::DeviceIdentity;
|
||||
const PROTOCOL_VERSION: &str = "v1";
|
||||
const PROFILE_CPU_JOB_TYPE: &str = "profile.cpu";
|
||||
const PROFILE_THREADS_JOB_TYPE: &str = "profile.threads";
|
||||
const PROFILE_MEMORY_JOB_TYPE: &str = "profile.memory";
|
||||
const PERFORMANCE_DRIVE_JOB_TYPE: &str = "performance.drive";
|
||||
const PERFORMANCE_NETWORK_JOB_TYPE: &str = "performance.network";
|
||||
const TOP_API_JOB_TYPE: &str = "top.api";
|
||||
@@ -56,6 +58,7 @@ const MAX_FUTURE_SKEW_SECONDS: i64 = 300;
|
||||
const MAX_OUTPUT_BYTES: u64 = 524_288;
|
||||
const MAX_MEMORY_BYTES: u64 = 64 * 1024 * 1024;
|
||||
const MAX_CPU_MILLIS: u64 = 30_000;
|
||||
const MAX_MEMORY_PROFILE_CPU_MILLIS: u64 = 5_000;
|
||||
const MAX_NETWORK_CPU_MILLIS: u64 = 5_000;
|
||||
const MIN_NETWORK_MEMORY_BYTES: u64 = 1_048_576;
|
||||
const DRIVE_TARGET_ALIAS: &str = "drive-1";
|
||||
@@ -75,6 +78,7 @@ const MIN_TOP_RPC_MEMORY_BYTES: u64 = 1_048_576;
|
||||
enum DiagnosticJobKind {
|
||||
ProfileCpu,
|
||||
ProfileThreads,
|
||||
ProfileMemory,
|
||||
PerformanceDrive,
|
||||
PerformanceNetwork,
|
||||
TopApi,
|
||||
@@ -424,6 +428,11 @@ impl DiagnosticJobEnvelope {
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
if kind == DiagnosticJobKind::ProfileMemory
|
||||
&& (self.limits.max_cpu_millis > MAX_MEMORY_PROFILE_CPU_MILLIS || self.limits.max_memory_bytes != MAX_MEMORY_BYTES)
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
match (kind, self.parameters.traffic_bytes) {
|
||||
(DiagnosticJobKind::PerformanceNetwork, Some(1..=MAX_NETWORK_TRAFFIC_BYTES)) => {
|
||||
if self.limits.max_cpu_millis > MAX_NETWORK_CPU_MILLIS || self.limits.max_memory_bytes < MIN_NETWORK_MEMORY_BYTES
|
||||
@@ -478,6 +487,11 @@ impl DiagnosticJobEnvelope {
|
||||
(PROFILE_THREADS_JOB_TYPE, [capability], PROFILE_SCHEMA_VERSION) if capability == THREAD_PROFILE_CAPABILITY => {
|
||||
Ok(DiagnosticJobKind::ProfileThreads)
|
||||
}
|
||||
(PROFILE_MEMORY_JOB_TYPE, [capability], PROFILE_SCHEMA_VERSION)
|
||||
if capability == MEMORY_PROFILE_SERVICE_CAPABILITY =>
|
||||
{
|
||||
Ok(DiagnosticJobKind::ProfileMemory)
|
||||
}
|
||||
(PERFORMANCE_DRIVE_JOB_TYPE, [capability], DRIVE_SCHEMA_VERSION) if capability == DRIVE_CAPABILITY => {
|
||||
Ok(DiagnosticJobKind::PerformanceDrive)
|
||||
}
|
||||
@@ -518,6 +532,7 @@ pub async fn execute_diagnostic_job(
|
||||
match envelope.kind()? {
|
||||
DiagnosticJobKind::ProfileCpu => execute_profile_cpu_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::ProfileThreads => execute_profile_threads_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::ProfileMemory => execute_profile_memory_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::PerformanceDrive => {
|
||||
let Some(scratch_root) = runtime_drive_scratch_root() else {
|
||||
return Ok(failed_drive_execution(&envelope.job_id, "SOURCE_UNAVAILABLE"));
|
||||
@@ -742,6 +757,54 @@ async fn execute_performance_network_job(
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_profile_memory_job(
|
||||
envelope: DiagnosticJobEnvelope,
|
||||
nonce: [u8; 32],
|
||||
identity: &DeviceIdentity,
|
||||
provenance: ProfileProvenance,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<DiagnosticJobExecution, DiagnosticJobError> {
|
||||
let expire = parse_time(&envelope.expire_time)?;
|
||||
let consent_expire = parse_time(&envelope.parameters.consent_expires_at)?;
|
||||
let request = ProfileCaptureRequest {
|
||||
organization_name: envelope.organization_name,
|
||||
cluster_name: envelope.cluster_name,
|
||||
device_name: envelope.device_name,
|
||||
run_uid: envelope.job_id.clone(),
|
||||
artifact_uid: envelope.parameters.artifact_uid,
|
||||
schema_version: envelope.schema_version,
|
||||
// The service capability gates dispatch; schema-v1 exports retain the
|
||||
// original allocation-aggregate capability and signed artifact format.
|
||||
capability: super::MEMORY_PROFILE_CAPABILITY.to_owned(),
|
||||
consent: LocalProfileConsent {
|
||||
consent_uid: envelope.parameters.consent_uid,
|
||||
policy_revision: envelope.parameters.consent_policy_revision,
|
||||
expires_at_unix: consent_expire.timestamp(),
|
||||
confirmed: true,
|
||||
},
|
||||
produced_at_unix: Utc::now().timestamp(),
|
||||
expires_at_unix: expire.timestamp(),
|
||||
nonce,
|
||||
duration: Duration::from_millis(envelope.parameters.duration_millis),
|
||||
sample_period: Duration::from_micros(envelope.parameters.sample_period_micros),
|
||||
provenance,
|
||||
};
|
||||
let export = super::export_memory_profile(&request, identity, cancel)
|
||||
.await
|
||||
.map_err(capture_failure)?;
|
||||
if export.archive_bytes.len() > usize::try_from(envelope.limits.max_output_bytes).unwrap_or(usize::MAX) {
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
Ok(DiagnosticJobExecution {
|
||||
job_id: envelope.job_id,
|
||||
outcome: "SUCCEEDED".to_owned(),
|
||||
reason: "COMPLETE".to_owned(),
|
||||
artifact_uid: Some(export.artifact_uid),
|
||||
artifact_sha256: Some(export.archive_sha256),
|
||||
artifact_bytes: Some(export.archive_bytes),
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_profile_threads_job(
|
||||
envelope: DiagnosticJobEnvelope,
|
||||
nonce: [u8; 32],
|
||||
@@ -1220,6 +1283,7 @@ mod tests {
|
||||
("logs.capture@1", &["logs"][..]),
|
||||
("profile.cpu@1", &["profile"][..]),
|
||||
("profile.memory@1", &["profile"][..]),
|
||||
("profile.memory.service@1", &[][..]),
|
||||
("profile.threads@1", &["profile"][..]),
|
||||
("telemetry.record@1", &["telemetry", "record"][..]),
|
||||
("telemetry.otlp@1", &["telemetry", "otlp"][..]),
|
||||
@@ -1240,13 +1304,16 @@ mod tests {
|
||||
let connect = command.find_subcommand("connect").expect("connect command");
|
||||
for (capability, path) in expected {
|
||||
if path.is_empty() {
|
||||
// Network probes use the authenticated service dispatcher and
|
||||
// locally resolved peers, not a standalone CLI command.
|
||||
let mut job = envelope();
|
||||
job.job_type = PERFORMANCE_NETWORK_JOB_TYPE.to_owned();
|
||||
let (job_type, kind) = match capability {
|
||||
NETWORK_CAPABILITY => (PERFORMANCE_NETWORK_JOB_TYPE, DiagnosticJobKind::PerformanceNetwork),
|
||||
MEMORY_PROFILE_SERVICE_CAPABILITY => (PROFILE_MEMORY_JOB_TYPE, DiagnosticJobKind::ProfileMemory),
|
||||
_ => panic!("{capability} is missing service dispatch"),
|
||||
};
|
||||
job.job_type = job_type.to_owned();
|
||||
job.required_capabilities = vec![capability.to_owned()];
|
||||
job.schema_version = NETWORK_SCHEMA_VERSION;
|
||||
assert_eq!(job.kind(), Ok(DiagnosticJobKind::PerformanceNetwork));
|
||||
assert_eq!(job.kind(), Ok(kind));
|
||||
continue;
|
||||
}
|
||||
let mut command = connect;
|
||||
|
||||
@@ -48,6 +48,7 @@ pub const CONNECT_DIAGNOSTIC_CAPABILITIES: &[&str] = &[
|
||||
logs::LOGS_CAPABILITY,
|
||||
profile_cpu::CPU_PROFILE_CAPABILITY,
|
||||
profile_cpu::MEMORY_PROFILE_CAPABILITY,
|
||||
profile_memory::MEMORY_PROFILE_SERVICE_CAPABILITY,
|
||||
profile_cpu::THREAD_PROFILE_CAPABILITY,
|
||||
trace_record::TELEMETRY_RECORD_CAPABILITY,
|
||||
trace_record::TELEMETRY_OTLP_CAPABILITY,
|
||||
|
||||
@@ -34,6 +34,9 @@ use super::profile_cpu::{
|
||||
|
||||
const MAX_ALLOCATOR_STATS_BYTES: usize = 262_144;
|
||||
|
||||
/// Service-job negotiation is separate from the unchanged memory result schema.
|
||||
pub const MEMORY_PROFILE_SERVICE_CAPABILITY: &str = "profile.memory.service@1";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct AllocationSnapshot {
|
||||
total_allocated_bytes: u64,
|
||||
|
||||
@@ -380,11 +380,20 @@ async fn run_collection_schedule(
|
||||
let _ = receipts.send(state.last_receipt.clone());
|
||||
}
|
||||
|
||||
// The runtime's initial stopped value is not a received revocation. Keep the
|
||||
// durable interval fenced until heartbeat supplies a policy to evaluate.
|
||||
while *policies.borrow() == DiagnosticCollectionPolicy::stopped() {
|
||||
let _ = status.send(DiagnosticScheduleStatus::Waiting);
|
||||
if wait_for_policy(policies, shutdown).await {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if shutdown.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
let policy = policies.borrow().clone();
|
||||
let policy = policies.borrow_and_update().clone();
|
||||
if let Err(error) = policy.validate()
|
||||
&& policy.should_run()
|
||||
{
|
||||
@@ -692,6 +701,291 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bootstrap_stopped_policy_preserves_a_due_interval_until_fresh_policy_arrives() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let active = policy(7, true);
|
||||
let due = (Utc::now() + chrono::Duration::seconds(300)).to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let previous = receipt(
|
||||
7,
|
||||
ENVIRONMENT_TOOL_ID,
|
||||
now_string(),
|
||||
ReceiptOutcome::Succeeded,
|
||||
1,
|
||||
None,
|
||||
Some(b"previous"),
|
||||
);
|
||||
store
|
||||
.write(ScheduleState {
|
||||
policy_revision: Some(active.revision),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(due.clone()),
|
||||
active_interval_started_at: None,
|
||||
last_receipt: Some(previous.clone()),
|
||||
})
|
||||
.await
|
||||
.expect("persist scheduled interval");
|
||||
let (policy_tx, policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
let shutdown = CancellationToken::new();
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), blocking_runner());
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.expect("bootstrap waiting timeout")
|
||||
.expect("bootstrap waiting notification");
|
||||
assert!(matches!(*runtime.status.borrow(), DiagnosticScheduleStatus::Waiting));
|
||||
let persisted = store.read().await.expect("bootstrap state");
|
||||
assert_eq!(persisted.next_due_at.as_deref(), Some(due.as_str()));
|
||||
assert_eq!(persisted.last_receipt, Some(previous.clone()));
|
||||
assert_eq!(*runtime.receipts.borrow(), Some(previous.clone()));
|
||||
|
||||
policy_tx
|
||||
.send(DiagnosticCollectionPolicy::stopped())
|
||||
.expect("bootstrap still waiting");
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.expect("repeated bootstrap timeout")
|
||||
.expect("repeated bootstrap notification");
|
||||
assert_eq!(store.read().await.expect("still scheduled").next_due_at.as_deref(), Some(due.as_str()));
|
||||
|
||||
policy_tx.send(active).expect("fresh unchanged policy");
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(!runtime.task.is_finished(), "unchanged policy must not kill the scheduler");
|
||||
let persisted = store.read().await.expect("resumed state");
|
||||
assert_eq!(persisted.next_due_at.as_deref(), Some(due.as_str()));
|
||||
assert_eq!(persisted.last_receipt, Some(previous));
|
||||
|
||||
policy_tx.send(policy(8, true)).expect("new revision");
|
||||
wait_running(&mut runtime.status).await;
|
||||
assert!(matches!(
|
||||
*runtime.status.borrow(),
|
||||
DiagnosticScheduleStatus::Running { policy_revision: 8, .. }
|
||||
));
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn overdue_restart_waits_for_fresh_policy_and_keeps_the_original_interval() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let active = policy(7, true);
|
||||
let due = Utc::now() - chrono::Duration::seconds(10);
|
||||
let due_string = due.to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
store
|
||||
.write(ScheduleState {
|
||||
policy_revision: Some(7),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(due_string.clone()),
|
||||
..ScheduleState::default()
|
||||
})
|
||||
.await
|
||||
.expect("persist overdue interval");
|
||||
let (policy_tx, policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
let shutdown = CancellationToken::new();
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), blocking_runner());
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.expect("bootstrap timeout")
|
||||
.expect("waiting");
|
||||
assert!(matches!(*runtime.status.borrow(), DiagnosticScheduleStatus::Waiting));
|
||||
let before = store.read().await.expect("before authorization");
|
||||
assert_eq!(before.next_due_at, Some(due_string.clone()));
|
||||
assert!(before.active_interval_started_at.is_none());
|
||||
assert!(before.last_receipt.is_none());
|
||||
|
||||
policy_tx.send(active).expect("fresh policy");
|
||||
wait_running(&mut runtime.status).await;
|
||||
let running = store.read().await.expect("running original interval");
|
||||
assert_eq!(running.active_interval_started_at, Some(due_string.clone()));
|
||||
assert_eq!(
|
||||
running.next_due_at,
|
||||
Some((due + chrono::Duration::seconds(300)).to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||
);
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
let completed = store.read().await.expect("cancelled interval").last_receipt.expect("receipt");
|
||||
assert_eq!(completed.interval_started_at, due_string);
|
||||
assert_eq!(completed.outcome, ReceiptOutcome::Cancelled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fresh_policy_received_before_startup_does_not_cancel_its_own_overdue_interval() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let active = policy(7, true);
|
||||
let due = Utc::now() - chrono::Duration::seconds(10);
|
||||
let due_string = due.to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
store
|
||||
.write(ScheduleState {
|
||||
policy_revision: Some(7),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(due_string.clone()),
|
||||
..ScheduleState::default()
|
||||
})
|
||||
.await
|
||||
.expect("persist overdue interval");
|
||||
let (policy_tx, policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
policy_tx.send(active).expect("heartbeat arrives before state read completes");
|
||||
let shutdown = CancellationToken::new();
|
||||
let runner: Runner = Arc::new(|_| Box::pin(async { Ok(b"collected".to_vec()) }));
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), runner);
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.receipts.changed())
|
||||
.await
|
||||
.expect("collection timeout")
|
||||
.expect("receipt");
|
||||
let receipt = runtime.receipts.borrow().clone().expect("collection receipt");
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
assert_eq!(receipt.outcome, ReceiptOutcome::Succeeded);
|
||||
assert_eq!(receipt.interval_started_at, due_string);
|
||||
assert_eq!(receipt.attempt_count, 1);
|
||||
assert_eq!(receipt.result_bytes, Some(b"collected".len()));
|
||||
let persisted = store.read().await.expect("completed interval");
|
||||
assert_eq!(
|
||||
persisted.next_due_at,
|
||||
Some((due + chrono::Duration::seconds(300)).to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||
);
|
||||
assert_eq!(persisted.last_receipt, Some(receipt));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restart_rejects_expired_policy_and_corrupt_same_revision_state() {
|
||||
for invalid in ["expired policy", "missing due", "invalid due", "fingerprint mismatch"] {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let mut active = policy(7, true);
|
||||
if invalid == "expired policy" {
|
||||
active.consent_expires_at = Some("2020-01-01T00:00:00Z".to_owned());
|
||||
}
|
||||
let mut state = ScheduleState {
|
||||
policy_revision: Some(7),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(now_string()),
|
||||
..ScheduleState::default()
|
||||
};
|
||||
match invalid {
|
||||
"missing due" => state.next_due_at = None,
|
||||
"invalid due" => state.next_due_at = Some("not-a-timestamp".to_owned()),
|
||||
"fingerprint mismatch" => state.policy_fingerprint = Some("different-policy".to_owned()),
|
||||
_ => {}
|
||||
}
|
||||
store.write(state).await.expect("persist invalid input");
|
||||
let (policy_tx, mut policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
let shutdown = CancellationToken::new();
|
||||
let runner = blocking_runner();
|
||||
let (status_tx, mut status_rx) = watch::channel(DiagnosticScheduleStatus::Waiting);
|
||||
let (receipt_tx, _receipt_rx) = watch::channel(None);
|
||||
let schedule = run_collection_schedule(&store, &mut policy_rx, &shutdown, &runner, &status_tx, &receipt_tx);
|
||||
tokio::pin!(schedule);
|
||||
tokio::select! {
|
||||
result = &mut schedule => panic!("scheduler ended before fresh policy: {result:?}"),
|
||||
result = tokio::time::timeout(Duration::from_secs(2), status_rx.changed()) => {
|
||||
result.expect("bootstrap timeout").expect("waiting");
|
||||
}
|
||||
}
|
||||
policy_tx.send(active).expect("fresh invalid policy");
|
||||
let result = tokio::time::timeout(Duration::from_secs(2), schedule)
|
||||
.await
|
||||
.expect("invalid input must fail closed");
|
||||
match invalid {
|
||||
"expired policy" | "invalid due" => {
|
||||
assert!(matches!(result, Err(DiagnosticScheduleError::Policy)), "{invalid}: {result:?}")
|
||||
}
|
||||
_ => assert!(matches!(result, Err(DiagnosticScheduleError::StateCorrupt)), "{invalid}: {result:?}"),
|
||||
}
|
||||
let persisted = store.read().await.expect("rejected state");
|
||||
assert!(persisted.active_interval_started_at.is_none());
|
||||
assert!(persisted.last_receipt.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fresh_disabled_or_revoked_policy_clears_due_before_a_new_revision_can_resume() {
|
||||
for reason in [ReasonCode::Disabled, ReasonCode::ConsentInactive] {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let active = policy(7, true);
|
||||
store
|
||||
.write(ScheduleState {
|
||||
policy_revision: Some(7),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(now_string()),
|
||||
..ScheduleState::default()
|
||||
})
|
||||
.await
|
||||
.expect("persist interval");
|
||||
let (policy_tx, policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
let shutdown = CancellationToken::new();
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), blocking_runner());
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.expect("bootstrap timeout")
|
||||
.expect("waiting");
|
||||
let mut stopped = policy(8, false);
|
||||
stopped.reason_code = Some(reason);
|
||||
policy_tx.send(stopped).expect("fresh stop");
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.status.changed())
|
||||
.await
|
||||
.expect("stop timeout")
|
||||
.expect("stopped policy processed");
|
||||
assert!(matches!(*runtime.status.borrow(), DiagnosticScheduleStatus::Waiting));
|
||||
let stopped = store.read().await.expect("revoked state");
|
||||
assert!(stopped.next_due_at.is_none());
|
||||
assert!(stopped.active_interval_started_at.is_none());
|
||||
assert!(stopped.last_receipt.is_none());
|
||||
policy_tx.send(policy(9, true)).expect("new authorization");
|
||||
wait_running(&mut runtime.status).await;
|
||||
assert!(matches!(
|
||||
*runtime.status.borrow(),
|
||||
DiagnosticScheduleStatus::Running { policy_revision: 9, .. }
|
||||
));
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupted_interval_is_reported_without_collecting_before_fresh_policy() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let active = policy(7, true);
|
||||
let started = now_string();
|
||||
let next = (Utc::now() + chrono::Duration::seconds(300)).to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
store
|
||||
.write(ScheduleState {
|
||||
policy_revision: Some(7),
|
||||
policy_fingerprint: Some(active.fingerprint().expect("fingerprint")),
|
||||
next_due_at: Some(next.clone()),
|
||||
active_interval_started_at: Some(started.clone()),
|
||||
last_receipt: None,
|
||||
})
|
||||
.await
|
||||
.expect("persist interrupted interval");
|
||||
let (_policy_tx, policy_rx) = watch::channel(DiagnosticCollectionPolicy::stopped());
|
||||
let shutdown = CancellationToken::new();
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), blocking_runner());
|
||||
tokio::time::timeout(Duration::from_secs(2), runtime.receipts.changed())
|
||||
.await
|
||||
.expect("recovery timeout")
|
||||
.expect("crash receipt");
|
||||
let receipt = runtime.receipts.borrow().clone().expect("recovery receipt");
|
||||
assert_eq!(receipt.policy_revision, 7);
|
||||
assert_eq!(receipt.interval_started_at, started);
|
||||
assert_eq!(receipt.outcome, ReceiptOutcome::Failed);
|
||||
assert_eq!(receipt.reason.as_deref(), Some("producer_restarted_during_collection"));
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
let persisted = store.read().await.expect("recovered state");
|
||||
assert!(persisted.active_interval_started_at.is_none());
|
||||
assert_eq!(persisted.next_due_at, Some(next));
|
||||
assert_eq!(persisted.last_receipt, Some(receipt));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disable_cancels_an_active_run_and_persists_receipt() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -111,7 +111,15 @@ impl PendingHeartbeat {
|
||||
super::diagnostics::CPU_PROFILE_CAPABILITY,
|
||||
]
|
||||
|| self.capabilities == heartbeat_capabilities(false)
|
||||
|| self.capabilities == heartbeat_capabilities(true))
|
||||
|| self.capabilities == heartbeat_capabilities(true)
|
||||
// RUSTFS_COMPAT_TODO(connect-894) Remove after upgrades from the pre-service-memory capability set are unsupported.
|
||||
// Preserve exact pending requests; never add the capability to a retry.
|
||||
|| [false, true].into_iter().any(|job_capable| {
|
||||
heartbeat_capabilities(job_capable)
|
||||
.iter()
|
||||
.filter(|capability| capability.as_str() != "profile.memory.service@1")
|
||||
.eq(self.capabilities.iter())
|
||||
}))
|
||||
&& self.sequence <= MAX_SEQUENCE
|
||||
&& self.coarse_node_summary.is_valid()
|
||||
&& is_exact_utc_seconds(&self.client_time)
|
||||
|
||||
@@ -544,6 +544,7 @@ async fn sends_only_l0_fields_and_accepts_additive_response_fields() {
|
||||
"logs.capture@1",
|
||||
"profile.cpu@1",
|
||||
"profile.memory@1",
|
||||
"profile.memory.service@1",
|
||||
"profile.threads@1",
|
||||
"telemetry.record@1",
|
||||
"telemetry.otlp@1",
|
||||
@@ -642,6 +643,116 @@ async fn restart_replays_pending_request_then_advances_sequence() {
|
||||
assert_eq!(seen[1]["sequence"].as_u64(), seen[0]["sequence"].as_u64().map(|value| value + 1));
|
||||
}
|
||||
|
||||
fn pre_service_memory_capabilities(job_capable: bool) -> Vec<&'static str> {
|
||||
let mut capabilities = vec!["heartbeat", "diagnostics.policy.v1", "inventory.environment@1"];
|
||||
if job_capable {
|
||||
capabilities.push("jobs");
|
||||
}
|
||||
// Freeze the preceding release, independently of today's advertisement.
|
||||
capabilities.extend([
|
||||
"performance.client@1",
|
||||
"performance.drive@1",
|
||||
"performance.network@1",
|
||||
"performance.object@1",
|
||||
"performance.siteReplication@1",
|
||||
"logs.capture@1",
|
||||
"profile.cpu@1",
|
||||
"profile.memory@1",
|
||||
"profile.threads@1",
|
||||
"telemetry.record@1",
|
||||
"telemetry.otlp@1",
|
||||
"telemetry.replay@1",
|
||||
"top.api@1",
|
||||
"top.disk@1",
|
||||
"top.locks@1",
|
||||
"top.net@1",
|
||||
"top.rpc@1",
|
||||
"inspect.object@1",
|
||||
]);
|
||||
capabilities
|
||||
}
|
||||
|
||||
fn pending_heartbeat_with_capabilities(capabilities: &[&str]) -> Value {
|
||||
json!({
|
||||
"protocolVersion": "v1",
|
||||
"requestId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"agentVersion": format!("rustfs-agent/{}", env!("CARGO_PKG_VERSION")),
|
||||
"capabilities": capabilities,
|
||||
"sequence": 0,
|
||||
"clientTime": "2026-08-22T01:02:03Z",
|
||||
"coarseNodeSummary": {"total": 1, "healthy": 1, "degraded": 0}
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restart_replays_exact_pre_service_memory_capabilities_with_and_without_jobs() {
|
||||
for job_capable in [false, true] {
|
||||
let pki = TestPki::new();
|
||||
let server = server(&pki, vec![Reply::ok("2026-08-22T01:02:03Z")]).await;
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let shutdown = CancellationToken::new();
|
||||
let config = config(&temp, &pki, &server);
|
||||
fs::create_dir_all(config.state_path.parent().expect("state directory")).expect("create state directory");
|
||||
let pending = pending_heartbeat_with_capabilities(&pre_service_memory_capabilities(job_capable));
|
||||
let state = json!({"nextSequence": 0, "pending": pending});
|
||||
fs::write(&config.state_path, serde_json::to_vec(&state).expect("heartbeat state JSON")).expect("write heartbeat state");
|
||||
private_mode(&config.state_path);
|
||||
let runtime = spawn_heartbeat_runtime(Some(config), &shutdown, summary)
|
||||
.expect("start runtime")
|
||||
.expect("configured runtime");
|
||||
let mut status = runtime.status();
|
||||
assert!(matches!(
|
||||
wait_for(&mut status, |status| matches!(status, HeartbeatStatus::Online { .. })).await,
|
||||
HeartbeatStatus::Online { .. }
|
||||
));
|
||||
runtime.shutdown().await;
|
||||
let seen = server.seen.lock().expect("seen lock");
|
||||
assert_eq!(seen.len(), 1);
|
||||
assert_eq!(seen[0], pending);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_service_memory_compatibility_does_not_accept_changed_capabilities() {
|
||||
for job_capable in [false, true] {
|
||||
for mutation in ["unknown", "missing", "duplicate", "reordered"] {
|
||||
let pki = TestPki::new();
|
||||
let server = server(&pki, vec![]).await;
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let shutdown = CancellationToken::new();
|
||||
let config = config(&temp, &pki, &server);
|
||||
fs::create_dir_all(config.state_path.parent().expect("state directory")).expect("create state directory");
|
||||
let mut capabilities = pre_service_memory_capabilities(job_capable);
|
||||
match mutation {
|
||||
"unknown" => capabilities.push("shell.exec@1"),
|
||||
"missing" => {
|
||||
capabilities.pop();
|
||||
}
|
||||
"duplicate" => capabilities.push("profile.memory@1"),
|
||||
"reordered" => capabilities.swap(5, 6),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let state = json!({"nextSequence": 0, "pending": pending_heartbeat_with_capabilities(&capabilities)});
|
||||
fs::write(&config.state_path, serde_json::to_vec(&state).expect("heartbeat state JSON"))
|
||||
.expect("write heartbeat state");
|
||||
private_mode(&config.state_path);
|
||||
let runtime = spawn_heartbeat_runtime(Some(config), &shutdown, summary)
|
||||
.expect("start runtime")
|
||||
.expect("configured runtime");
|
||||
let mut status = runtime.status();
|
||||
assert!(
|
||||
matches!(
|
||||
wait_for(&mut status, |status| matches!(status, HeartbeatStatus::Failed { .. })).await,
|
||||
HeartbeatStatus::Failed { reason } if reason.contains("violates the protocol invariants")
|
||||
),
|
||||
"accepted altered persisted capabilities: {mutation}, jobs={job_capable}"
|
||||
);
|
||||
assert!(server.seen.lock().expect("seen lock").is_empty());
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restart_sends_the_last_durable_diagnostic_receipt_outbound() {
|
||||
let pki = TestPki::new();
|
||||
|
||||
@@ -30,6 +30,8 @@ use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use ed25519_dalek::{Signer as _, SigningKey};
|
||||
use p256::ecdsa::signature::Verifier as _;
|
||||
use p256::ecdsa::{Signature, VerifyingKey};
|
||||
use p256::pkcs8::DecodePublicKey as _;
|
||||
@@ -37,7 +39,13 @@ use profile_cpu::{
|
||||
LocalProfileConsent, MEMORY_PROFILE_CAPABILITY, ProfileCaptureRequest, ProfileError, ProfileProvenance,
|
||||
save_signed_profile_export,
|
||||
};
|
||||
use profile_memory::{AllocationProfileSource, export_memory_profile, export_memory_profile_from, parse_allocator_stats};
|
||||
use profile_memory::{
|
||||
AllocationProfileSource, MEMORY_PROFILE_SERVICE_CAPABILITY, export_memory_profile, export_memory_profile_from,
|
||||
parse_allocator_stats,
|
||||
};
|
||||
use rustfs::connect::{
|
||||
DiagnosticJobEnvelope, DiagnosticJobError, DiagnosticJobTarget, TrustedDiagnosticJobSigner, execute_diagnostic_job,
|
||||
};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -117,6 +125,232 @@ fn archive_entry(archive: &mut ZipArchive<Cursor<Vec<u8>>>, name: &str) -> Vec<u
|
||||
bytes
|
||||
}
|
||||
|
||||
fn service_job() -> DiagnosticJobEnvelope {
|
||||
let request = request();
|
||||
let now = Utc::now();
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"jobId": request.run_uid,
|
||||
"protocolVersion": "v1",
|
||||
"jobType": "profile.memory",
|
||||
"schemaVersion": 1,
|
||||
"organizationName": request.organization_name,
|
||||
"clusterName": request.cluster_name,
|
||||
"deviceName": request.device_name,
|
||||
"createTime": now.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
"expireTime": (now + chrono::Duration::seconds(30)).to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
"nonce": URL_SAFE_NO_PAD.encode_to_string(request.nonce),
|
||||
"requiredCapabilities": [MEMORY_PROFILE_SERVICE_CAPABILITY],
|
||||
"authorization": {
|
||||
"actorType": "BROWSER_USER",
|
||||
"actorName": "users/019e3ae0-0000-7000-8000-000000000018",
|
||||
"requestId": "123e4567-e89b-42d3-a456-426614174001"
|
||||
},
|
||||
"limits": {
|
||||
"timeoutSeconds": 30,
|
||||
"maxOutputBytes": 524288,
|
||||
"maxMemoryBytes": 67108864,
|
||||
"maxCpuMillis": 5000
|
||||
},
|
||||
"parameters": {
|
||||
"artifactUid": request.artifact_uid,
|
||||
"consentUid": request.consent.consent_uid,
|
||||
"consentPolicyRevision": request.consent.policy_revision,
|
||||
"consentExpiresAt": (now + chrono::Duration::seconds(60)).to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
"durationMillis": 1000,
|
||||
"samplePeriodMicros": 10000
|
||||
},
|
||||
"signature": {"algorithm": "Ed25519", "keyId": "", "value": ""}
|
||||
}))
|
||||
.expect("service job fixture")
|
||||
}
|
||||
|
||||
fn sign_service_job(job: DiagnosticJobEnvelope) -> (DiagnosticJobEnvelope, TrustedDiagnosticJobSigner) {
|
||||
let key = SigningKey::from_bytes(&[0x17; 32]);
|
||||
let key_id = hex(&Sha256::digest(key.verifying_key().as_bytes()));
|
||||
let signer = TrustedDiagnosticJobSigner::new(key_id.clone(), *key.verifying_key().as_bytes()).expect("trusted signer");
|
||||
let signature = URL_SAFE_NO_PAD.encode_to_string(key.sign(&job.signing_payload().expect("job payload")).to_bytes());
|
||||
let mut value = serde_json::to_value(job).expect("job JSON");
|
||||
value["signature"]["keyId"] = key_id.into();
|
||||
value["signature"]["value"] = signature.into();
|
||||
(serde_json::from_value(value).expect("signed job"), signer)
|
||||
}
|
||||
|
||||
fn service_target(job: &DiagnosticJobEnvelope) -> DiagnosticJobTarget {
|
||||
DiagnosticJobTarget {
|
||||
organization_name: job.organization_name.clone(),
|
||||
cluster_name: job.cluster_name.clone(),
|
||||
device_name: job.device_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_job_requires_the_independently_negotiated_capability_and_signed_bounds() {
|
||||
let (job, signer) = sign_service_job(service_job());
|
||||
let target = service_target(&job);
|
||||
signer.verify(&job, &target, Utc::now()).expect("bounded service job");
|
||||
|
||||
for capabilities in [
|
||||
vec![MEMORY_PROFILE_CAPABILITY.to_owned()],
|
||||
vec![
|
||||
MEMORY_PROFILE_SERVICE_CAPABILITY.to_owned(),
|
||||
MEMORY_PROFILE_CAPABILITY.to_owned(),
|
||||
],
|
||||
vec![],
|
||||
] {
|
||||
let mut candidate = job.clone();
|
||||
candidate.required_capabilities = capabilities;
|
||||
let (candidate, signer) = sign_service_job(candidate);
|
||||
assert_eq!(signer.verify(&candidate, &target, Utc::now()), Err(DiagnosticJobError::Unsupported));
|
||||
}
|
||||
let mut limits = vec![job.limits.clone(); 4];
|
||||
limits[0].max_cpu_millis = 5001;
|
||||
limits[1].max_memory_bytes = 67108863;
|
||||
limits[2].max_output_bytes = 524289;
|
||||
limits[3].timeout_seconds = 31;
|
||||
for limit in limits {
|
||||
let mut candidate = job.clone();
|
||||
candidate.limits = limit;
|
||||
let (candidate, signer) = sign_service_job(candidate);
|
||||
assert_eq!(signer.verify(&candidate, &target, Utc::now()), Err(DiagnosticJobError::LimitExceeded));
|
||||
}
|
||||
let mut tampered = job.clone();
|
||||
tampered.parameters.sample_period_micros += 1;
|
||||
assert_eq!(signer.verify(&tampered, &target, Utc::now()), Err(DiagnosticJobError::SignatureInvalid));
|
||||
|
||||
let mut wrong_target = target.clone();
|
||||
wrong_target.organization_name.push('x');
|
||||
assert_eq!(signer.verify(&job, &wrong_target, Utc::now()), Err(DiagnosticJobError::TargetMismatch));
|
||||
wrong_target = target.clone();
|
||||
wrong_target.device_name.push('x');
|
||||
assert_eq!(signer.verify(&job, &wrong_target, Utc::now()), Err(DiagnosticJobError::TargetMismatch));
|
||||
|
||||
let mut expired = job;
|
||||
expired.parameters.consent_expires_at =
|
||||
(Utc::now() - chrono::Duration::seconds(1)).to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let (expired, signer) = sign_service_job(expired);
|
||||
assert_eq!(signer.verify(&expired, &target, Utc::now()), Err(DiagnosticJobError::Expired));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signed_service_memory_job_exports_the_same_process_allocator_with_original_bindings() {
|
||||
let _guard = TEST_PROFILE_LOCK.lock().await;
|
||||
let (job, signer) = sign_service_job(service_job());
|
||||
let verified = signer
|
||||
.verify(&job, &service_target(&job), Utc::now())
|
||||
.expect("verified memory job");
|
||||
let identity = connect::DeviceIdentity::generate();
|
||||
let provenance = rustfs::connect::ProfileProvenance::new("c".repeat(40), "d".repeat(64), "1.0.0-rc.6", vec![]);
|
||||
// This invokes the service dispatcher directly in this mimalloc-using process,
|
||||
// not the standalone CLI or a fixture allocation source.
|
||||
let execution = execute_diagnostic_job(verified, &identity, provenance, &CancellationToken::new()).await;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(execution, Err(DiagnosticJobError::ProfileSourceUnavailable));
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let execution = execution.expect("service allocator capture");
|
||||
assert_eq!(execution.job_id, job.job_id);
|
||||
assert_eq!(execution.outcome, "SUCCEEDED");
|
||||
assert_eq!(execution.reason, "COMPLETE");
|
||||
assert_eq!(execution.artifact_uid.as_deref(), Some(job.parameters.artifact_uid.as_str()));
|
||||
let bytes = execution.artifact_bytes.expect("signed artifact");
|
||||
assert_eq!(execution.artifact_sha256, Some(hex(&Sha256::digest(&bytes))));
|
||||
let mut archive = ZipArchive::new(Cursor::new(bytes)).expect("profile archive");
|
||||
let envelope_bytes = archive_entry(&mut archive, "envelope.json");
|
||||
let envelope: serde_json::Value = serde_json::from_slice(&envelope_bytes).expect("envelope JSON");
|
||||
let result_bytes = archive_entry(&mut archive, "result.json");
|
||||
let result: serde_json::Value = serde_json::from_slice(&result_bytes).expect("result JSON");
|
||||
assert_eq!(result["toolId"], "profile.memory");
|
||||
assert_eq!(result["schemaVersion"], 1);
|
||||
assert_eq!(result["capability"], MEMORY_PROFILE_CAPABILITY);
|
||||
assert_eq!(result["runUid"], job.job_id);
|
||||
assert_eq!(result["provenance"]["repository"], "rustfs/rustfs");
|
||||
assert_eq!(result["provenance"]["sourceCommit"], "c".repeat(40));
|
||||
assert_eq!(result["provenance"]["executableSha256"], "d".repeat(64));
|
||||
assert_eq!(result["data"]["scope"], "ALLOCATION_AGGREGATES");
|
||||
assert!(result["data"]["allocatedBytes"].is_u64());
|
||||
assert!(result["data"]["allocationCount"].is_u64());
|
||||
assert_eq!(envelope["organizationName"], job.organization_name);
|
||||
assert_eq!(envelope["clusterName"], job.cluster_name);
|
||||
assert_eq!(envelope["deviceName"], job.device_name);
|
||||
assert_eq!(envelope["artifactUid"], job.parameters.artifact_uid);
|
||||
assert_eq!(envelope["runUid"], job.job_id);
|
||||
assert_eq!(envelope["consentUid"], job.parameters.consent_uid);
|
||||
assert_eq!(envelope["policyRevision"], job.parameters.consent_policy_revision);
|
||||
assert_eq!(envelope["classification"], "L3");
|
||||
assert_eq!(envelope["nonce"], job.nonce);
|
||||
assert_eq!(envelope["payload"]["sha256"], hex(&Sha256::digest(&result_bytes)));
|
||||
let signature: serde_json::Value =
|
||||
serde_json::from_slice(&archive_entry(&mut archive, "envelope.sig")).expect("signature JSON");
|
||||
let raw = URL_SAFE_NO_PAD
|
||||
.decode_to_vec(signature["value"].as_str().expect("signature"))
|
||||
.expect("base64 signature");
|
||||
let signature = Signature::from_slice(&raw).expect("ES256 signature");
|
||||
let public = VerifyingKey::from_public_key_der(&identity.public_key_der()).expect("device public key");
|
||||
let mut input = b"rustfs-diagnostic-envelope-v1\0".to_vec();
|
||||
input.extend_from_slice(&envelope_bytes);
|
||||
public.verify(&input, &signature).expect("original device signed artifact");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancelled_service_memory_job_produces_no_artifact() {
|
||||
let (job, signer) = sign_service_job(service_job());
|
||||
let verified = signer
|
||||
.verify(&job, &service_target(&job), Utc::now())
|
||||
.expect("verified memory job");
|
||||
let cancel = CancellationToken::new();
|
||||
cancel.cancel();
|
||||
assert_eq!(
|
||||
execute_diagnostic_job(
|
||||
verified,
|
||||
&connect::DeviceIdentity::generate(),
|
||||
rustfs::connect::ProfileProvenance::new("c".repeat(40), "d".repeat(64), "1.0.0-rc.6", vec![]),
|
||||
&cancel,
|
||||
)
|
||||
.await,
|
||||
Err(DiagnosticJobError::Cancelled)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_memory_job_honors_cancellation_during_capture_and_the_signed_output_limit() {
|
||||
let _guard = TEST_PROFILE_LOCK.lock().await;
|
||||
let mut job = service_job();
|
||||
job.parameters.sample_period_micros = 500_000;
|
||||
let (job, signer) = sign_service_job(job);
|
||||
let verified = signer
|
||||
.verify(&job, &service_target(&job), Utc::now())
|
||||
.expect("verified memory job");
|
||||
let identity = connect::DeviceIdentity::generate();
|
||||
let provenance = rustfs::connect::ProfileProvenance::new("c".repeat(40), "d".repeat(64), "1.0.0-rc.6", vec![]);
|
||||
let cancel = CancellationToken::new();
|
||||
let execution = execute_diagnostic_job(verified, &identity, provenance.clone(), &cancel);
|
||||
let cancellation = async {
|
||||
// The first join branch polls the collector through its first snapshot
|
||||
// into the bounded wait before this branch cancels the same token.
|
||||
cancel.cancel();
|
||||
};
|
||||
let (result, ()) = tokio::join!(biased; execution, cancellation);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(result, Err(DiagnosticJobError::Cancelled));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(result, Err(DiagnosticJobError::ProfileSourceUnavailable));
|
||||
|
||||
let mut job = service_job();
|
||||
job.limits.max_output_bytes = 1;
|
||||
let (job, signer) = sign_service_job(job);
|
||||
let verified = signer
|
||||
.verify(&job, &service_target(&job), Utc::now())
|
||||
.expect("bounded output memory job");
|
||||
let result = execute_diagnostic_job(verified, &identity, provenance, &CancellationToken::new()).await;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(result, Err(DiagnosticJobError::LimitExceeded));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(result, Err(DiagnosticJobError::ProfileSourceUnavailable));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn real_mimalloc_profile_produces_a_signed_three_file_export() {
|
||||
let _guard = TEST_PROFILE_LOCK.lock().await;
|
||||
|
||||
@@ -506,6 +506,25 @@ SCHEDULED_ALERT_WORKFLOWS = tuple(
|
||||
item["workflow"]
|
||||
for item in json.loads((ROOT / ".github/scheduled-validations.json").read_text())
|
||||
)
|
||||
# Each migrated scheduled-alert workflow requires the new pin; this is not a list of pins that
|
||||
# every workflow may choose between during the staged checkout update.
|
||||
SCHEDULED_ALERT_CHECKOUT_OVERRIDES = {
|
||||
".github/workflows/audit.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/coverage.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/e2e-replication-nightly.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/e2e-distributed.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/e2e-s3tests.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/mint.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/minio-interop.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/build.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/ci.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/fuzz.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/nightly-gnu.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/performance-ab.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/runner-hygiene.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/scheduled-validation-watchdog.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
".github/workflows/scheduled-validation-freshness.yml": "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a",
|
||||
}
|
||||
|
||||
|
||||
def words(value: str) -> set[str]:
|
||||
@@ -1030,7 +1049,10 @@ def check_quick_checks(root: Path) -> list[str]:
|
||||
yaml_scalar_continues(job, index, 4) for index in conditions
|
||||
):
|
||||
errors.append(f"{relative}: Quick Checks job must not add dependencies, bypass failures, or change its event condition")
|
||||
checkout = workflow_step_block(job, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0")
|
||||
checkout_ref = SCHEDULED_ALERT_CHECKOUT_OVERRIDES.get(
|
||||
relative, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
|
||||
)
|
||||
checkout = workflow_step_block(job, checkout_ref)
|
||||
action = workflow_step_block(job, "./.github/actions/quick-checks")
|
||||
if checkout is None or action is None:
|
||||
errors.append(f"{relative}: Quick Checks requires checkout and the shared quick-checks action")
|
||||
@@ -1073,8 +1095,9 @@ def alert_step_errors(
|
||||
expected_action_if: str | None,
|
||||
required_permissions: tuple[str, ...],
|
||||
required_action_tokens: tuple[str, ...],
|
||||
checkout_ref: str = "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
) -> list[str]:
|
||||
checkout = workflow_step_block(job_lines, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0")
|
||||
checkout = workflow_step_block(job_lines, checkout_ref)
|
||||
action = workflow_step_block(job_lines, "./.github/actions/schedule-failure-issue")
|
||||
errors: list[str] = []
|
||||
permissions = yaml_block(job_lines, "permissions", 4)
|
||||
@@ -1161,12 +1184,15 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
errors.append(f"{relative}: missing alert-on-failure job")
|
||||
continue
|
||||
job = "\n".join(line.split("#", 1)[0] for line in job_lines)
|
||||
checkout_ref = SCHEDULED_ALERT_CHECKOUT_OVERRIDES.get(
|
||||
relative, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
|
||||
)
|
||||
required = (
|
||||
"always()",
|
||||
"github.event_name == 'schedule'",
|
||||
"contains(needs.*.result, 'failure')",
|
||||
"issues: write",
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {checkout_ref}",
|
||||
"uses: ./.github/actions/schedule-failure-issue",
|
||||
"github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",
|
||||
)
|
||||
@@ -1176,7 +1202,10 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
else:
|
||||
errors.extend(
|
||||
f"{relative}: {error}"
|
||||
for error in alert_step_errors(job_lines, None, ("issues: write",), ("github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",))
|
||||
for error in alert_step_errors(
|
||||
job_lines, None, ("issues: write",),
|
||||
("github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",), checkout_ref=checkout_ref,
|
||||
)
|
||||
)
|
||||
|
||||
for (hour, minute), workflows in schedule_slots.items():
|
||||
@@ -1213,13 +1242,17 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
errors.append(".github/workflows/scheduled-validation-watchdog.yml: missing alert-on-incomplete-run job")
|
||||
return errors
|
||||
watchdog_job = "\n".join(line.split("#", 1)[0] for line in watchdog_job_lines)
|
||||
watchdog_checkout_ref = SCHEDULED_ALERT_CHECKOUT_OVERRIDES.get(
|
||||
".github/workflows/scheduled-validation-watchdog.yml",
|
||||
"actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
)
|
||||
required = (
|
||||
"github.event.workflow_run.event == 'schedule'",
|
||||
"github.event.workflow_run.conclusion != 'success'",
|
||||
"github.event.workflow_run.conclusion != 'failure'",
|
||||
"actions: read",
|
||||
"issues: write",
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {watchdog_checkout_ref}",
|
||||
"uses: ./.github/actions/schedule-failure-issue",
|
||||
"github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",
|
||||
"workflow-name: ${{ github.event.workflow_run.name }}",
|
||||
@@ -1250,6 +1283,7 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
"source-ref-name: ${{ github.event.workflow_run.head_branch }}",
|
||||
"source-sha: ${{ github.event.workflow_run.head_sha }}",
|
||||
),
|
||||
checkout_ref=watchdog_checkout_ref,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1264,12 +1298,16 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
errors.append(".github/workflows/scheduled-validation-freshness.yml: missing check-freshness job")
|
||||
return errors
|
||||
freshness_job = "\n".join(line.split("#", 1)[0] for line in freshness_job_lines)
|
||||
freshness_checkout_ref = SCHEDULED_ALERT_CHECKOUT_OVERRIDES.get(
|
||||
".github/workflows/scheduled-validation-freshness.yml",
|
||||
"actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
)
|
||||
required = (
|
||||
"python3 scripts/check_scheduled_validation_freshness.py",
|
||||
"actions: read",
|
||||
"issues: write",
|
||||
"if: failure()",
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {freshness_checkout_ref}",
|
||||
"uses: ./.github/actions/schedule-failure-issue",
|
||||
"github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",
|
||||
"details-file: ${{ runner.temp }}/scheduled-validation-freshness.md",
|
||||
@@ -1290,6 +1328,7 @@ def check_scheduled_alerts(root: Path) -> list[str]:
|
||||
"github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}",
|
||||
"details-file: ${{ runner.temp }}/scheduled-validation-freshness.md",
|
||||
),
|
||||
checkout_ref=freshness_checkout_ref,
|
||||
)
|
||||
)
|
||||
if not (root / "scripts/check_scheduled_validation_freshness.py").is_file():
|
||||
@@ -3466,7 +3505,7 @@ class SelfTests(unittest.TestCase):
|
||||
root = Path(tmp)
|
||||
caller = (
|
||||
"jobs:\n quick-checks:\n steps:\n"
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
" - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a\n"
|
||||
" with:\n persist-credentials: false\n"
|
||||
" - uses: ./.github/actions/quick-checks\n"
|
||||
)
|
||||
@@ -5850,6 +5889,23 @@ class SelfTests(unittest.TestCase):
|
||||
self.assertEqual(selection.read_text(), f"sha256={digest}\n")
|
||||
|
||||
def test_scheduled_alerts_require_completion_watchdog(self) -> None:
|
||||
old_checkout = "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
|
||||
new_checkout = "actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a"
|
||||
upgraded_workflows = (
|
||||
".github/workflows/audit.yml",
|
||||
".github/workflows/coverage.yml",
|
||||
".github/workflows/e2e-replication-nightly.yml",
|
||||
".github/workflows/e2e-distributed.yml",
|
||||
".github/workflows/e2e-s3tests.yml",
|
||||
".github/workflows/mint.yml",
|
||||
".github/workflows/minio-interop.yml",
|
||||
".github/workflows/build.yml",
|
||||
".github/workflows/ci.yml",
|
||||
".github/workflows/fuzz.yml",
|
||||
".github/workflows/nightly-gnu.yml",
|
||||
".github/workflows/performance-ab.yml",
|
||||
".github/workflows/runner-hygiene.yml",
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
alert = (
|
||||
@@ -5869,11 +5925,15 @@ class SelfTests(unittest.TestCase):
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
names.append(path.stem)
|
||||
checkout_ref = new_checkout if relative in upgraded_workflows else old_checkout
|
||||
workflow_alert = alert.replace(old_checkout, checkout_ref)
|
||||
path.write_text(
|
||||
f'name: "{path.stem}"\n'
|
||||
f'on:\n schedule:\n - cron: "{index} {index} * * *"\n'
|
||||
f'jobs:\n{alert}'
|
||||
f'jobs:\n{workflow_alert}'
|
||||
)
|
||||
watchdog_checkout = new_checkout
|
||||
freshness_checkout = new_checkout
|
||||
watchdog = root / ".github/workflows/scheduled-validation-watchdog.yml"
|
||||
watchdog.write_text(
|
||||
"on:\n workflow_run:\n workflows:\n"
|
||||
@@ -5887,7 +5947,7 @@ class SelfTests(unittest.TestCase):
|
||||
+ " actions: read\n"
|
||||
+ " issues: write\n"
|
||||
+ " steps:\n"
|
||||
+ " - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
+ f" - uses: {watchdog_checkout}\n"
|
||||
+ " - uses: ./.github/actions/schedule-failure-issue\n"
|
||||
+ " with:\n"
|
||||
+ " github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}\n"
|
||||
@@ -5906,7 +5966,7 @@ class SelfTests(unittest.TestCase):
|
||||
" actions: read\n"
|
||||
" issues: write\n"
|
||||
" steps:\n"
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" - uses: {freshness_checkout}\n"
|
||||
" - run: python3 scripts/check_scheduled_validation_freshness.py\n"
|
||||
" - uses: ./.github/actions/schedule-failure-issue\n"
|
||||
" if: failure()\n"
|
||||
@@ -5919,17 +5979,34 @@ class SelfTests(unittest.TestCase):
|
||||
checker.write_text("")
|
||||
self.assertEqual(check_scheduled_alerts(root), [])
|
||||
|
||||
# Reverting a migrated workflow must fail, just as independently
|
||||
# upgrading an unmigrated one must fail: no shared two-pin allowlist.
|
||||
self.assertTrue(set(upgraded_workflows).issubset(SCHEDULED_ALERT_WORKFLOWS))
|
||||
for relative in SCHEDULED_ALERT_WORKFLOWS:
|
||||
expected = new_checkout if relative in upgraded_workflows else old_checkout
|
||||
rejected = old_checkout if relative in upgraded_workflows else new_checkout
|
||||
path = root / relative
|
||||
original = path.read_text()
|
||||
with self.subTest(workflow=relative, rejected_pin=rejected):
|
||||
self.assertIn(expected, original)
|
||||
path.write_text(original.replace(expected, rejected))
|
||||
errors = check_scheduled_alerts(root)
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertIn(f"{relative}: alert-on-failure missing uses: {expected}", errors[0])
|
||||
path.write_text(original)
|
||||
|
||||
first = root / SCHEDULED_ALERT_WORKFLOWS[0]
|
||||
first_checkout = new_checkout if SCHEDULED_ALERT_WORKFLOWS[0] in upgraded_workflows else old_checkout
|
||||
mutations = (
|
||||
("contains(needs.*.result, 'failure')", "false"),
|
||||
("issues: write", "issues: read"),
|
||||
(
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {first_checkout}",
|
||||
"uses: actions/checkout@missing",
|
||||
),
|
||||
(
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n",
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" - uses: {first_checkout}\n",
|
||||
f" - uses: {first_checkout}\n"
|
||||
" if: github.event_name == 'workflow_dispatch'\n",
|
||||
),
|
||||
(
|
||||
@@ -5948,7 +6025,7 @@ class SelfTests(unittest.TestCase):
|
||||
|
||||
first_original = first.read_text()
|
||||
real_steps = (
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" - uses: {first_checkout}\n"
|
||||
" - uses: ./.github/actions/schedule-failure-issue\n"
|
||||
" with:\n"
|
||||
" github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}\n"
|
||||
@@ -5958,7 +6035,7 @@ class SelfTests(unittest.TestCase):
|
||||
real_steps,
|
||||
" - run: |\n"
|
||||
" : <<'MARKER'\n"
|
||||
" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" uses: {first_checkout}\n"
|
||||
" MARKER\n"
|
||||
" - run: |\n"
|
||||
" : <<'MARKER'\n"
|
||||
@@ -5974,7 +6051,7 @@ class SelfTests(unittest.TestCase):
|
||||
" - uses: ./.github/actions/schedule-failure-issue\n"
|
||||
" with:\n"
|
||||
" github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}\n"
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n",
|
||||
f" - uses: {first_checkout}\n",
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(check_scheduled_alerts(root)), 1)
|
||||
@@ -5984,12 +6061,12 @@ class SelfTests(unittest.TestCase):
|
||||
("actions: read", "actions: none"),
|
||||
("issues: write", "issues: read"),
|
||||
(
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {watchdog_checkout}",
|
||||
"uses: actions/checkout@missing",
|
||||
),
|
||||
(
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n",
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" - uses: {watchdog_checkout}\n",
|
||||
f" - uses: {watchdog_checkout}\n"
|
||||
" if: github.event_name == 'workflow_dispatch'\n",
|
||||
),
|
||||
(
|
||||
@@ -6092,15 +6169,15 @@ class SelfTests(unittest.TestCase):
|
||||
self.assertEqual(len(check_scheduled_alerts(root)), 1)
|
||||
freshness.write_text(
|
||||
freshness_original.replace(
|
||||
"uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
f"uses: {freshness_checkout}",
|
||||
"uses: actions/checkout@missing",
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(check_scheduled_alerts(root)), 1)
|
||||
freshness.write_text(
|
||||
freshness_original.replace(
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n",
|
||||
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
|
||||
f" - uses: {freshness_checkout}\n",
|
||||
f" - uses: {freshness_checkout}\n"
|
||||
" if: github.event_name == 'workflow_dispatch'\n",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -21,18 +21,28 @@ fail() {
|
||||
}
|
||||
|
||||
if [[ $# -ne 5 ]]; then
|
||||
fail "expected BUILD_TYPE SOURCE_VERSION DEV_SEQUENCE DEB_ARCH RPM_ARCH"
|
||||
fail "expected BUILD_TYPE SOURCE_VERSION DEV_SEQUENCE ARCH LIBC"
|
||||
fi
|
||||
|
||||
build_type=$1
|
||||
source_version=$2
|
||||
dev_sequence=$3
|
||||
deb_arch=$4
|
||||
rpm_arch=$5
|
||||
arch=$4
|
||||
libc=$5
|
||||
|
||||
case "${deb_arch}:${rpm_arch}" in
|
||||
amd64:x86_64 | arm64:aarch64) ;;
|
||||
*) fail "unsupported or mismatched architecture pair" ;;
|
||||
case "$arch" in
|
||||
x86_64 | aarch64) ;;
|
||||
*) fail "unsupported architecture (expected x86_64 or aarch64)" ;;
|
||||
esac
|
||||
|
||||
# The libc variant of the binary being packaged. It only distinguishes the
|
||||
# package FILE names (gnu and musl builds of the same version would otherwise
|
||||
# collide on the release); the dpkg/rpm package identity stays plain "rustfs"
|
||||
# so the two variants remain mutually exclusive upgrades, not co-installable
|
||||
# packages.
|
||||
case "$libc" in
|
||||
gnu | musl) ;;
|
||||
*) fail "unsupported libc variant (expected gnu or musl)" ;;
|
||||
esac
|
||||
|
||||
semver_core='(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)'
|
||||
@@ -89,8 +99,23 @@ case "$build_type" in
|
||||
*) fail "unsupported build type" ;;
|
||||
esac
|
||||
|
||||
deb_file="rustfs_${deb_version}_${deb_arch}.deb"
|
||||
rpm_file="rustfs-${rpm_version}-${rpm_release}.${rpm_arch}.rpm"
|
||||
# Package file names mirror the binary artifact names, whose zips are named
|
||||
# rustfs-linux-<arch>-<libc>-<version>.zip: all release assets of one build
|
||||
# share the same stem and differ only by extension. Non-development builds
|
||||
# embed the raw release tag after the 'v' marker exactly like the zips;
|
||||
# development builds embed dev-<sha> (zips use the short SHA, packages the
|
||||
# full one). The dpkg/rpm versions with their '~' prerelease ordering live
|
||||
# in the package metadata above and are independent of the file name.
|
||||
case "$build_type" in
|
||||
development)
|
||||
package_stem="rustfs-linux-${arch}-${libc}-dev-${source_sha}"
|
||||
;;
|
||||
*)
|
||||
package_stem="rustfs-linux-${arch}-${libc}-v${source_version}"
|
||||
;;
|
||||
esac
|
||||
deb_file="${package_stem}.deb"
|
||||
rpm_file="${package_stem}.rpm"
|
||||
|
||||
# Emit only after every input and derived value has been validated. Consumers
|
||||
# may append this fixed five-line protocol directly to GITHUB_OUTPUT.
|
||||
|
||||
@@ -70,61 +70,73 @@ run_failure() {
|
||||
|
||||
sha=0123456789abcdef0123456789abcdef01234567
|
||||
|
||||
run_success stable-amd64 \
|
||||
$'deb_version=1.2.3\nrpm_version=1.2.3\nrpm_release=1\ndeb_file=rustfs_1.2.3_amd64.deb\nrpm_file=rustfs-1.2.3-1.x86_64.rpm' \
|
||||
release 1.2.3 '' amd64 x86_64
|
||||
run_success alpha-arm64 \
|
||||
$'deb_version=1.2.3~alpha.1\nrpm_version=1.2.3~alpha.1\nrpm_release=1\ndeb_file=rustfs_1.2.3~alpha.1_arm64.deb\nrpm_file=rustfs-1.2.3~alpha.1-1.aarch64.rpm' \
|
||||
prerelease 1.2.3-alpha.1 '' arm64 aarch64
|
||||
run_success beta-amd64 \
|
||||
$'deb_version=1.2.3~beta.2\nrpm_version=1.2.3~beta.2\nrpm_release=1\ndeb_file=rustfs_1.2.3~beta.2_amd64.deb\nrpm_file=rustfs-1.2.3~beta.2-1.x86_64.rpm' \
|
||||
prerelease 1.2.3-beta.2 '' amd64 x86_64
|
||||
run_success rc-amd64 \
|
||||
$'deb_version=1.2.3~rc.4\nrpm_version=1.2.3~rc.4\nrpm_release=1\ndeb_file=rustfs_1.2.3~rc.4_amd64.deb\nrpm_file=rustfs-1.2.3~rc.4-1.x86_64.rpm' \
|
||||
prerelease 1.2.3-rc.4 '' amd64 x86_64
|
||||
run_success preview-amd64 \
|
||||
$'deb_version=1.0.0~rc.5~preview.2\nrpm_version=1.0.0~rc.5~preview.2\nrpm_release=1\ndeb_file=rustfs_1.0.0~rc.5~preview.2_amd64.deb\nrpm_file=rustfs-1.0.0~rc.5~preview.2-1.x86_64.rpm' \
|
||||
preview 1.0.0-rc.5-preview.2 '' amd64 x86_64
|
||||
run_success development-amd64 \
|
||||
run_success stable-x86_64-gnu \
|
||||
$'deb_version=1.2.3\nrpm_version=1.2.3\nrpm_release=1\ndeb_file=rustfs-linux-x86_64-gnu-v1.2.3.deb\nrpm_file=rustfs-linux-x86_64-gnu-v1.2.3.rpm' \
|
||||
release 1.2.3 '' x86_64 gnu
|
||||
run_success stable-x86_64-musl \
|
||||
$'deb_version=1.2.3\nrpm_version=1.2.3\nrpm_release=1\ndeb_file=rustfs-linux-x86_64-musl-v1.2.3.deb\nrpm_file=rustfs-linux-x86_64-musl-v1.2.3.rpm' \
|
||||
release 1.2.3 '' x86_64 musl
|
||||
run_success alpha-aarch64-gnu \
|
||||
$'deb_version=1.2.3~alpha.1\nrpm_version=1.2.3~alpha.1\nrpm_release=1\ndeb_file=rustfs-linux-aarch64-gnu-v1.2.3-alpha.1.deb\nrpm_file=rustfs-linux-aarch64-gnu-v1.2.3-alpha.1.rpm' \
|
||||
prerelease 1.2.3-alpha.1 '' aarch64 gnu
|
||||
run_success alpha-aarch64-musl \
|
||||
$'deb_version=1.2.3~alpha.1\nrpm_version=1.2.3~alpha.1\nrpm_release=1\ndeb_file=rustfs-linux-aarch64-musl-v1.2.3-alpha.1.deb\nrpm_file=rustfs-linux-aarch64-musl-v1.2.3-alpha.1.rpm' \
|
||||
prerelease 1.2.3-alpha.1 '' aarch64 musl
|
||||
run_success beta-x86_64-gnu \
|
||||
$'deb_version=1.2.3~beta.2\nrpm_version=1.2.3~beta.2\nrpm_release=1\ndeb_file=rustfs-linux-x86_64-gnu-v1.2.3-beta.2.deb\nrpm_file=rustfs-linux-x86_64-gnu-v1.2.3-beta.2.rpm' \
|
||||
prerelease 1.2.3-beta.2 '' x86_64 gnu
|
||||
run_success rc-x86_64-musl \
|
||||
$'deb_version=1.2.3~rc.4\nrpm_version=1.2.3~rc.4\nrpm_release=1\ndeb_file=rustfs-linux-x86_64-musl-v1.2.3-rc.4.deb\nrpm_file=rustfs-linux-x86_64-musl-v1.2.3-rc.4.rpm' \
|
||||
prerelease 1.2.3-rc.4 '' x86_64 musl
|
||||
run_success preview-x86_64-gnu \
|
||||
$'deb_version=1.0.0~rc.5~preview.2\nrpm_version=1.0.0~rc.5~preview.2\nrpm_release=1\ndeb_file=rustfs-linux-x86_64-gnu-v1.0.0-rc.5-preview.2.deb\nrpm_file=rustfs-linux-x86_64-gnu-v1.0.0-rc.5-preview.2.rpm' \
|
||||
preview 1.0.0-rc.5-preview.2 '' x86_64 gnu
|
||||
run_success preview-aarch64-musl \
|
||||
$'deb_version=1.0.0~rc.5~preview.2\nrpm_version=1.0.0~rc.5~preview.2\nrpm_release=1\ndeb_file=rustfs-linux-aarch64-musl-v1.0.0-rc.5-preview.2.deb\nrpm_file=rustfs-linux-aarch64-musl-v1.0.0-rc.5-preview.2.rpm' \
|
||||
preview 1.0.0-rc.5-preview.2 '' aarch64 musl
|
||||
run_success development-x86_64-gnu \
|
||||
"deb_version=0~dev.7463.${sha}
|
||||
rpm_version=0
|
||||
rpm_release=0.dev.7463.${sha}
|
||||
deb_file=rustfs_0~dev.7463.${sha}_amd64.deb
|
||||
rpm_file=rustfs-0-0.dev.7463.${sha}.x86_64.rpm" \
|
||||
development "dev-${sha}" 7463 amd64 x86_64
|
||||
run_success development-arm64 \
|
||||
deb_file=rustfs-linux-x86_64-gnu-dev-${sha}.deb
|
||||
rpm_file=rustfs-linux-x86_64-gnu-dev-${sha}.rpm" \
|
||||
development "dev-${sha}" 7463 x86_64 gnu
|
||||
run_success development-aarch64-musl \
|
||||
"deb_version=0~dev.7463.${sha}
|
||||
rpm_version=0
|
||||
rpm_release=0.dev.7463.${sha}
|
||||
deb_file=rustfs_0~dev.7463.${sha}_arm64.deb
|
||||
rpm_file=rustfs-0-0.dev.7463.${sha}.aarch64.rpm" \
|
||||
development "dev-${sha}" 7463 arm64 aarch64
|
||||
deb_file=rustfs-linux-aarch64-musl-dev-${sha}.deb
|
||||
rpm_file=rustfs-linux-aarch64-musl-dev-${sha}.rpm" \
|
||||
development "dev-${sha}" 7463 aarch64 musl
|
||||
|
||||
run_failure missing-arguments
|
||||
run_failure empty-build-type '' 1.2.3 '' amd64 x86_64
|
||||
run_failure unknown-build-type nightly 1.2.3 '' amd64 x86_64
|
||||
run_failure empty-version release '' '' amd64 x86_64
|
||||
run_failure release-with-sequence release 1.2.3 1 amd64 x86_64
|
||||
run_failure release-prerelease-mismatch release 1.2.3-rc.1 '' amd64 x86_64
|
||||
run_failure prerelease-release-mismatch prerelease 1.2.3 '' amd64 x86_64
|
||||
run_failure preview-malformed preview 1.2.3-rc.1-preview '' amd64 x86_64
|
||||
run_failure preview-wrong-shape preview 1.2.3-preview.1 '' amd64 x86_64
|
||||
run_failure short-semver release 1.2 '' amd64 x86_64
|
||||
run_failure leading-v release v1.2.3 '' amd64 x86_64
|
||||
run_failure leading-zero release 01.2.3 '' amd64 x86_64
|
||||
run_failure zero-sequence development "dev-${sha}" 0 amd64 x86_64
|
||||
run_failure leading-zero-sequence development "dev-${sha}" 01 amd64 x86_64
|
||||
run_failure non-decimal-sequence development "dev-${sha}" seven amd64 x86_64
|
||||
run_failure empty-dev-sha development dev- 1 amd64 x86_64
|
||||
run_failure short-dev-sha development dev-0123456 1 amd64 x86_64
|
||||
run_failure uppercase-dev-sha development dev-0123456789ABCDEF0123456789ABCDEF01234567 1 amd64 x86_64
|
||||
run_failure dev-extra-suffix development "dev-${sha}-dirty" 1 amd64 x86_64
|
||||
run_failure whitespace release '1.2.3 bad' '' amd64 x86_64
|
||||
run_failure command-substitution release "1.2.3\$(id)" '' amd64 x86_64
|
||||
run_failure backticks release "1.2.3\`id\`" '' amd64 x86_64
|
||||
run_failure newline release $'1.2.3\nforged=1' '' amd64 x86_64
|
||||
run_failure unsupported-deb-arch release 1.2.3 '' x86_64 x86_64
|
||||
run_failure mismatched-arch release 1.2.3 '' amd64 aarch64
|
||||
run_failure empty-build-type '' 1.2.3 '' x86_64 gnu
|
||||
run_failure unknown-build-type nightly 1.2.3 '' x86_64 gnu
|
||||
run_failure empty-version release '' '' x86_64 gnu
|
||||
run_failure release-with-sequence release 1.2.3 1 x86_64 gnu
|
||||
run_failure release-prerelease-mismatch release 1.2.3-rc.1 '' x86_64 gnu
|
||||
run_failure prerelease-release-mismatch prerelease 1.2.3 '' x86_64 gnu
|
||||
run_failure preview-malformed preview 1.2.3-rc.1-preview '' x86_64 gnu
|
||||
run_failure preview-wrong-shape preview 1.2.3-preview.1 '' x86_64 gnu
|
||||
run_failure short-semver release 1.2 '' x86_64 gnu
|
||||
run_failure leading-v release v1.2.3 '' x86_64 gnu
|
||||
run_failure leading-zero release 01.2.3 '' x86_64 gnu
|
||||
run_failure zero-sequence development "dev-${sha}" 0 x86_64 gnu
|
||||
run_failure leading-zero-sequence development "dev-${sha}" 01 x86_64 gnu
|
||||
run_failure non-decimal-sequence development "dev-${sha}" seven x86_64 gnu
|
||||
run_failure empty-dev-sha development dev- 1 x86_64 gnu
|
||||
run_failure short-dev-sha development dev-0123456 1 x86_64 gnu
|
||||
run_failure uppercase-dev-sha development dev-0123456789ABCDEF0123456789ABCDEF01234567 1 x86_64 gnu
|
||||
run_failure dev-extra-suffix development "dev-${sha}-dirty" 1 x86_64 gnu
|
||||
run_failure whitespace release '1.2.3 bad' '' x86_64 gnu
|
||||
run_failure command-substitution release "1.2.3\$(id)" '' x86_64 gnu
|
||||
run_failure backticks release "1.2.3\`id\`" '' x86_64 gnu
|
||||
run_failure newline release $'1.2.3\nforged=1' '' x86_64 gnu
|
||||
run_failure unsupported-arch release 1.2.3 '' armv7 gnu
|
||||
run_failure empty-arch release 1.2.3 '' '' gnu
|
||||
run_failure empty-libc release 1.2.3 '' x86_64 ''
|
||||
run_failure unknown-libc release 1.2.3 '' x86_64 static
|
||||
run_failure too-many-arguments release 1.2.3 '' x86_64 gnu extra
|
||||
|
||||
# Ordering contract shared by both package managers: every pre-release sorts
|
||||
# below its final release, every preview sorts below the pre-release it
|
||||
|
||||
Reference in New Issue
Block a user