From a7341abdbdae760c5a513a3b982e33d27d7412a4 Mon Sep 17 00:00:00 2001 From: hector <42570491+majinghe@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:32:38 +0800 Subject: [PATCH] ci(chain): stop gating all suites on the performance runner (#7924) ci(chain): gate the performance lane on its fleet probe (rebase + contract) Rebased onto latest main; adds the contract-test update that the first push of this branch missed: - test_functional_chain: the performance lane now asserts the new probe-gated 'if' (prepare success AND performance_ready == online); the other nine lanes keep the plain prepare gate; prepare must carry the two preflight steps (hard smoke-testing check, pf-testing probe) and publish the performance_ready output Carried over unchanged from the previous push: per-fleet preflight (prepare hard-fails only on smoke-testing), the pf-testing probe step, the probe-gated performance lane, and aggregate's --allow-skipped (permitted skips shrink the evidence set; unpermitted skips still fail aggregation). Chain contract tests: 27/27 locally. --- .github/workflows/rustfs-functional-chain.yml | 23 +++++++++++++-- scripts/functional_chain_evidence.py | 28 ++++++++++++------- scripts/test_functional_chain.py | 12 +++++++- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/rustfs-functional-chain.yml b/.github/workflows/rustfs-functional-chain.yml index 5a300b463..2cd07c210 100644 --- a/.github/workflows/rustfs-functional-chain.yml +++ b/.github/workflows/rustfs-functional-chain.yml @@ -46,6 +46,7 @@ jobs: timeout-minutes: 10 outputs: manifest: ${{ steps.candidate.outputs.manifest }} + performance_ready: ${{ steps.perf_probe.outputs.performance_ready }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: @@ -65,10 +66,25 @@ jobs: path: ${{ runner.temp }}/chain-candidate.json if-no-files-found: error - - name: Check shared functional runners before scheduling suites + - name: Check shared functional fleet runner before scheduling suites + # The nine functional suites run on the smoke-testing fleet; a hard + # requirement. The performance lane runs on pf-testing, which is a + # separate fleet - probe it without failing the whole chain so an + # offline performance runner skips only the performance lane. env: GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }} - run: python3 scripts/check_functional_runners.py smoke-testing pf-testing + run: python3 scripts/check_functional_runners.py smoke-testing + + - name: Probe performance fleet runner + id: perf_probe + env: + GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }} + run: | + if python3 scripts/check_functional_runners.py pf-testing; then + echo "performance_ready=online" >> "$GITHUB_OUTPUT" + else + echo "performance_ready=offline" >> "$GITHUB_OUTPUT" + fi upgrade: needs: [prepare] @@ -144,7 +160,7 @@ jobs: performance: needs: [prepare, replication] - if: ${{ always() && needs.prepare.result == 'success' }} + if: ${{ always() && needs.prepare.result == 'success' && needs.prepare.outputs.performance_ready == 'online' }} uses: ./.github/workflows/rustfs-performance-test.yml with: chain_manifest: ${{ needs.prepare.outputs.manifest }} @@ -192,6 +208,7 @@ jobs: CHAIN_NEEDS: ${{ toJSON(needs) }} run: >- python3 scripts/functional_chain_evidence.py aggregate + --allow-skipped "${{ needs.prepare.outputs.performance_ready == 'online' && '' || 'performance' }}" --directory "${RUNNER_TEMP}/chain-evidence" --output "${RUNNER_TEMP}/chain-complete.json" - name: Upload complete-chain evidence diff --git a/scripts/functional_chain_evidence.py b/scripts/functional_chain_evidence.py index e768ad91e..e36e4abab 100644 --- a/scripts/functional_chain_evidence.py +++ b/scripts/functional_chain_evidence.py @@ -162,18 +162,24 @@ def render_summary(result): return "\n".join(lines) + "\n" -def aggregate(chain, directory, needs): +def aggregate(chain, directory, needs, allow_skipped=()): + allowed = {name for name in allow_skipped if name} + require(allowed <= set(SUITES), "aggregate allow-list names an unknown lane") require(set(needs) == set(SUITES), "aggregate is missing a required lane") - require(all(value.get("result") == "success" for value in needs.values()), "a required suite did not succeed") - require({path.name for path in directory.iterdir()} == {suite + ".json" for suite in SUITES}, "missing or unexpected suite evidence") - records = [json.loads((directory / (suite + ".json")).read_text()) for suite in SUITES] - validate_records(chain, records) - return {"schema": 1, "chain": chain, "suites": records, "complete": True, "completed_at": datetime.now(timezone.utc).isoformat()} + skipped = {name for name, value in needs.items() if value.get("result") == "skipped"} + require(skipped <= allowed, "a lane was skipped without preflight permission: " + ", ".join(sorted(skipped - allowed))) + require(all(value.get("result") == "success" for name, value in needs.items() if name not in skipped), "a required suite did not succeed") + expected = [suite for suite in SUITES if suite not in skipped] + require({path.name for path in directory.iterdir()} == {suite + ".json" for suite in expected}, "missing or unexpected suite evidence") + records = [json.loads((directory / (suite + ".json")).read_text()) for suite in expected] + validate_records(chain, records, expected) + return {"schema": 1, "chain": chain, "suites": records, "complete": True, + "skipped_lanes": sorted(skipped), "completed_at": datetime.now(timezone.utc).isoformat()} -def validate_records(chain, records): - require(isinstance(records, list) and len(records) == len(SUITES), "missing suite evidence") - require([record.get("suite") for record in records] == list(SUITES), "missing, duplicate or reordered suite evidence") +def validate_records(chain, records, expected_suites=SUITES): + require(isinstance(records, list) and len(records) == len(expected_suites), "missing suite evidence") + require([record.get("suite") for record in records] == list(expected_suites), "missing, duplicate or reordered suite evidence") for suite, result in zip(SUITES, records): require(type(result.get("schema")) is int and result["schema"] == 1 and result.get("suite") == suite and result.get("chain") == chain, "suite evidence identity mismatch") require(result.get("valid") is True and sha(result.get("report_sha256"), 64), "suite evidence is invalid") @@ -189,6 +195,8 @@ def main(): parser.add_argument("--report", type=Path) parser.add_argument("--output", type=Path) parser.add_argument("--directory", type=Path) + parser.add_argument("--allow-skipped", default="", + help="comma-separated lanes the preflight deliberately skipped (e.g. performance)") args = parser.parse_args() if args.mode == "summarize": chain = current_chain() if os.environ.get("CHAIN_MANIFEST") else None @@ -207,7 +215,7 @@ def main(): else: needs = json.loads(os.environ["CHAIN_NEEDS"]) needs.pop("prepare", None) - result = aggregate(chain, args.directory, needs) + result = aggregate(chain, args.directory, needs, args.allow_skipped.split(",")) args.output.write_text(json.dumps(result, sort_keys=True) + "\n") diff --git a/scripts/test_functional_chain.py b/scripts/test_functional_chain.py index bda4da5d1..b0c8e50c0 100644 --- a/scripts/test_functional_chain.py +++ b/scripts/test_functional_chain.py @@ -239,7 +239,11 @@ class EnvelopeTests(unittest.TestCase): for suite in evidence.SUITES: job = "\n".join(yaml_block(lines, suite, 2)) self.assertIn("needs: [prepare" + (", " + previous if previous else "") + "]", job) - self.assertIn("if: ${{ always() && needs.prepare.result == 'success' }}", job) + if suite == "performance": + # gated on the preflight probe: runs only when its fleet is online + self.assertIn("if: ${{ always() && needs.prepare.result == 'success' && needs.prepare.outputs.performance_ready == 'online' }}", job) + else: + self.assertIn("if: ${{ always() && needs.prepare.result == 'success' }}", job) self.assertIn("chain_manifest: ${{ needs.prepare.outputs.manifest }}", job) previous = suite complete = "\n".join(yaml_block(lines, "complete-chain", 2)) @@ -248,6 +252,12 @@ class EnvelopeTests(unittest.TestCase): self.assertIn("functional_chain_evidence.py summarize", complete) self.assertIn("functional-chain-report-", complete) self.assertIn("needs.prepare.result != 'skipped'", complete) + prepare = "\n".join(yaml_block(lines, "prepare", 2)) + self.assertIn("Check shared functional fleet runner before scheduling suites", prepare) + self.assertIn("check_functional_runners.py smoke-testing", prepare) + self.assertIn("Probe performance fleet runner", prepare) + self.assertIn("check_functional_runners.py pf-testing", prepare) + self.assertIn("performance_ready: ${{ steps.perf_probe.outputs.performance_ready }}", prepare) def test_every_lane_retains_failed_evidence_and_deduplicates_its_own_attempt(self): paths = list((candidate.ROOT / ".github/workflows").glob("rustfs-*-test.yml"))