mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
235 lines
9.1 KiB
JavaScript
235 lines
9.1 KiB
JavaScript
import { affectsLocalizationExtraction } from './localization-extraction-change-scope.mjs'
|
|
import { classifyPrJobs, isDocsOnlyPath } from './pr-code-change-scope.mjs'
|
|
import { listRatchetScripts } from './run-ratchets.mjs'
|
|
|
|
// Any of these can change what every check reports, so they select all of them.
|
|
const SELECT_ALL_FILES = new Set(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
|
|
const SELECT_ALL_PREFIXES = ['config/scripts/check-changed', 'config/scripts/pr-code-change-scope']
|
|
|
|
// Mirrors forceRerunTriggers in config/vitest.config.ts: one such path in a `related` run selects
|
|
// every test. That full run is CI's job; locally these paths run only their own colocated tests.
|
|
const FULL_SUITE_TRIGGER =
|
|
/(?:^|\/)(?:package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|vitest\.config\.[^/]+|vite\.config\.[^/]+)$|(?:^|\/)(?:config|patches)\/|^src\/packages\//
|
|
// Only module-graph files can make Vitest's `related` selection meaningful.
|
|
const IMPORTABLE = /\.(?:[cm]?[jt]sx?|json|css|scss|svg|html|wasm|ya?ml|txt|sql)$/
|
|
const UNIT_TEST_FILE = /\.test\.[cm]?[jt]sx?$/
|
|
|
|
const SOURCE_ROOTS = ['src/', 'mobile/', 'tests/', 'config/']
|
|
const TEST_FILE = /\.(?:test|spec|e2e|bench)\.[cm]?[jt]sx?$/
|
|
const LOCALES = 'src/renderer/src/i18n/'
|
|
|
|
// Ratchets default to SOURCE_ROOTS; list narrower inputs only where a scan is clearly scoped.
|
|
const RATCHET_INPUTS = {
|
|
'check-e2e-typecheck-ratchet.mjs': ['tests/', 'config/tsconfig.e2e.json'],
|
|
'check-focus-read-ratchet.mjs': ['src/renderer/src/', 'src/shared/'],
|
|
'check-host-list-render-ratchet.mjs': ['src/renderer/src/'],
|
|
'check-id-only-store-write-ratchet.mjs': ['src/main/persistence/'],
|
|
'check-layout-writer-ratchets.mjs': ['src/main/', 'src/renderer/src/'],
|
|
'check-main-sync-spawn-ratchet.mjs': ['src/main/'],
|
|
'check-repo-host-read-ratchet.mjs': ['src/cli/', 'src/main/', 'src/shared/'],
|
|
'check-runtime-electron-ratchet.mjs': ['src/main/', 'src/cli/', 'src/shared/'],
|
|
'check-unproven-pty-retirement-ratchet.mjs': ['src/main/'],
|
|
'check-workspace-record-read-ratchet.mjs': ['src/renderer/src/'],
|
|
'check-worktree-id-composer-ratchet.mjs': ['src/', 'mobile/src/']
|
|
}
|
|
|
|
// The lint-only verifiers from package.json `lint`, keyed to the inputs they read. A check also
|
|
// runs when its own script changes.
|
|
export const VERIFY_CHECKS = [
|
|
{
|
|
name: 'rpc params catalog',
|
|
args: ['config/scripts/generate-rpc-params-catalog.mjs', '--check'],
|
|
inputs: ['src/main/runtime/rpc/', 'src/shared/']
|
|
},
|
|
{
|
|
name: 'acp protocol',
|
|
args: ['config/scripts/acp/generate-protocol.mjs', '--check'],
|
|
inputs: ['src/main/acp/generated/', 'config/scripts/acp/']
|
|
},
|
|
{
|
|
name: 'bundled skill guides',
|
|
args: ['config/scripts/generate-bundled-skill-guides.mjs', '--check'],
|
|
inputs: ['skills/', 'config/scripts/skill-stub-composition']
|
|
},
|
|
{
|
|
name: 'skill bundle manifest',
|
|
args: ['config/scripts/generate-skill-bundle-manifest.mjs'],
|
|
inputs: ['skills/', 'resources/skills/']
|
|
},
|
|
{
|
|
name: 'localization catalogs',
|
|
runtime: 'bun',
|
|
args: ['config/scripts/verify-localization-catalogs.mjs'],
|
|
inputs: ['src/renderer/', 'src/shared/', LOCALES, 'config/scripts/verify-localization-catalog']
|
|
},
|
|
{
|
|
name: 'localization coverage',
|
|
runtime: 'bun',
|
|
args: ['config/scripts/audit-localization-coverage.mjs', '--check'],
|
|
inputs: ['src/renderer/src/', LOCALES]
|
|
},
|
|
{
|
|
// CI's own predicate, so local and CI select this check identically.
|
|
name: 'localization extraction',
|
|
args: ['config/scripts/verify-localization-extraction.mjs'],
|
|
matches: (files) => affectsLocalizationExtraction(files)
|
|
},
|
|
{
|
|
name: 'reliability gates',
|
|
args: ['config/scripts/check-reliability-gates.mjs'],
|
|
inputs: ['config/reliability-gates.jsonc']
|
|
},
|
|
{
|
|
name: 'node runtime pin',
|
|
args: ['config/scripts/check-node-runtime-pin.mjs'],
|
|
inputs: ['src/shared/node-runtime-pin', 'config/scripts/node-dist-archive-name']
|
|
},
|
|
{
|
|
name: 'test file runners',
|
|
args: ['config/scripts/check-test-file-runners.mjs'],
|
|
inputs: [
|
|
'config/scripts/ci-unit-files.mjs',
|
|
'tests/playwright.config.ts',
|
|
'mobile/vitest.config.ts'
|
|
],
|
|
matches: (files) =>
|
|
files.some((file) => TEST_FILE.test(file) || /vitest[^/]*\.config/.test(file))
|
|
},
|
|
{
|
|
name: 'mobile shared dependency versions',
|
|
args: ['config/scripts/check-mobile-shared-dependency-versions.mjs'],
|
|
inputs: ['mobile/package.json', 'mobile/pnpm-lock.yaml']
|
|
},
|
|
{
|
|
name: 'readme local links',
|
|
args: ['config/scripts/check-readme-local-links.mjs'],
|
|
inputs: ['README.md', 'docs/readme/']
|
|
},
|
|
{
|
|
name: 'import boundaries',
|
|
runtime: 'bun',
|
|
args: ['config/scripts/check-import-boundaries.mjs'],
|
|
inputs: [...SOURCE_ROOTS, 'config/process-host-direct-import-baseline.txt']
|
|
},
|
|
{
|
|
name: 'dead design-system classes',
|
|
oxlint: ['--config', 'config/oxlint-dead-classes.json', 'src/renderer'],
|
|
inputs: ['src/renderer/', 'config/oxlint-dead-classes.json']
|
|
}
|
|
]
|
|
|
|
// Projects of run-typecheck-projects-in-parallel.mjs and the source trees whose types reach them.
|
|
const TYPECHECK_INPUTS = [
|
|
{
|
|
config: 'config/tsconfig.node.json',
|
|
inputs: [
|
|
'src/main/',
|
|
'src/preload/',
|
|
'src/shared/',
|
|
'src/renderer/',
|
|
'src/cli/',
|
|
'src/wsl-guest/',
|
|
'config/'
|
|
]
|
|
},
|
|
{
|
|
config: 'config/tsconfig.tc.web.json',
|
|
inputs: ['src/renderer/', 'src/preload/', 'src/shared/', 'src/main/']
|
|
},
|
|
{ config: 'config/tsconfig.tc.cli.json', inputs: ['src/cli/', 'src/shared/', 'src/main/'] },
|
|
{
|
|
config: 'src/packages/process-host/tsconfig.test.json',
|
|
inputs: ['src/packages/process-host/']
|
|
}
|
|
]
|
|
const TYPECHECK_ALL_PREFIXES = ['src/packages/', 'src/types/', 'config/tsconfig', 'tsconfig.json']
|
|
// JSON reaches the type graph only through src/ imports (resolveJsonModule).
|
|
const isTypedSource = (file) =>
|
|
/\.[cm]?tsx?$/.test(file) || (file.startsWith('src/') && file.endsWith('.json'))
|
|
|
|
function matchesPrefix(file, prefixes) {
|
|
return prefixes.some((prefix) => file === prefix || file.startsWith(prefix))
|
|
}
|
|
|
|
// A script's stem also matches its tests and sibling modules (`verify-localization-catalog*`).
|
|
function ownScriptStem(check) {
|
|
return check.args?.[0].replace(/\.mjs$/, '')
|
|
}
|
|
|
|
export function selectTypecheckProjects(files) {
|
|
if (
|
|
files.some((file) => SELECT_ALL_FILES.has(file) || matchesPrefix(file, TYPECHECK_ALL_PREFIXES))
|
|
) {
|
|
return TYPECHECK_INPUTS.map(({ config }) => config)
|
|
}
|
|
const typed = files.filter(isTypedSource)
|
|
return TYPECHECK_INPUTS.filter(({ inputs }) =>
|
|
typed.some((file) => matchesPrefix(file, inputs))
|
|
).map(({ config }) => config)
|
|
}
|
|
|
|
function listRatchetChecks(scriptsDir) {
|
|
const scripts = new Set(listRatchetScripts(scriptsDir))
|
|
// `pnpm lint` runs these although CI checks them in their own steps outside run-ratchets.mjs.
|
|
scripts.add('check-main-sync-spawn-ratchet.mjs')
|
|
scripts.add('check-flaky-test-pattern-ratchet.mjs')
|
|
return [...scripts].sort().map((script) => ({
|
|
name: script.replace(/^check-|\.mjs$/g, ''),
|
|
args: [`config/scripts/${script}`],
|
|
inputs: RATCHET_INPUTS[script] ?? SOURCE_ROOTS,
|
|
matches: (files) => files.some((file) => /^config\/[^/]+-baseline\.txt$/.test(file))
|
|
}))
|
|
}
|
|
|
|
function isSelected(check, files) {
|
|
const stem = ownScriptStem(check)
|
|
return (
|
|
(stem !== undefined && files.some((file) => file.startsWith(stem))) ||
|
|
files.some((file) => matchesPrefix(file, check.inputs ?? [])) ||
|
|
(check.matches?.(files) ?? false)
|
|
)
|
|
}
|
|
|
|
// `related` paths go through Vitest's import graph; `colocated` are explicit test files, either
|
|
// changed tests under a trigger path or the `<stem>.test.*` / `<stem>.*.test.*` siblings of one.
|
|
export function splitUnitTestInputs(files, listDirectory = () => []) {
|
|
const related = files.filter(
|
|
(file) => IMPORTABLE.test(file) && !FULL_SUITE_TRIGGER.test(file) && !isDocsOnlyPath(file)
|
|
)
|
|
const colocated = new Set()
|
|
for (const file of files.filter((candidate) => FULL_SUITE_TRIGGER.test(candidate))) {
|
|
if (UNIT_TEST_FILE.test(file)) {
|
|
colocated.add(file)
|
|
continue
|
|
}
|
|
const slash = file.lastIndexOf('/')
|
|
const directory = file.slice(0, slash + 1)
|
|
const stem = file.slice(slash + 1).replace(/\.[^.]+$/, '')
|
|
for (const name of listDirectory(directory).map(String)) {
|
|
if (UNIT_TEST_FILE.test(name) && name.startsWith(`${stem}.`)) {
|
|
colocated.add(directory + name)
|
|
}
|
|
}
|
|
}
|
|
return { related, colocated: [...colocated].sort() }
|
|
}
|
|
|
|
/** Decides which checks a set of changed paths needs; CI's classifier gates the expensive jobs. */
|
|
export function planChangedChecks(files, { scriptsDir, listDirectory }) {
|
|
const jobs = classifyPrJobs(files)
|
|
const selectAll = files.some(
|
|
(file) => SELECT_ALL_FILES.has(file) || matchesPrefix(file, SELECT_ALL_PREFIXES)
|
|
)
|
|
const checks = [...listRatchetChecks(scriptsDir), ...VERIFY_CHECKS]
|
|
return {
|
|
staticAnalysis: jobs.static_analysis,
|
|
typecheckProjects: jobs.typecheck ? selectTypecheckProjects(files) : [],
|
|
unitTests: jobs.test
|
|
? splitUnitTestInputs(files, listDirectory)
|
|
: { related: [], colocated: [] },
|
|
checks: jobs.static_analysis
|
|
? checks.filter((check) => selectAll || isSelected(check, files))
|
|
: []
|
|
}
|
|
}
|