mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-06 03:59:14 +00:00
fix(ci): preserve reported functional suite failures
This commit is contained in:
@@ -54,9 +54,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
heal-test:
|
heal-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
# Requirement: a failing suite must not fail the workflow; failures
|
|
||||||
# are filed to rustfs/backlog and the chain continues.
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 480
|
timeout-minutes: 480
|
||||||
# Standalone manual run, or one link of the nightly functional chain
|
# Standalone manual run, or one link of the nightly functional chain
|
||||||
# (storage -> heal -> pool). Pool expansion no longer re-runs heal.
|
# (storage -> heal -> pool). Pool expansion no longer re-runs heal.
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
kms-test:
|
kms-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 420
|
timeout-minutes: 420
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -109,7 +108,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run KMS suite
|
- name: Run KMS suite
|
||||||
id: test
|
id: test
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
LOG_FILE: /tmp/rustfs-kms.log
|
LOG_FILE: /tmp/rustfs-kms.log
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
performance-test:
|
performance-test:
|
||||||
runs-on: pf-testing
|
runs-on: pf-testing
|
||||||
# Requirement: a failing benchmark must not fail the workflow;
|
|
||||||
# failures are filed to rustfs/backlog.
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 900
|
timeout-minutes: 900
|
||||||
# Run on manual dispatch, or when the nightly build completed successfully.
|
# Run on manual dispatch, or when the nightly build completed successfully.
|
||||||
# Skipped when nightly failed.
|
# Skipped when nightly failed.
|
||||||
|
|||||||
@@ -76,9 +76,6 @@ jobs:
|
|||||||
pool-expansion-test:
|
pool-expansion-test:
|
||||||
name: Pool expansion / decommission test
|
name: Pool expansion / decommission test
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
# Requirement: a failing suite must not fail the workflow; failures
|
|
||||||
# are filed to rustfs/backlog and the chain continues.
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 360
|
timeout-minutes: 360
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
replication-test:
|
replication-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
# A failed replication run must not break the chain or the workflow: the
|
|
||||||
# failure is reported to rustfs/backlog instead (see the issue step).
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 360
|
timeout-minutes: 360
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -116,7 +113,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run replication suite
|
- name: Run replication suite
|
||||||
id: test
|
id: test
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
LOG_FILE: /tmp/rustfs-replication.log
|
LOG_FILE: /tmp/rustfs-replication.log
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
s3-compat-test:
|
s3-compat-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 360
|
timeout-minutes: 360
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -88,7 +87,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run S3 compatibility suite
|
- name: Run S3 compatibility suite
|
||||||
id: test
|
id: test
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
LOG_FILE: /tmp/rustfs-s3-compat.log
|
LOG_FILE: /tmp/rustfs-s3-compat.log
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
storage-test:
|
storage-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 360
|
timeout-minutes: 360
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -97,7 +96,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run storage engine suite
|
- name: Run storage engine suite
|
||||||
id: test
|
id: test
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
LOG_FILE: /tmp/rustfs-storage.log
|
LOG_FILE: /tmp/rustfs-storage.log
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -61,9 +61,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
tier-test:
|
tier-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
# Requirement: a failing suite must not fail the workflow; failures
|
|
||||||
# are filed to rustfs/backlog and the chain continues.
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 420
|
timeout-minutes: 420
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
upgrade-test:
|
upgrade-test:
|
||||||
runs-on: smoke-testing
|
runs-on: smoke-testing
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 420
|
timeout-minutes: 420
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -142,7 +141,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run upgrade compatibility suite
|
- name: Run upgrade compatibility suite
|
||||||
id: test
|
id: test
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
LOG_FILE: /tmp/rustfs-upgrade.log
|
LOG_FILE: /tmp/rustfs-upgrade.log
|
||||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ Scheduled lanes never block a PR. Their workflow-local gate fails the run, sched
|
|||||||
|
|
||||||
Manual `workflow_dispatch` runs are debugging evidence and do not open scheduled-failure issues. A manual performance run may explicitly allow a known regression; that override is not a passing baseline.
|
Manual `workflow_dispatch` runs are debugging evidence and do not open scheduled-failure issues. A manual performance run may explicitly allow a known regression; that override is not a passing baseline.
|
||||||
|
|
||||||
|
## Packaged functional acceptance
|
||||||
|
|
||||||
|
`rustfs-functional-chain.yml` dispatches the packaged-build suites in `rustfs-*-test.yml` on the shared lab runners. A failing suite step or job must fail its workflow. Report collection, cleanup, and dispatch of the next suite can still run with `always()`; continuing diagnostics does not make the failed suite successful.
|
||||||
|
|
||||||
|
Workflow status preserves errors that the test scripts report. It does not establish complete execution or a common package identity across the chain: inspect the current run's case results, package identity, and test-script revision as well. A script that returns zero after a failed tool invocation needs its own result check.
|
||||||
|
|
||||||
## Release validation
|
## Release validation
|
||||||
|
|
||||||
Post-merge and tag-driven; not a substitute for a PR gate.
|
Post-merge and tag-driven; not a substitute for a PR gate.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Run the security workflow's evidence and result steps without remote VMs."""
|
"""Exercise functional workflow failures and security evidence without remote VMs."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,16 +18,32 @@ WORKFLOW = ROOT / ".github/workflows/rustfs-security-test.yml"
|
|||||||
CASE_ROW = "| IAM-101 | user CRUD lifecycle | PASS |"
|
CASE_ROW = "| IAM-101 | user CRUD lifecycle | PASS |"
|
||||||
|
|
||||||
|
|
||||||
|
def named_steps(job: list[str]) -> dict[str, list[str]]:
|
||||||
|
starts = [i for i, line in enumerate(job) if line.startswith(" - name: ")]
|
||||||
|
return {
|
||||||
|
job[start].split(": ", 1)[1].strip('"'): job[start:end]
|
||||||
|
for start, end in zip(starts, starts[1:] + [len(job)])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def shell_body(lines: list[str]) -> str:
|
||||||
|
start = lines.index(" run: |") + 1
|
||||||
|
shell_lines = []
|
||||||
|
for line in lines[start:]:
|
||||||
|
if line.strip() and not line.startswith(" "):
|
||||||
|
break
|
||||||
|
shell_lines.append(line[10:])
|
||||||
|
if not shell_lines:
|
||||||
|
raise ValueError("missing literal shell body")
|
||||||
|
return "\n".join(shell_lines)
|
||||||
|
|
||||||
|
|
||||||
class SecurityWorkflowTests(unittest.TestCase):
|
class SecurityWorkflowTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.source = WORKFLOW.read_text()
|
self.source = WORKFLOW.read_text()
|
||||||
self.job = yaml_block(self.source.splitlines(), "security-test", 2)
|
self.job = yaml_block(self.source.splitlines(), "security-test", 2)
|
||||||
self.assertIsNotNone(self.job)
|
self.assertIsNotNone(self.job)
|
||||||
starts = [i for i, line in enumerate(self.job) if line.startswith(" - name: ")]
|
self.steps = named_steps(self.job)
|
||||||
self.steps = {
|
|
||||||
self.job[start].split(": ", 1)[1].strip('"'): self.job[start:end]
|
|
||||||
for start, end in zip(starts, starts[1:] + [len(self.job)])
|
|
||||||
}
|
|
||||||
self.temp = tempfile.TemporaryDirectory()
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
self.addCleanup(self.temp.cleanup)
|
self.addCleanup(self.temp.cleanup)
|
||||||
self.directory = Path(self.temp.name)
|
self.directory = Path(self.temp.name)
|
||||||
@@ -83,15 +99,8 @@ class SecurityWorkflowTests(unittest.TestCase):
|
|||||||
|
|
||||||
def run_step(self, name: str) -> subprocess.CompletedProcess[str]:
|
def run_step(self, name: str) -> subprocess.CompletedProcess[str]:
|
||||||
lines = self.steps[name]
|
lines = self.steps[name]
|
||||||
start = lines.index(" run: |") + 1
|
|
||||||
shell_lines = []
|
|
||||||
for line in lines[start:]:
|
|
||||||
if line.strip() and not line.startswith(" "):
|
|
||||||
break
|
|
||||||
shell_lines.append(line[10:])
|
|
||||||
self.assertTrue(shell_lines, f"missing literal shell body: {name}")
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", self.render("\n".join(shell_lines))],
|
["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", self.render(shell_body(lines))],
|
||||||
cwd=self.directory, env={**self.env, **self.step_env(lines)}, capture_output=True, text=True,
|
cwd=self.directory, env={**self.env, **self.step_env(lines)}, capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@@ -193,5 +202,89 @@ class SecurityWorkflowTests(unittest.TestCase):
|
|||||||
self.assertIn("https://github.com/rustfs/rustfs/actions/runs/314159", body.read_text())
|
self.assertIn("https://github.com/rustfs/rustfs/actions/runs/314159", body.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionalWorkflowTests(unittest.TestCase):
|
||||||
|
JOBS = {
|
||||||
|
"kms": "kms-test", "storage": "storage-test", "s3-compat": "s3-compat-test",
|
||||||
|
"upgrade": "upgrade-test", "replication": "replication-test", "heal": "heal-test",
|
||||||
|
"tier": "tier-test", "pool-expand": "pool-expansion-test", "performance": "performance-test",
|
||||||
|
}
|
||||||
|
DIRECT_TESTS = {
|
||||||
|
"kms": "Run KMS suite", "storage": "Run storage engine suite",
|
||||||
|
"s3-compat": "Run S3 compatibility suite", "upgrade": "Run upgrade compatibility suite",
|
||||||
|
"replication": "Run replication suite",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_failure_and_always_step_wiring(self) -> None:
|
||||||
|
for suite, job_id in self.JOBS.items():
|
||||||
|
with self.subTest(suite=suite):
|
||||||
|
source = (ROOT / f".github/workflows/rustfs-{suite}-test.yml").read_text()
|
||||||
|
job = yaml_block(source.splitlines(), job_id, 2)
|
||||||
|
self.assertIsNotNone(job)
|
||||||
|
self.assertNotRegex("\n".join(job), r'''(?m)^ ["']?continue-on-error["']?\s*:''')
|
||||||
|
steps = named_steps(job)
|
||||||
|
if suite in self.DIRECT_TESTS:
|
||||||
|
test = steps[self.DIRECT_TESTS[suite]]
|
||||||
|
self.assertNotRegex("\n".join(test), r'''(?m)^ ["']?continue-on-error["']?\s*:''')
|
||||||
|
self.assertIn(" if: always()", steps["Generate report"])
|
||||||
|
cleanup = steps["Reset test environment (after)" if suite == "performance" else "Cleanup environment (after)"]
|
||||||
|
condition = next(line.strip() for line in cleanup if line.startswith(" if:"))
|
||||||
|
self.assertIn(condition, (
|
||||||
|
"if: always()",
|
||||||
|
"if: ${{ always() && inputs.cleanup_after != 'false' }}",
|
||||||
|
"if: ${{ always() && (inputs.cleanup_after != 'false' || github.event_name != 'workflow_dispatch') }}",
|
||||||
|
))
|
||||||
|
if suite != "performance":
|
||||||
|
handoff = steps["Chain complete"] if suite == "replication" else next(
|
||||||
|
value for name, value in steps.items() if name.startswith("Continue functional chain")
|
||||||
|
)
|
||||||
|
self.assertIn(" if: ${{ always() && github.event_name == 'repository_dispatch' }}", handoff)
|
||||||
|
|
||||||
|
def test_failed_suite_preserves_exit_and_cleanup_and_dispatch_execute(self) -> None:
|
||||||
|
for suite, test_name in self.DIRECT_TESTS.items():
|
||||||
|
with self.subTest(suite=suite), tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
(root / "auto-testing").mkdir()
|
||||||
|
script = root / f"auto-testing/rustfs-{suite}-test.sh"
|
||||||
|
script.write_text('#!/bin/sh\nprintf "partial suite diagnostics\\n"\nexit 17\n')
|
||||||
|
script.chmod(0o755)
|
||||||
|
fake_bin = root / "bin"
|
||||||
|
fake_bin.mkdir()
|
||||||
|
for command, marker in (("ssh", "cleanup"), ("gh", "dispatch")):
|
||||||
|
fake = fake_bin / command
|
||||||
|
fake.write_text(f'#!/bin/sh\nprintf "{marker}\\n" >> "$EXECUTED"\n')
|
||||||
|
fake.chmod(0o755)
|
||||||
|
env = {
|
||||||
|
**os.environ, "PATH": f"{fake_bin}{os.pathsep}{os.environ['PATH']}",
|
||||||
|
"EXECUTED": str(root / "executed"), "RUSTFS_NODES": "fixture-node",
|
||||||
|
"RUSTFS_SSH_USER": "fixture-user", "RUSTFS_NIGHTLY_PACKAGE_URL": "https://example.invalid/package.deb",
|
||||||
|
"GH_TOKEN": "local-fixture", "GITHUB_EVENT_NAME": "repository_dispatch", "GITHUB_RUN_ID": "314159",
|
||||||
|
}
|
||||||
|
source = (ROOT / f".github/workflows/rustfs-{suite}-test.yml").read_text()
|
||||||
|
steps = named_steps(yaml_block(source.splitlines(), self.JOBS[suite], 2))
|
||||||
|
context = {"github.event_name": "repository_dispatch", "steps.test.outcome": "failure"}
|
||||||
|
for expression in re.findall(r"\$\{\{\s*(.*?)\s*\}\}", source):
|
||||||
|
if expression.startswith("inputs.") and re.fullmatch(r"inputs\.\w+", expression):
|
||||||
|
context[expression] = ""
|
||||||
|
def execute(name):
|
||||||
|
lines = steps[name]
|
||||||
|
rendered = re.sub(r"\$\{\{\s*(.*?)\s*\}\}", lambda match: context[match[1]], shell_body(lines))
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", rendered],
|
||||||
|
cwd=root, env={**env, "LOG_FILE": str(root / "suite.log")}, capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
failed = execute(test_name)
|
||||||
|
self.assertEqual(failed.returncode, 17, failed.stderr)
|
||||||
|
self.assertIn("partial suite diagnostics", failed.stdout)
|
||||||
|
cleanup = execute("Cleanup environment (after)")
|
||||||
|
self.assertEqual(cleanup.returncode, 0, cleanup.stderr)
|
||||||
|
handoff_name = "Chain complete" if suite == "replication" else next(
|
||||||
|
name for name in steps if name.startswith("Continue functional chain")
|
||||||
|
)
|
||||||
|
handoff = execute(handoff_name)
|
||||||
|
self.assertEqual(handoff.returncode, 0, handoff.stderr)
|
||||||
|
markers = (root / "executed").read_text().splitlines()
|
||||||
|
self.assertEqual(markers, ["cleanup"] if suite == "replication" else ["cleanup", "dispatch"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user