From 6b83ffa4f52f2522fb31b19bbf736632b37dfb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 2 May 2026 16:08:28 +0800 Subject: [PATCH] Report the full quality gate impact Release quality gates are used as maintainer evidence, so a live baseline failure must not hide later provider or desktop-smoke lanes. Run every lane and summarize all pass/fail statuses so reviewers can tell whether the whole matrix was exercised. Constraint: Live model lanes can fail independently and may be slow, but release evidence needs complete coverage. Rejected: Keep fail-fast behavior | it makes reports ambiguous after the first live failure. Confidence: high Scope-risk: narrow Directive: Do not reintroduce fail-fast behavior for release gates without adding explicit unexecuted-lane reporting. Tested: bun test scripts/quality-gate/runner.test.ts scripts/quality-gate/baseline/cases.test.ts scripts/quality-gate/providerTargets.test.ts Tested: bun run quality:gate --mode release --allow-live --provider-model codingplan:main:codingplan-main --provider-model minimax:main:minimax-main Not-tested: clean-worktree release rerun before this commit; run immediately after committing. --- scripts/quality-gate/runner.test.ts | 54 +++++++++++++++++++++++++++-- scripts/quality-gate/runner.ts | 17 ++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/scripts/quality-gate/runner.test.ts b/scripts/quality-gate/runner.test.ts index f4db2237..b120f810 100644 --- a/scripts/quality-gate/runner.test.ts +++ b/scripts/quality-gate/runner.test.ts @@ -4,8 +4,8 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { lanesForMode } from './modes' import { renderMarkdownReport } from './reporter' -import { runQualityGate } from './runner' -import type { QualityGateReport } from './types' +import { runQualityGate, runQualityGateLanes } from './runner' +import type { LaneDefinition, QualityGateReport } from './types' describe('quality gate modes', () => { test('pr mode includes existing path-aware PR checks', () => { @@ -70,6 +70,56 @@ describe('runQualityGate', () => { rmSync(artifactsDir, { recursive: true, force: true }) } }) + + test('continues through later lanes after a failure so reports show full impact', async () => { + const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-')) + const lanes: LaneDefinition[] = [ + { + id: 'first-lane', + title: 'First lane', + description: 'Fails intentionally', + kind: 'command', + command: ['false'], + requiredForModes: ['pr'], + }, + { + id: 'second-lane', + title: 'Second lane', + description: 'Still runs', + kind: 'command', + command: ['true'], + requiredForModes: ['pr'], + }, + ] + + try { + const { report } = await runQualityGateLanes({ + mode: 'pr', + dryRun: false, + allowLive: false, + baselineTargets: [], + rootDir: process.cwd(), + artifactsDir, + runId: 'continue-after-failure-test', + }, lanes, async (lane) => ({ + id: lane.id, + title: lane.title, + status: lane.id === 'first-lane' ? 'failed' : 'passed', + command: lane.command, + durationMs: 1, + exitCode: lane.id === 'first-lane' ? 1 : 0, + })) + + expect(report.results.map((result) => result.id)).toEqual(['first-lane', 'second-lane']) + expect(report.summary).toEqual({ + passed: 1, + failed: 1, + skipped: 0, + }) + } finally { + rmSync(artifactsDir, { recursive: true, force: true }) + } + }) }) describe('renderMarkdownReport', () => { diff --git a/scripts/quality-gate/runner.ts b/scripts/quality-gate/runner.ts index f5e6b58c..f03990a4 100644 --- a/scripts/quality-gate/runner.ts +++ b/scripts/quality-gate/runner.ts @@ -7,6 +7,8 @@ import { lanesForMode } from './modes' import { writeReport } from './reporter' import type { LaneDefinition, LaneResult, QualityGateOptions, QualityGateReport } from './types' +type LaneExecutor = (lane: LaneDefinition, options: QualityGateOptions) => Promise + function nowId() { return new Date().toISOString().replace(/[:.]/g, '-') } @@ -140,6 +142,14 @@ function summarize(results: LaneResult[]) { } export async function runQualityGate(options: QualityGateOptions) { + return runQualityGateLanes(options, lanesForMode(options.mode, options.baselineTargets)) +} + +export async function runQualityGateLanes( + options: QualityGateOptions, + lanes: LaneDefinition[], + executeLane: LaneExecutor = runLane, +) { const runId = options.runId ?? nowId() const startedAt = new Date().toISOString() const artifactsRoot = options.artifactsDir ?? join(options.rootDir, 'artifacts', 'quality-runs') @@ -148,12 +158,9 @@ export async function runQualityGate(options: QualityGateOptions) { const runOptions = { ...options, runId, runOutputDir: outputDir } const results: LaneResult[] = [] - for (const lane of lanesForMode(options.mode, options.baselineTargets)) { - const result = await runLane(lane, runOptions) + for (const lane of lanes) { + const result = await executeLane(lane, runOptions) results.push(result) - if (result.status === 'failed') { - break - } } const report: QualityGateReport = {