mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix(ci): preserve rename boundaries in React Doctor gate * fix(ci): retain coverage and reject inconsistent audit reports * test(ci): update Git status fixture for rename-aware ranges
213 lines
8.0 KiB
JavaScript
213 lines
8.0 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { realpathSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
import { isDeepStrictEqual } from 'node:util'
|
|
import { collectAddedLineRanges, overlapsAddedLines } from './check-changed-code-quality.mjs'
|
|
import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs'
|
|
import { runAuditTool } from './run-audit-tool.mjs'
|
|
|
|
const SOURCE_FILE_PATTERN = /\.(?:astro|tsx?|jsx?|mts|mjs|[hH][tT][mM][lL])$/
|
|
|
|
function isCount(value) {
|
|
return Number.isSafeInteger(value) && value >= 0
|
|
}
|
|
|
|
function reportPath(root, file, directory = root) {
|
|
if (typeof file !== 'string' || file.length === 0) {
|
|
throw new Error('React Doctor returned an invalid file path.')
|
|
}
|
|
const absolute = file.startsWith('file://')
|
|
? fileURLToPath(file)
|
|
: path.resolve(directory, file.replaceAll('\\', '/'))
|
|
const canonical = realpathSync(absolute)
|
|
const relative = path.relative(root, canonical)
|
|
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
throw new Error('React Doctor returned a path outside the report root.')
|
|
}
|
|
return canonical
|
|
}
|
|
|
|
function readCompleteDiagnostics(stdout, root, comparisonBase) {
|
|
const report = JSON.parse(stdout)
|
|
if (
|
|
report?.schemaVersion !== 3 ||
|
|
report.ok !== true ||
|
|
report.error !== null ||
|
|
report.mode !== 'diff' ||
|
|
report.baselineDegraded ||
|
|
typeof report.version !== 'string' ||
|
|
report.version.length === 0 ||
|
|
!Number.isFinite(report.elapsedMilliseconds) ||
|
|
report.elapsedMilliseconds < 0 ||
|
|
!path.isAbsolute(report.directory ?? '') ||
|
|
reportPath(root, report.directory) !== root ||
|
|
report.diff?.baseBranch !== comparisonBase ||
|
|
!(report.diff.currentBranch === null || typeof report.diff.currentBranch === 'string') ||
|
|
!isCount(report.diff.changedFileCount) ||
|
|
typeof report.diff.isCurrentChanges !== 'boolean' ||
|
|
!Array.isArray(report.projects) ||
|
|
!Array.isArray(report.diagnostics) ||
|
|
(report.skippedProjects !== undefined &&
|
|
(!Array.isArray(report.skippedProjects) || report.skippedProjects.length > 0))
|
|
) {
|
|
throw new Error('React Doctor did not return a complete diff report.')
|
|
}
|
|
const diagnostics = []
|
|
const flattened = []
|
|
const analyzedPaths = new Set()
|
|
let affectedFiles = 0
|
|
for (const project of report.projects) {
|
|
if (
|
|
project?.complete !== true ||
|
|
!path.isAbsolute(project.directory ?? '') ||
|
|
!path.isAbsolute(project.packageRoot ?? '') ||
|
|
!Array.isArray(project.diagnostics) ||
|
|
!Array.isArray(project.skippedChecks) ||
|
|
project.skippedChecks.length > 0 ||
|
|
(project.skippedCheckReasons !== undefined &&
|
|
(!project.skippedCheckReasons ||
|
|
typeof project.skippedCheckReasons !== 'object' ||
|
|
Array.isArray(project.skippedCheckReasons) ||
|
|
Object.keys(project.skippedCheckReasons).length > 0)) ||
|
|
!Array.isArray(project.analyzedFiles) ||
|
|
!isCount(project.analyzedFileCount) ||
|
|
project.analyzedFileCount !== project.analyzedFiles.length ||
|
|
!isCount(project.scannedFileCount) ||
|
|
project.scannedFileCount !== project.analyzedFileCount
|
|
) {
|
|
throw new Error('React Doctor returned incomplete project coverage.')
|
|
}
|
|
reportPath(root, project.directory)
|
|
const packageRoot = reportPath(root, project.packageRoot)
|
|
const analyzedFiles = new Set(
|
|
project.analyzedFiles.map((file) => reportPath(root, file, packageRoot))
|
|
)
|
|
if (analyzedFiles.size !== project.analyzedFileCount) {
|
|
throw new Error('React Doctor returned duplicate analyzed paths.')
|
|
}
|
|
for (const file of analyzedFiles) {
|
|
analyzedPaths.add(file)
|
|
}
|
|
const diagnosedFiles = new Set()
|
|
for (const diagnostic of project.diagnostics) {
|
|
if (
|
|
!['filePath', 'normalizedFilePath', 'id', 'plugin', 'rule', 'message'].every(
|
|
(field) => typeof diagnostic?.[field] === 'string' && diagnostic[field].length > 0
|
|
) ||
|
|
!['error', 'warning'].includes(diagnostic.severity) ||
|
|
!isCount(diagnostic.line) ||
|
|
!isCount(diagnostic.column) ||
|
|
(diagnostic.endLine !== undefined &&
|
|
(!isCount(diagnostic.endLine) || diagnostic.endLine < diagnostic.line)) ||
|
|
['offset', 'length', 'endColumn'].some(
|
|
(field) => diagnostic[field] !== undefined && !isCount(diagnostic[field])
|
|
)
|
|
) {
|
|
throw new Error('React Doctor returned an invalid diagnostic identity or span.')
|
|
}
|
|
const absolute = reportPath(root, diagnostic.filePath, packageRoot)
|
|
const normalized = path.relative(packageRoot, absolute).split(path.sep).join('/')
|
|
const file = path.relative(root, absolute).split(path.sep).join('/')
|
|
const digest = createHash('sha256')
|
|
.update(JSON.stringify([diagnostic.severity, diagnostic.message]))
|
|
.digest('hex')
|
|
const identity = `${file}::${diagnostic.line}:${diagnostic.column}::${diagnostic.plugin}/${diagnostic.rule}::${digest}`
|
|
if (
|
|
diagnostic.normalizedFilePath !== normalized ||
|
|
diagnostic.id !== identity ||
|
|
!analyzedFiles.has(absolute)
|
|
) {
|
|
throw new Error('React Doctor returned inconsistent diagnostic paths or coverage.')
|
|
}
|
|
diagnosedFiles.add(normalized)
|
|
diagnostics.push({ diagnostic, file })
|
|
flattened.push(diagnostic)
|
|
}
|
|
affectedFiles += diagnosedFiles.size
|
|
}
|
|
const summary = report.summary
|
|
if (
|
|
!isDeepStrictEqual(report.diagnostics, flattened) ||
|
|
analyzedPaths.size > report.diff.changedFileCount ||
|
|
!summary ||
|
|
summary.totalDiagnosticCount !== flattened.length ||
|
|
summary.errorCount !==
|
|
flattened.filter((diagnostic) => diagnostic.severity === 'error').length ||
|
|
summary.warningCount !==
|
|
flattened.filter((diagnostic) => diagnostic.severity === 'warning').length ||
|
|
summary.affectedFileCount !== affectedFiles
|
|
) {
|
|
throw new Error('React Doctor returned inconsistent diagnostics or summary counts.')
|
|
}
|
|
return diagnostics
|
|
}
|
|
|
|
export function main(
|
|
root = process.cwd(),
|
|
requestedBase = process.argv.slice(2).find((argument) => argument !== '--') ??
|
|
process.env.ORCA_CODE_QUALITY_BASE ??
|
|
'origin/main',
|
|
run = runAuditTool
|
|
) {
|
|
root = realpathSync(root)
|
|
const base = resolvePullRequestDiffBase(root, requestedBase)
|
|
// Accept revision syntax, never shell fragments or option injection.
|
|
if (!/^(?!-)[A-Za-z0-9._/@^~-]+$/.test(base)) {
|
|
throw new Error(`Refusing to pass an unsafe diff base to React Doctor: ${base}`)
|
|
}
|
|
const { comparisonBase, rangesByFile } = collectAddedLineRanges(root, base, {
|
|
includePath: (file) => SOURCE_FILE_PATTERN.test(file)
|
|
})
|
|
const result = run(
|
|
'react-doctor',
|
|
[
|
|
'.',
|
|
'--yes',
|
|
'--scope',
|
|
'files',
|
|
'--base',
|
|
comparisonBase,
|
|
'--include-untracked',
|
|
'--no-dead-code',
|
|
'--no-supply-chain',
|
|
'--no-telemetry',
|
|
'--blocking',
|
|
'none',
|
|
'--json'
|
|
],
|
|
{ root, stdio: 'pipe' }
|
|
)
|
|
if (result.code !== 0 || result.signal || result.timedOut || result.outputTruncated) {
|
|
throw new Error(`React Doctor scan failed: ${result.stderr || result.signal || result.code}`)
|
|
}
|
|
const diagnostics = readCompleteDiagnostics(result.stdout, root, comparisonBase)
|
|
let failures = 0
|
|
for (const { diagnostic, file } of diagnostics) {
|
|
const ranges = rangesByFile.get(file)
|
|
if (
|
|
diagnostic.severity === 'error' &&
|
|
ranges &&
|
|
(diagnostic.line === 0 ||
|
|
overlapsAddedLines(diagnostic.line, diagnostic.endLine ?? diagnostic.line, ranges))
|
|
) {
|
|
console.error(
|
|
`${file}:${diagnostic.line} ${diagnostic.plugin}/${diagnostic.rule}: ${diagnostic.message}`
|
|
)
|
|
failures += 1
|
|
}
|
|
}
|
|
console.log(`React Doctor changed-lines gate: ${failures} error(s).`)
|
|
return failures > 0 ? 1 : 0
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
try {
|
|
process.exitCode = main()
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error)
|
|
process.exitCode = 1
|
|
}
|
|
}
|