程序员阿江(Relakkes) 3489157bad Keep default pre-push checks fast for contributors
The pre-push hook already treats live model smoke as optional at runtime, but an existing local config can keep it enabled after reinstalling the hook. Default hook installation now writes the fast local setting explicitly unless a provider selector is supplied, so fresh forks and repeated installs stay on the PR gate path by default while release maintainers can still opt into live smoke.

Constraint: Daily contributor pushes should not depend on real provider access or long desktop smoke runs
Rejected: Remove live smoke lanes entirely | release and provider changes still need an explicit high-confidence validation path
Confidence: high
Scope-risk: narrow
Directive: Keep live provider and desktop smoke opt-in for push hooks; use quality:smoke or baseline gates for release validation
Tested: bun test scripts/git-hooks/install.test.ts
Tested: bun run check:policy
Not-tested: Full bun run verify before commit; push hook will run the non-live PR gate
2026-05-16 19:04:48 +08:00

217 lines
6.8 KiB
TypeScript
Executable File

#!/usr/bin/env bun
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
type LiveMode = 'smoke' | 'baseline'
export type InstallPrePushHookOptions = {
rootDir?: string
sourcePath?: string
hookPath?: string
force?: boolean
allowCliCoreChange?: boolean
allowCoverageBaselineChange?: boolean
allowMissingTests?: boolean
liveProviderModels?: string[]
liveMode?: LiveMode
live?: boolean
}
export type InstallPrePushHookResult = {
hookPath: string
liveConfigured: boolean
}
function decode(buffer: ArrayBuffer | Uint8Array) {
return new TextDecoder().decode(buffer).trim()
}
function runGit(rootDir: string, args: string[]) {
const proc = Bun.spawnSync(['git', ...args], {
cwd: rootDir,
stdout: 'pipe',
stderr: 'pipe',
})
if (proc.exitCode !== 0) {
throw new Error(decode(proc.stderr) || decode(proc.stdout) || `git ${args.join(' ')} failed`)
}
return decode(proc.stdout)
}
function gitHookPath(rootDir: string) {
return resolve(rootDir, runGit(rootDir, ['rev-parse', '--git-path', 'hooks/pre-push']))
}
function gitConfig(rootDir: string, key: string, value: string) {
runGit(rootDir, ['config', '--local', key, value])
}
function sameFileContent(leftPath: string, rightPath: string) {
return existsSync(leftPath) && readFileSync(leftPath, 'utf8') === readFileSync(rightPath, 'utf8')
}
export function installPrePushHook(options: InstallPrePushHookOptions = {}): InstallPrePushHookResult {
const rootDir = options.rootDir ? resolve(options.rootDir) : process.cwd()
const sourcePath = options.sourcePath ? resolve(options.sourcePath) : resolve(rootDir, 'scripts/git-hooks/pre-push')
const hookPath = options.hookPath ? resolve(options.hookPath) : gitHookPath(rootDir)
if (!existsSync(sourcePath)) {
throw new Error(`Pre-push hook source not found: ${sourcePath}`)
}
if (existsSync(hookPath) && !options.force && !sameFileContent(hookPath, sourcePath)) {
throw new Error(`Refusing to overwrite existing hook at ${hookPath}. Re-run with --force after reviewing it.`)
}
mkdirSync(dirname(hookPath), { recursive: true })
copyFileSync(sourcePath, hookPath)
chmodSync(hookPath, 0o755)
if (options.allowCliCoreChange) {
gitConfig(rootDir, 'quality.allowCliCoreChange', 'true')
}
if (options.allowCoverageBaselineChange) {
gitConfig(rootDir, 'quality.allowCoverageBaselineChange', 'true')
}
if (options.allowMissingTests) {
gitConfig(rootDir, 'quality.allowMissingTests', 'true')
}
const shouldWriteGitConfig = !options.hookPath
const shouldEnableLive = options.live !== false && Boolean(options.liveProviderModels?.length)
if (shouldWriteGitConfig && shouldEnableLive) {
gitConfig(rootDir, 'quality.prePushLive', 'true')
gitConfig(rootDir, 'quality.prePushProviderModels', options.liveProviderModels?.join(' ') ?? '')
} else if (shouldWriteGitConfig) {
gitConfig(rootDir, 'quality.prePushLive', 'false')
}
if (options.liveMode) {
gitConfig(rootDir, 'quality.prePushLiveMode', options.liveMode)
}
return {
hookPath,
liveConfigured: Boolean(options.liveProviderModels?.length || options.liveMode || options.live === false),
}
}
type ParsedArgs = {
force: boolean
allowCliCoreChange: boolean
allowCoverageBaselineChange: boolean
allowMissingTests: boolean
liveProviderModels: string[]
liveMode?: LiveMode
live?: boolean
help: boolean
}
function parseArgs(argv: string[]): ParsedArgs {
const parsed: ParsedArgs = {
force: false,
allowCliCoreChange: false,
allowCoverageBaselineChange: false,
allowMissingTests: false,
liveProviderModels: [],
help: false,
}
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index]
const next = argv[index + 1]
if (arg === '--force') {
parsed.force = true
} else if (arg === '--allow-cli-core-change') {
parsed.allowCliCoreChange = true
} else if (arg === '--allow-coverage-baseline-change') {
parsed.allowCoverageBaselineChange = true
} else if (arg === '--allow-missing-tests') {
parsed.allowMissingTests = true
} else if (arg === '--no-live') {
parsed.live = false
} else if (arg === '--live-provider-model') {
if (!next || next.startsWith('--')) {
throw new Error('--live-provider-model requires a provider:model[:label] value')
}
parsed.liveProviderModels.push(next)
index += 1
} else if (arg === '--live-mode') {
if (next !== 'smoke' && next !== 'baseline') {
throw new Error('--live-mode must be smoke or baseline')
}
parsed.liveMode = next
index += 1
} else if (arg === '--help' || arg === '-h') {
parsed.help = true
} else {
throw new Error(`Unknown option: ${arg}`)
}
}
return parsed
}
function printHelp() {
console.log(`Install the repository pre-push quality gate.
Usage:
bun run hooks:install [-- --force] [-- --no-live] [-- --live-provider-model <selector>] [-- --live-mode smoke|baseline]
bun run hooks:install -- --allow-cli-core-change --allow-coverage-baseline-change
Examples:
bun run hooks:install
bun run hooks:install -- --no-live
bun run quality:providers
bun run hooks:install -- --live-provider-model codingplan:main:codingplan-main
bun run hooks:install -- --live-provider-model codingplan:main:codingplan-main --live-mode baseline
bun run hooks:install -- --allow-cli-core-change --allow-coverage-baseline-change
`)
}
if (import.meta.main) {
try {
const args = parseArgs(process.argv.slice(2))
if (args.help) {
printHelp()
process.exit(0)
}
const result = installPrePushHook({
force: args.force,
allowCliCoreChange: args.allowCliCoreChange,
allowCoverageBaselineChange: args.allowCoverageBaselineChange,
allowMissingTests: args.allowMissingTests,
liveProviderModels: args.liveProviderModels,
liveMode: args.liveMode,
live: args.live,
})
console.log(`Installed pre-push quality gate: ${result.hookPath}`)
console.log('Every git push now runs: bun run quality:pr')
if (args.liveProviderModels.length > 0) {
console.log(`Live ${args.liveMode ?? 'smoke'} gate is enabled for ${args.liveProviderModels.length} provider selector(s).`)
} else if (args.live === false) {
console.log('Live model gate is disabled in local git config.')
} else {
console.log('Live model gate is disabled. Enable it with --live-provider-model after running bun run quality:providers.')
}
if (args.allowCliCoreChange || args.allowCoverageBaselineChange || args.allowMissingTests) {
console.log('Local maintainer override config was updated for this clone.')
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
}