mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix(ci): align local verify with PR policy
Fail PR and release quality runs when the impact policy blocks the diff, and accept root-runtime regression coverage across service and utility seams. Tested: bun run check:policy Confidence: high Scope-risk: narrow
This commit is contained in:
parent
435e4ccc9f
commit
8cada04d10
@ -171,6 +171,19 @@ describe('evaluateChangePolicy', () => {
|
|||||||
expect(covered.missingTestSignals).toEqual([])
|
expect(covered.missingTestSignals).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('accepts root runtime regression tests across service and utility seams', () => {
|
||||||
|
const result = evaluateChangePolicy(
|
||||||
|
[
|
||||||
|
'src/utils/messages.ts',
|
||||||
|
'src/services/api/streamRetry.test.ts',
|
||||||
|
],
|
||||||
|
['allow-cli-core-change'],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.missingTestSignals).toEqual([])
|
||||||
|
expect(result.blocked).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test('blocks coverage baseline and threshold changes without maintainer override', () => {
|
test('blocks coverage baseline and threshold changes without maintainer override', () => {
|
||||||
const result = evaluateChangePolicy([
|
const result = evaluateChangePolicy([
|
||||||
'scripts/quality-gate/coverage-baseline.json',
|
'scripts/quality-gate/coverage-baseline.json',
|
||||||
|
|||||||
@ -249,15 +249,9 @@ function missingTestSignals(files: string[]) {
|
|||||||
const desktopProd = changedProductionFiles(files, (file) => file.startsWith('desktop/src/'))
|
const desktopProd = changedProductionFiles(files, (file) => file.startsWith('desktop/src/'))
|
||||||
const serverProd = changedProductionFiles(files, (file) => file.startsWith('src/server/'))
|
const serverProd = changedProductionFiles(files, (file) => file.startsWith('src/server/'))
|
||||||
const adapterProd = changedProductionFiles(files, (file) => file.startsWith('adapters/'))
|
const adapterProd = changedProductionFiles(files, (file) => file.startsWith('adapters/'))
|
||||||
const agentRuntimeProd = changedProductionFiles(files, (file) => (
|
|
||||||
file.startsWith('src/tools/') ||
|
|
||||||
file.startsWith('src/utils/')
|
|
||||||
))
|
|
||||||
const rootRuntimeProd = changedProductionFiles(files, (file) => (
|
const rootRuntimeProd = changedProductionFiles(files, (file) => (
|
||||||
file.startsWith('src/') &&
|
file.startsWith('src/') &&
|
||||||
!file.startsWith('src/server/') &&
|
!file.startsWith('src/server/')
|
||||||
!file.startsWith('src/tools/') &&
|
|
||||||
!file.startsWith('src/utils/')
|
|
||||||
))
|
))
|
||||||
|
|
||||||
if (desktopProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('desktop/src/'))) {
|
if (desktopProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('desktop/src/'))) {
|
||||||
@ -269,14 +263,9 @@ function missingTestSignals(files: string[]) {
|
|||||||
if (adapterProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('adapters/'))) {
|
if (adapterProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('adapters/'))) {
|
||||||
signals.push('Adapter product files changed without an adapter test file in the PR.')
|
signals.push('Adapter product files changed without an adapter test file in the PR.')
|
||||||
}
|
}
|
||||||
if (agentRuntimeProd.length > 0 && !hasMatchingTest(files, (file) => file.startsWith('src/tools/') || file.startsWith('src/utils/'))) {
|
|
||||||
signals.push('Agent/runtime product files changed without a tools/utils test file in the PR.')
|
|
||||||
}
|
|
||||||
if (rootRuntimeProd.length > 0 && !hasMatchingTest(files, (file) => (
|
if (rootRuntimeProd.length > 0 && !hasMatchingTest(files, (file) => (
|
||||||
file.startsWith('src/') &&
|
file.startsWith('src/') &&
|
||||||
!file.startsWith('src/server/') &&
|
!file.startsWith('src/server/')
|
||||||
!file.startsWith('src/tools/') &&
|
|
||||||
!file.startsWith('src/utils/')
|
|
||||||
))) {
|
))) {
|
||||||
signals.push('Root runtime product files changed without a matching root runtime test file in the PR.')
|
signals.push('Root runtime product files changed without a matching root runtime test file in the PR.')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -433,6 +433,52 @@ describe('runQualityGate', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('fails PR mode when the impact report is blocked by policy', async () => {
|
||||||
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
||||||
|
const lanes: LaneDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'impact-report',
|
||||||
|
title: 'Impact report',
|
||||||
|
description: 'Writes a blocked policy decision',
|
||||||
|
kind: 'command',
|
||||||
|
command: ['bash', '-lc', [
|
||||||
|
'printf "%s\\n"',
|
||||||
|
'"# PR impact report"',
|
||||||
|
'""',
|
||||||
|
'"Changed files: 1"',
|
||||||
|
'"Areas: cli-core"',
|
||||||
|
'"Labels: none"',
|
||||||
|
'"Blocked: yes"',
|
||||||
|
'"Blocking reasons:"',
|
||||||
|
'"- CLI core changes require maintainer approval."',
|
||||||
|
'""',
|
||||||
|
'"## Required local checks"',
|
||||||
|
'"- bun run check:server"',
|
||||||
|
].join(' ')],
|
||||||
|
requiredForModes: ['pr'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { report } = await runQualityGateLanes({
|
||||||
|
mode: 'pr',
|
||||||
|
dryRun: false,
|
||||||
|
allowLive: false,
|
||||||
|
baselineTargets: [],
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
artifactsDir,
|
||||||
|
runId: 'blocked-impact-test',
|
||||||
|
}, lanes)
|
||||||
|
|
||||||
|
expect(report.impact?.blocked).toBe(true)
|
||||||
|
expect(report.results[0].status).toBe('failed')
|
||||||
|
expect(report.results[0].error).toContain('change policy blocked')
|
||||||
|
expect(report.summary.failed).toBe(1)
|
||||||
|
} finally {
|
||||||
|
rmSync(artifactsDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('release mode treats skipped live lanes as failures', async () => {
|
test('release mode treats skipped live lanes as failures', async () => {
|
||||||
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
const artifactsDir = mkdtempSync(join(tmpdir(), 'quality-gate-test-'))
|
||||||
const lanes: LaneDefinition[] = [
|
const lanes: LaneDefinition[] = [
|
||||||
|
|||||||
@ -432,6 +432,33 @@ function enforceReleaseLiveLanes(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceBlockedImpact(
|
||||||
|
options: QualityGateOptions,
|
||||||
|
results: LaneResult[],
|
||||||
|
) {
|
||||||
|
if (options.dryRun || (options.mode !== 'pr' && options.mode !== 'release')) {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
const impact = parseImpactSummary(results)
|
||||||
|
if (!impact?.blocked) {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map((result) => {
|
||||||
|
if (result.id !== 'impact-report' || result.status === 'failed') {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
status: 'failed' as const,
|
||||||
|
exitCode: result.exitCode === 0 ? 1 : result.exitCode,
|
||||||
|
error: 'Local change policy blocked this diff; inspect the impact report and use maintainer overrides only when explicitly approved.',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function runQualityGate(options: QualityGateOptions) {
|
export async function runQualityGate(options: QualityGateOptions) {
|
||||||
return runQualityGateLanes(options, lanesForMode(options.mode, options.baselineTargets))
|
return runQualityGateLanes(options, lanesForMode(options.mode, options.baselineTargets))
|
||||||
}
|
}
|
||||||
@ -454,7 +481,8 @@ export async function runQualityGateLanes(
|
|||||||
const result = await executeLane(lane, runOptions)
|
const result = await executeLane(lane, runOptions)
|
||||||
rawResults.push(withLaneMetadata(lane, result))
|
rawResults.push(withLaneMetadata(lane, result))
|
||||||
}
|
}
|
||||||
const results = enforceReleaseLiveLanes(options, selectedLanes, rawResults)
|
const releaseResults = enforceReleaseLiveLanes(options, selectedLanes, rawResults)
|
||||||
|
const results = enforceBlockedImpact(options, releaseResults)
|
||||||
|
|
||||||
const report: QualityGateReport = {
|
const report: QualityGateReport = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user