From dafaf33309a6c77807110be0fbda2a2ebd1bf9cf Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:11:54 +0800 Subject: [PATCH] fix(ci): keep infrastructure failures visible in smoke verdicts --- .github/workflows/core-smoke.yml | 4 +++- tools/regression_ci.py | 12 +++++++++++- tools/regression_gate.py | 8 ++++++-- tools/test_regression_gate.py | 26 ++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/core-smoke.yml b/.github/workflows/core-smoke.yml index b9938391b..722fb3ae7 100644 --- a/.github/workflows/core-smoke.yml +++ b/.github/workflows/core-smoke.yml @@ -88,7 +88,9 @@ jobs: if path.exists(): data = json.loads(path.read_text()) out.write(f"Core smoke: {'PASS' if data['passed'] else 'FAIL'}; " - f"{data['elapsed_seconds']:.2f}/60 seconds; {data['requests']} requests.\n") + f"{data.get('elapsed_seconds', 0):.2f}/60 seconds; {data.get('requests', 0)} requests.\n") + if data.get('infrastructure_error'): + out.write('Preparation or teardown failed; inspect service logs.\n') out.write('Service/dependency preparation is outside the 60-second execution budget.\n') else: out.write('Core smoke did not produce a verdict; inspect preparation failure.\n') diff --git a/tools/regression_ci.py b/tools/regression_ci.py index c61481d7c..0d57d4585 100644 --- a/tools/regression_ci.py +++ b/tools/regression_ci.py @@ -216,7 +216,17 @@ def main(): parser.add_argument('--lbctl', type=Path, required=True) parser.add_argument('--plugins', type=Path, required=True) args = parser.parse_args() - result = run(args) + if args.output.exists(): + parser.error('--output must be a new directory') + try: + result = run(args) + except Exception as exc: + verdict = args.output / 'gate' / 'gate.json' + result = json.loads(verdict.read_text()) if verdict.exists() else {} + result.update(passed=False, infrastructure_error=f'{type(exc).__name__}: {exc}') + verdict.parent.mkdir(parents=True, exist_ok=True) + verdict.write_text(json.dumps(result, indent=2) + '\n') + raise print(json.dumps(result, indent=2)) raise SystemExit(0 if result['passed'] else 1) diff --git a/tools/regression_gate.py b/tools/regression_gate.py index 20e2b069a..2e3217b8b 100644 --- a/tools/regression_gate.py +++ b/tools/regression_gate.py @@ -10,7 +10,7 @@ import subprocess import sys import time -from regression_suite import validate +from regression_suite import Suite, validate from regression_traffic import read_process_snapshot @@ -84,6 +84,10 @@ def main(): validate(profile) if 'smoke' not in profile: parser.error('a smoke profile is required') + suite = Suite(profile, args.profile.resolve(), args.output / 'suite') + pids = dict(profile.get('observe', {}).get('pids', {})) + for name, path in profile.get('observe', {}).get('pid_files', {}).items(): + pids[name] = int(Path(suite.path(path)).read_text().strip()) result = supervise( [ sys.executable, @@ -96,7 +100,7 @@ def main(): ], args.output, 60, - profile.get('observe', {}).get('pids', {}), + pids, ) print(json.dumps(result, indent=2)) raise SystemExit(0 if result['passed'] else 1) diff --git a/tools/test_regression_gate.py b/tools/test_regression_gate.py index 95aa14d58..360796f8d 100644 --- a/tools/test_regression_gate.py +++ b/tools/test_regression_gate.py @@ -1,10 +1,12 @@ """A gate must fail closed on timeout, missing verdict, and inner failure.""" import os +import json from pathlib import Path import sys import tempfile import unittest +from unittest.mock import patch from regression_gate import supervise @@ -30,6 +32,30 @@ class GateTest(unittest.TestCase): self.assertTrue(result['timed_out']) self.assertLess(result['elapsed_seconds'], 2) + def test_teardown_failure_overrides_successful_smoke_verdict(self): + from regression_ci import main + + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / 'ci' + + def fail_teardown(args): + verdict = args.output / 'gate' / 'gate.json' + verdict.parent.mkdir(parents=True) + verdict.write_text(json.dumps({'passed': True})) + raise RuntimeError('sandbox teardown failed') + + with ( + patch('regression_ci.run', side_effect=fail_teardown), + patch.object( + sys, 'argv', ['regression_ci', '--output', str(output), '--lbctl', 'unused', '--plugins', 'unused'] + ), + self.assertRaises(RuntimeError), + ): + main() + result = json.loads((output / 'gate/gate.json').read_text()) + self.assertFalse(result['passed']) + self.assertIn('sandbox teardown failed', result['infrastructure_error']) + def test_zero_exit_without_business_verdict_is_failed(self): with tempfile.TemporaryDirectory() as directory: result = supervise([sys.executable, '-c', 'pass'], Path(directory) / 'gate', 5, {})