mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
The repository now has a measurable PR quality path instead of a loose set of manual checks. Coverage, quarantine governance, provider smoke, desktop smoke, and workflow wiring all produce durable reports that contributors and maintainers can inspect without reconstructing terminal output. This also fixes the desktop smoke current-runtime path so browser-driven smoke runs use the desktop default active provider instead of forcing the official current model, and records that runtime decision as an artifact. Constraint: Default PR gates must remain non-live and contributor-safe while live model checks stay explicit. Constraint: Release packaging is still GitHub Actions based, so release preflight must run before the build matrix. Rejected: Make live provider or desktop smoke mandatory on every PR | secrets, quotas, and model availability are maintainer-controlled. Rejected: Let PRs lower coverage baselines in the same change | base-branch ratchet comparison must remain authoritative. Confidence: high Scope-risk: moderate Directive: Do not relax coverage or quarantine policy without a maintainer approval label and a fresh quality report. Tested: ALLOW_CLI_CORE_CHANGE=1 ALLOW_COVERAGE_BASELINE_CHANGE=1 bun run quality:gate --mode pr Tested: bun run quality:gate --mode baseline --allow-live --only provider-smoke:* --provider-model nvidia-custom:main:nvidia-custom-main --artifacts-dir /tmp/quality-gate-live-smoke Tested: bun run quality:gate --mode baseline --allow-live --only desktop-smoke:* --provider-model current:current:current-runtime --artifacts-dir /tmp/quality-gate-desktop-smoke-fixed Tested: git diff --check Not-tested: Full live release mode with multiple providers in hosted CI; provider credentials and quota remain maintainer-controlled.
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { evaluateChangePolicy } from './change-policy'
|
|
|
|
async function run(cmd: string[], options: { optional?: boolean } = {}) {
|
|
console.log(`\n$ ${cmd.join(' ')}`)
|
|
const proc = Bun.spawn(cmd, {
|
|
stdout: 'inherit',
|
|
stderr: 'inherit',
|
|
})
|
|
const code = await proc.exited
|
|
|
|
if (code !== 0 && !options.optional) {
|
|
process.exit(code)
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
async function output(cmd: string[]) {
|
|
const proc = Bun.spawn(cmd, {
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
})
|
|
const stdout = await new Response(proc.stdout).text()
|
|
const stderr = await new Response(proc.stderr).text()
|
|
const code = await proc.exited
|
|
|
|
if (code !== 0) {
|
|
throw new Error(stderr || stdout || `Command failed: ${cmd.join(' ')}`)
|
|
}
|
|
|
|
return stdout.trim()
|
|
}
|
|
|
|
async function outputOrEmpty(cmd: string[]) {
|
|
try {
|
|
return await output(cmd)
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
async function localChangedFiles() {
|
|
const staged = await outputOrEmpty(['git', 'diff', '--name-only', '--cached'])
|
|
const unstaged = await outputOrEmpty(['git', 'diff', '--name-only'])
|
|
const untracked = await outputOrEmpty(['git', 'ls-files', '--others', '--exclude-standard'])
|
|
|
|
return [...staged.split(/\r?\n/), ...unstaged.split(/\r?\n/), ...untracked.split(/\r?\n/)]
|
|
.filter(Boolean)
|
|
}
|
|
|
|
async function changedFiles() {
|
|
const explicit = process.argv.slice(2).filter((arg) => !arg.startsWith('--'))
|
|
if (explicit.length > 0) {
|
|
return explicit
|
|
}
|
|
|
|
const base = process.env.PR_BASE_REF ?? 'origin/main'
|
|
const localFiles = await localChangedFiles()
|
|
try {
|
|
const diff = await output(['git', 'diff', '--name-only', `${base}...HEAD`])
|
|
return [...new Set([...diff.split(/\r?\n/), ...localFiles].filter(Boolean))]
|
|
} catch {
|
|
try {
|
|
const diff = await output(['git', 'diff', '--name-only', 'main...HEAD'])
|
|
return [...new Set([...diff.split(/\r?\n/), ...localFiles].filter(Boolean))]
|
|
} catch {
|
|
return [...new Set(localFiles)]
|
|
}
|
|
}
|
|
}
|
|
|
|
const files = await changedFiles()
|
|
const labels = process.env.PR_LABELS?.split(',').map((label) => label.trim()).filter(Boolean) ?? []
|
|
if (process.env.ALLOW_CLI_CORE_CHANGE === '1' && !labels.includes('allow-cli-core-change')) {
|
|
labels.push('allow-cli-core-change')
|
|
}
|
|
if (process.env.ALLOW_MISSING_TESTS === '1' && !labels.includes('allow-missing-tests')) {
|
|
labels.push('allow-missing-tests')
|
|
}
|
|
if (process.env.ALLOW_COVERAGE_BASELINE_CHANGE === '1' && !labels.includes('allow-coverage-baseline-change')) {
|
|
labels.push('allow-coverage-baseline-change')
|
|
}
|
|
|
|
const result = evaluateChangePolicy(files, labels)
|
|
|
|
console.log('PR local check plan')
|
|
console.log(` Files: ${files.length}`)
|
|
console.log(` Areas: ${result.areas.length ? result.areas.join(', ') : 'none'}`)
|
|
|
|
if (result.blockingReason) {
|
|
console.error('\nBlocked:')
|
|
for (const reason of result.blockingReasons) {
|
|
console.error(`- ${reason}`)
|
|
}
|
|
console.error('Use ALLOW_CLI_CORE_CHANGE=1, ALLOW_MISSING_TESTS=1, or ALLOW_COVERAGE_BASELINE_CHANGE=1 only after maintainer approval.')
|
|
process.exit(1)
|
|
}
|
|
|
|
await run(['bun', 'run', 'check:policy'])
|
|
|
|
if (result.checks.desktop) {
|
|
await run(['bun', 'run', 'check:desktop'])
|
|
}
|
|
|
|
if (result.checks.server) {
|
|
await run(['bun', 'run', 'check:server'])
|
|
}
|
|
|
|
if (result.checks.adapters) {
|
|
await run(['bun', 'run', 'check:adapters'])
|
|
}
|
|
|
|
if (result.checks.desktopNative) {
|
|
await run(['bun', 'run', 'check:native'])
|
|
}
|
|
|
|
if (result.checks.docs) {
|
|
await run(['bun', 'run', 'check:docs'])
|
|
}
|
|
|
|
console.log('\nPR local checks completed.')
|