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.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-02 16:08:28 +08:00
parent 9ed4588db3
commit 6b83ffa4f5
2 changed files with 64 additions and 7 deletions

View File

@ -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', () => {

View File

@ -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<LaneResult>
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 = {