mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* feat(wsl): bridge sh/dash/ash panes report their folder (OSC 7) at every prompt bash, zsh, fish and nushell panes already tell the terminal daemon their folder; a bridge pane whose login shell is sh, dash or busybox ash did not, so restores fell back to the starting folder. dash has no rcfile flag or prompt hook, but an interactive sh reads $ENV after ~/.profile, and dash 0.5.8+ (plus ash and bash-as-sh) expand $(...) in PS1. The bridge now points ENV at a new sh/env file in the published wrapper tree, saving the user's own ENV. That file hands ENV back to the user's value (so children see theirs), sources it, and inside WSL only prefixes PS1 with the report. A root containing $, ` or \ is skipped, since the shell expands ENV's value. A non-interactive sh -c never reads ENV, and a missing file (Orca updated away or uninstalled) is silently ignored by dash, ash and bash. * fix(wsl): sh ENV hook survives quotes in the user's ENV and keeps prompt width - The user's ENV is expanded through a here-document, so a value with a quote, $ or space no longer breaks startup with a syntax error. - bash-as-sh and busybox ash (fancy prompt) get the report inside \[ \] so its invisible bytes don't count toward the prompt width; dash has no line editor to miscount and would print \[ literally, so it stays bare. - Tests: a user ENV of `$HOME/u "q" sp/env`, a user PS1 with $, a quote and a backslash, and reports read through the daemon's WSL parser. - CI: edits to the sh hook and the bridge launch trigger the shell contracts job (its prefix list moves to its own module, keeping the scope script under its line limit), and the sh and fish WSL pty tests leave the unit shards. * ci: check out the shell-contract prefix module in the change detector The detector job sparse-checks-out a fixed file list; the new pr-shell-contract-prefixes.mjs that pr-code-change-scope.mjs imports was missing from it, so the job failed with ERR_MODULE_NOT_FOUND. A contract test now walks the scope script's local imports and requires each in that list. * ci: list the shell-contract prefix module wherever the scope script's modules are listed Two config-script suites copy pr-code-change-scope.mjs and its sibling modules into a fixture repo; without the new pr-shell-contract-prefixes.mjs the copied script failed to load, and the detector fell back to running everything. Also lists it with the scope script's other modules in the mobile workflow trigger and the mobile non-test file set.
655 lines
24 KiB
JavaScript
655 lines
24 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { isLayoutOracleSource } from './pr-layout-oracle-route.mjs'
|
|
import { SHELL_PREFIXES } from './pr-shell-contract-prefixes.mjs'
|
|
import {
|
|
isCloudTestRunnerInput,
|
|
isWindowsProcessProbePath,
|
|
LINUX_PACKAGE_TESTS,
|
|
WINDOWS_PACKAGE_TESTS
|
|
} from './pr-package-test-paths.mjs'
|
|
|
|
const DOCS_ONLY_FILES = new Set([
|
|
'README.md',
|
|
'LICENSE',
|
|
'AGENTS.md',
|
|
'CLAUDE.md',
|
|
'Agents.md',
|
|
'Claude.md',
|
|
'.github/CONTRIBUTING.md',
|
|
'.github/pull_request_template.md',
|
|
'.github/CODEOWNERS'
|
|
])
|
|
|
|
const DOCS_ONLY_PREFIXES = ['docs/', '.github/ISSUE_TEMPLATE/']
|
|
|
|
export const PR_CHECK_JOBS = [
|
|
'static_analysis',
|
|
'typecheck',
|
|
'git_compatibility',
|
|
'codex_index_heal_contract',
|
|
'xterm_patch_sync',
|
|
'shell_contracts',
|
|
'test',
|
|
'orcad_browser',
|
|
'mobile_web_app',
|
|
'cross-version-wire',
|
|
'managed_hook_node18',
|
|
'package',
|
|
'package_windows',
|
|
'layout_oracle',
|
|
'dependency_isolation'
|
|
]
|
|
|
|
const ALWAYS_ON_CODE_JOBS = new Set(['static_analysis', 'typecheck', 'test'])
|
|
|
|
const MOBILE_CI_TOOL_FILES = new Set([
|
|
'.github/workflows/mobile.yml',
|
|
'config/mobile-ci.vitest.config.mjs',
|
|
'config/scripts/run-mobile-ci-tests.mjs',
|
|
'config/scripts/mobile-release-check-scope.test.mjs',
|
|
'config/scripts/mobile-test-change-scope.test.mjs'
|
|
])
|
|
|
|
const GLOBAL_FORCE_PREFIXES = [
|
|
'.github/workflows/pr.yml',
|
|
'.github/actions/install-node-dependencies/',
|
|
'.github/actions/setup-pnpm/',
|
|
'.github/actions/restore-pnpm-verification/',
|
|
'.github/actions/prepare-native-runtime/',
|
|
'config/scripts/pr-code-change-scope',
|
|
'config/scripts/pr-package-test-paths'
|
|
]
|
|
|
|
const GLOBAL_FORCE_FILES = new Set(['package.json', 'pnpm-lock.yaml'])
|
|
|
|
const GIT_COMPAT_PREFIXES = [
|
|
'.github/actions/prepare-git-compatibility/',
|
|
'src/shared/git-',
|
|
'src/shared/review-head-tracking-ref',
|
|
'src/shared/worktree/local-base-branch-fast-forward',
|
|
'src/main/git/',
|
|
'config/scripts/git-binary-compatibility'
|
|
]
|
|
|
|
// Scope real Codex contract to read-repair/transport plus wrapper --no-daemon, project trust and hook approvals in managed/user homes.
|
|
const CODEX_INDEX_HEAL_CONTRACT_PREFIXES = [
|
|
'src/main/codex/codex-hook-file-entry-binary-contract',
|
|
'src/main/codex/codex-hook-trust-',
|
|
'src/main/codex/codex-hook-approval-first-write',
|
|
'src/main/codex/codex-hook-hash-lookup',
|
|
'src/main/codex/codex-hook-orca-approvals',
|
|
'src/main/codex/codex-hook-reconcile',
|
|
'src/main/codex/codex-hook-local-install',
|
|
'src/main/codex/codex-hook-user-mirroring',
|
|
'src/main/codex/codex-real-home-hook-',
|
|
'src/main/codex/codex-real-home-hooks-json',
|
|
'src/main/codex/codex-user-hook-trust-moves',
|
|
'src/main/codex/codex-hook-definition',
|
|
'src/main/codex/codex-hook-command-form',
|
|
'src/main/codex/codex-hook-identity',
|
|
'src/main/codex/codex-app-server-client',
|
|
'src/main/codex/codex-trust-identity',
|
|
'src/main/codex/config-toml-hook-trust-',
|
|
'src/main/codex/config-toml-key-path',
|
|
'src/main/agent-hooks/posix-hook-command',
|
|
'src/main/agent-hooks/hook-post-command',
|
|
'src/main/codex-cli/codex-read-only-app-server-args',
|
|
'src/main/agent-trust-presets',
|
|
'src/main/codex/config-toml-trust',
|
|
'src/main/pty/codex-no-daemon-binary-contract',
|
|
'src/main/pty/codex-shell-launch-preflight',
|
|
'src/shared/codex-shell-function',
|
|
'src/main/codex/codex-index-heal-binary-contract',
|
|
'src/main/codex/codex-session-index-heal',
|
|
'src/main/codex/codex-app-server-session',
|
|
'src/main/codex/codex-state-db',
|
|
'src/main/sqlite/sync-database',
|
|
'src/main/codex/app-server/codex-app-server-capability-signal',
|
|
'src/main/provider-process/provider-process-exit-deadline',
|
|
'src/main/provider-process/provider-process-launch',
|
|
'src/main/provider-process/provider-record-reader',
|
|
'src/main/codex/codex-session-backfill',
|
|
'src/main/codex/codex-session-index-heal-state',
|
|
'src/main/codex-cli/command',
|
|
'src/main/win32-utils',
|
|
'src/shared/node-cli-command-resolution',
|
|
'src/shared/windows-batch-spawn'
|
|
]
|
|
|
|
const XTERM_PREFIXES = [
|
|
'config/patches/xterm-upstream.json',
|
|
'config/patches/@xterm',
|
|
'config/patches/xterm-src/',
|
|
'config/scripts/regenerate-xterm-patches'
|
|
]
|
|
|
|
const ORCAD_BROWSER_PREFIXES = [
|
|
'src/main/orcad/external-chromium-',
|
|
'src/main/orcad/orcad-browser-provider',
|
|
'src/main/orcad/orcad-agent-browser-binary',
|
|
'src/main/orcad/electron-serve-browser-process'
|
|
]
|
|
|
|
// Include page build/verification/manifest/packaging, entry/routes/imported mobile sources and render-check shell policy.
|
|
// Dependency visibility also matters; package.json/lockfile already force all jobs.
|
|
const DEPENDENCY_ISOLATION_PREFIXES = [
|
|
'pnpm-workspace.yaml',
|
|
'config/pnpmfile.cjs',
|
|
'config/scripts/undeclared-dependency-resolution',
|
|
'config/build-plugins/markdown-parser-exports',
|
|
'config/tsconfig',
|
|
'src/packages/',
|
|
'native/windows-registry/package.json',
|
|
'native/windows-security/package.json'
|
|
]
|
|
|
|
const MOBILE_WEB_APP_PREFIXES = [
|
|
'.github/scripts/prepare-ubuntu-apt.sh',
|
|
'config/scripts/build-mobile-web-app',
|
|
'config/scripts/run-mobile-web-app-checks',
|
|
'src/packages/process-host/',
|
|
'config/scripts/verify-mobile-web-app-bundle',
|
|
'config/scripts/mobile-web-app-',
|
|
'config/scripts/mobile-web-app/',
|
|
'config/scripts/mobile-web-bundle-',
|
|
'config/scripts/verify-packaged-mobile-web-bundle',
|
|
'config/scripts/mobile-web-source-line-endings',
|
|
'config/scripts/script-entry-detection',
|
|
'mobile/web-entry/',
|
|
'mobile/app/',
|
|
'mobile/src/',
|
|
'mobile/packages/',
|
|
'mobile/package.json',
|
|
'mobile/pnpm-lock.yaml',
|
|
'mobile/modules/orca-mobile-web-shell/',
|
|
// Chat visuals on both mobile surfaces are built from it; their in-page links check runs here.
|
|
'src/shared/native-chat/native-chat-visual-shell'
|
|
]
|
|
|
|
function changesMobileWebApp(changedFiles) {
|
|
return changedFiles.some((file) => matchesPrefix(file, MOBILE_WEB_APP_PREFIXES))
|
|
}
|
|
|
|
const CROSS_VERSION_WIRE_PREFIXES = [
|
|
'tests/e2e/cross-version-wire/',
|
|
// COMPAT(legacy-relay-bridge): the bridge is proven against a real 1.4.224 relay here.
|
|
'src/main/ssh/legacy-relay/',
|
|
'config/scripts/stable-release-tags',
|
|
// The R1 daemon protocol crossing gate runs in this job.
|
|
'config/scripts/daemon-protocol-facts',
|
|
'config/scripts/check-daemon-protocol-crossing',
|
|
// R3 runtime launcher protocol ratchet; a bump always routes here via the protocol file.
|
|
'config/scripts/check-runtime-launcher-protocol-ratchet',
|
|
'src/main/daemon/daemon-protocol-version.ts',
|
|
'src/shared/protocol-version',
|
|
'src/shared/terminal-stream-protocol',
|
|
'src/shared/browser-client-host-protocol',
|
|
'src/shared/browser-network-tunnel-protocol',
|
|
// Both released/current WebSocket transports must accept web upgrade marker (cross-version-web-upgrade-marker).
|
|
'src/main/runtime/rpc/ws-transport.ts',
|
|
'src/shared/web-origin-security.ts',
|
|
'src/shared/browser-client-host-placement',
|
|
'src/shared/agent-launch-intent',
|
|
'src/shared/rpc-contract/agent-launch-params',
|
|
'src/shared/agent-session-wire',
|
|
'src/shared/agent-session-mutation-envelope',
|
|
// Cover client sends to release hosts and fingerprints re-derived by host ledger/journal.
|
|
'src/shared/structured-agent-session-mutation.ts',
|
|
'src/shared/structured-agent-session-send-mutation.ts',
|
|
'src/shared/agent-session-record',
|
|
'src/shared/agent-session-provider-handle',
|
|
'src/shared/agent-session-journal-',
|
|
'src/main/ai-vault/structured-session-ownership.ts',
|
|
'src/main/native-chat/agent-session-journal/',
|
|
'src/main/native-chat/agent-session-wire/',
|
|
// The released phone contract runs the current discovery owner and Claude producer.
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.ts',
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts',
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts',
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-lister.ts',
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-listing-waiters.ts',
|
|
'src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.ts',
|
|
'src/main/claude/claude-model-catalog-probe.ts',
|
|
'src/main/runtime/agent-session-record-store',
|
|
'src/main/runtime/agent-session-recovery-capsule',
|
|
'src/shared/agent-session-resume-marker',
|
|
'src/main/runtime/rpc/dispatcher',
|
|
// Run on every request the suites dispatch, whatever its method.
|
|
'src/main/runtime/rpc/core.ts',
|
|
'src/main/runtime/rpc/errors.ts',
|
|
'src/main/runtime/rpc/orchestration-contract-fence.ts',
|
|
'src/main/runtime/rpc/orchestration-session-caller.ts',
|
|
'src/main/runtime/rpc/orchestration-legacy-compatibility.ts',
|
|
'src/main/runtime/rpc/orchestration-mutation-executor.ts',
|
|
'src/shared/orchestration-rpc-contract.ts',
|
|
'src/main/runtime/rpc/methods/agent-launch',
|
|
'src/main/runtime/rpc/methods/ai-vault.ts',
|
|
'src/shared/rpc-contract/browser-tab-create-params',
|
|
'src/main/runtime/rpc/methods/session-tabs.ts',
|
|
'src/main/runtime/rpc/methods/structured-agent-session',
|
|
'src/main/runtime/rpc/methods/terminal',
|
|
'src/main/runtime/runtime-worktree-agent-',
|
|
'src/main/runtime/runtime-worktree-pty-agent-sources',
|
|
'src/shared/runtime-worktree-contracts',
|
|
'src/renderer/src/runtime/remote-runtime-terminal-multiplexer',
|
|
// Turn-end status a newer host publishes and an older desktop reads (cross-version-host-observed-turn-end).
|
|
'src/shared/agent-turn-outcome',
|
|
'src/shared/agent-status-types',
|
|
'src/shared/agent-lead-status-fold',
|
|
'src/shared/agent-session-turn-record',
|
|
'src/shared/structured-agent-session-agent-status',
|
|
'src/shared/structured-agent-session-projection',
|
|
'src/shared/workspace-session-sleeping-agents',
|
|
// A current desktop's launch route against a released server's capabilities (cross-version-paired-structured-launch).
|
|
'src/shared/structured-native-chat-launch-route.ts',
|
|
'src/renderer/src/lib/agent-launch-routing.ts',
|
|
'src/renderer/src/runtime/paired-host-client-capabilities.ts',
|
|
'src/shared/electron-remote-runtime-client-capabilities.ts',
|
|
'src/shared/remote-runtime/remote-runtime-client-capabilities.ts',
|
|
// An older app opening a newer orchestration database (orchestration-delivery-downgrade).
|
|
'src/main/runtime/orchestration/db.ts',
|
|
'src/main/runtime/orchestration/db/',
|
|
'src/main/runtime/orchestration/orchestration-schema-version-skew'
|
|
]
|
|
|
|
const MANAGED_HOOK_PREFIXES = [
|
|
'config/scripts/smoke-process-host-node18',
|
|
'config/scripts/packaged-process-host-fixture',
|
|
'src/packages/process-host/',
|
|
'config/scripts/build-wsl-guest',
|
|
'src/wsl-guest/',
|
|
'src/shared/agent-hook',
|
|
'src/main/agent-hooks/'
|
|
]
|
|
|
|
const NATIVE_RUNTIME_PREFIXES = [
|
|
'config/scripts/ensure-native-runtime',
|
|
'config/scripts/initialize-windows-native-toolchain',
|
|
'config/scripts/capture-windows-native-environment',
|
|
'config/scripts/rebuild-native-deps',
|
|
'config/scripts/node-pty-job-ownership',
|
|
'config/scripts/windows-pe-machine',
|
|
'config/scripts/windows-pe-image-fixture',
|
|
'config/scripts/script-module-dependencies',
|
|
'config/scripts/windows-process-tree-creation-time',
|
|
'config/scripts/windows-process-tree-gyp-rebuild',
|
|
'config/scripts/electron-builder-native-rebuild',
|
|
'config/scripts/native-build-entry',
|
|
'config/scripts/shared-native-build',
|
|
'config/patches/node-pty@',
|
|
'config/patches/@vscode__windows-process-tree'
|
|
]
|
|
|
|
const NATIVE_CACHE_FILES = new Set([
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
'.github/actions/install-node-dependencies/action.yml',
|
|
'.github/actions/prepare-native-runtime/action.yml',
|
|
'pnpm-workspace.yaml',
|
|
'.npmrc',
|
|
'config/pnpmfile.cjs',
|
|
'config/scripts/ensure-native-runtime.mjs',
|
|
'config/scripts/initialize-windows-native-toolchain.mjs',
|
|
'config/scripts/capture-windows-native-environment.cmd',
|
|
'config/scripts/rebuild-native-deps.mjs',
|
|
'config/scripts/node-pty-job-ownership.cjs',
|
|
'config/scripts/windows-pe-machine.cjs',
|
|
'config/scripts/windows-process-tree-gyp-rebuild.mjs',
|
|
'config/scripts/windows-process-tree-creation-time.cjs',
|
|
'config/scripts/install-electron-package-binary.mjs',
|
|
'config/scripts/electron-platform-path.mjs',
|
|
'config/scripts/zip-extractor-command.mjs',
|
|
'src/shared/zip-extractor-command.ts',
|
|
'config/scripts/space-sharing-copy.mjs',
|
|
'config/scripts/native-dependency-lock-digest.mjs',
|
|
'config/scripts/orcad-prebuild-node-gyp.mjs',
|
|
'config/scripts/msbuild-file-tracking.mjs'
|
|
])
|
|
|
|
const NATIVE_CACHE_PREFIXES = [
|
|
'config/scripts/native-build-entry',
|
|
'config/scripts/shared-',
|
|
'config/patches/node-pty@',
|
|
'config/patches/@vscode__windows-process-tree',
|
|
// The vendored addons' sources: build/ and bin/ beside them are ignored, so nothing else lands here.
|
|
'native/windows-registry/',
|
|
'native/windows-security/'
|
|
]
|
|
|
|
const SHARED_PACKAGE_PREFIXES = [
|
|
'electron.vite.config.ts',
|
|
'config/electron-builder',
|
|
'config/packaged-runtime',
|
|
'config/packaged-native-build-entries',
|
|
'config/build-plugins/',
|
|
'config/scripts/build-',
|
|
'config/scripts/smoke-packaged',
|
|
'config/scripts/install-electron-package-binary',
|
|
'config/scripts/verify-packaged',
|
|
'config/scripts/verify-skills-cli-runtime',
|
|
'config/scripts/verify-linux-glibc',
|
|
'config/scripts/run-electron-vite',
|
|
'skills/',
|
|
'skill-guides/',
|
|
'resources/build/',
|
|
'resources/onboarding/',
|
|
'resources/plugins/',
|
|
'resources/skills/',
|
|
...NATIVE_RUNTIME_PREFIXES
|
|
]
|
|
|
|
const LINUX_PACKAGE_PREFIXES = [
|
|
...SHARED_PACKAGE_PREFIXES,
|
|
'config/scripts/package-linux-formats',
|
|
'config/scripts/process-failure-message.mjs',
|
|
'config/scripts/space-sharing-copy.mjs',
|
|
'.github/actions/prepare-linux-package-fixture/',
|
|
'.github/actions/prepare-wayland-clipboard/',
|
|
'config/docker/cli-launch-contract/',
|
|
'config/docker/headless-pairing/',
|
|
'config/docker/headless-serve-shutdown/',
|
|
'config/docker/daemon-shutdown-descendants/',
|
|
'config/scripts/run-linux-cli-launch-contract',
|
|
'config/scripts/run-headless-linux-pairing-docker',
|
|
'config/scripts/run-daemon-shutdown-descendants-docker',
|
|
'config/scripts/static-appimage-package-contract',
|
|
'native/computer-use-linux/',
|
|
'native/wayland-clipboard/',
|
|
'resources/linux/',
|
|
'config/scripts/run-headless-serve'
|
|
]
|
|
|
|
const WINDOWS_PACKAGE_PREFIXES = [
|
|
...SHARED_PACKAGE_PREFIXES,
|
|
'native/windows-cli-launcher/',
|
|
'native/windows-agent-hook/',
|
|
'native/computer-use-windows/',
|
|
'resources/win32/',
|
|
'config/scripts/build-windows-cli-launcher',
|
|
'config/scripts/build-windows-agent-hook',
|
|
'config/scripts/windows-rust-executable-build',
|
|
'config/scripts/verify-packaged-windows-agent-hook',
|
|
'config/scripts/windows-pty-native-capability',
|
|
'tests/tools/windows-pty-native-capability-smoke/'
|
|
]
|
|
|
|
const DESKTOP_IRRELEVANT_PREFIXES = [
|
|
'mobile/',
|
|
'cloud/',
|
|
'.github/workflows/cloud-',
|
|
'.github/workflows/mobile.yml',
|
|
'.github/workflows/mobile-ios-release.yml',
|
|
'.github/workflows/mobile-android-release.yml'
|
|
]
|
|
|
|
const STATIC_ANALYSIS_AUDIT_SCRIPTS = [
|
|
'lint:js-plugins',
|
|
'audit:code-quality:native',
|
|
'audit:code-quality:type-aware',
|
|
'audit:anti-slop'
|
|
]
|
|
|
|
// oxlint positional args name lint trees; --config consumes one value, other listed flags none.
|
|
function oxlintScanRoots(command) {
|
|
const roots = []
|
|
for (const segment of command.split('&&')) {
|
|
const tokens = segment.trim().split(/\s+/).filter(Boolean)
|
|
if (tokens[0] !== 'oxlint') {
|
|
continue
|
|
}
|
|
for (let index = 1; index < tokens.length; index += 1) {
|
|
if (tokens[index] === '--config') {
|
|
index += 1
|
|
} else if (!tokens[index].startsWith('-')) {
|
|
roots.push(tokens[index])
|
|
}
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
// Derive lint trees from command argv: mobile is otherwise desktop-irrelevant, and duplicate scope lists let #20702 bypass audits.
|
|
function readStaticAnalysisScanRoots() {
|
|
const manifest = join(import.meta.dirname, '../../package.json')
|
|
const { scripts = {} } = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
return [
|
|
...new Set(
|
|
STATIC_ANALYSIS_AUDIT_SCRIPTS.flatMap((name) => oxlintScanRoots(scripts[name] ?? ''))
|
|
)
|
|
]
|
|
}
|
|
|
|
export const STATIC_ANALYSIS_SCAN_ROOTS = readStaticAnalysisScanRoots()
|
|
|
|
const STATIC_ANALYSIS_SCAN_PREFIXES = STATIC_ANALYSIS_SCAN_ROOTS.map((root) => `${root}/`)
|
|
|
|
// Over-inclusive on purpose: a type oxfmt skips only costs this PR a static run.
|
|
const FORMAT_CHECKED_EXTENSION =
|
|
/\.(?:[cm]?[jt]sx?|json[c5]?|css|less|scss|md|mdx|ya?ml|vue|html|graphql|gql|toml)$/i
|
|
|
|
function readFormatIgnoredPrefixes() {
|
|
let config
|
|
try {
|
|
config = JSON.parse(readFileSync(join(import.meta.dirname, '../../.oxfmtrc.json'), 'utf8'))
|
|
} catch {
|
|
// Fail closed: with no ignores known, every formatted type keeps the gate.
|
|
return []
|
|
}
|
|
return (config.ignorePatterns ?? [])
|
|
.filter((pattern) => pattern.endsWith('/**'))
|
|
.map((pattern) => pattern.slice(0, -2))
|
|
}
|
|
|
|
const FORMAT_IGNORED_PREFIXES = readFormatIgnoredPrefixes()
|
|
|
|
export function isDocsOnlyPath(file) {
|
|
return (
|
|
DOCS_ONLY_FILES.has(file) ||
|
|
DOCS_ONLY_PREFIXES.some((prefix) => file.startsWith(prefix)) ||
|
|
/^README\.[^/]+\.md$/.test(file)
|
|
)
|
|
}
|
|
|
|
export function shouldRunPrChecks(changedFiles) {
|
|
// Empty diff likely signals detector failure; fail closed and retain expensive jobs.
|
|
if (changedFiles.length === 0) {
|
|
return true
|
|
}
|
|
return changedFiles.some((file) => !isDocsOnlyPath(file) && !isDesktopIrrelevantPath(file))
|
|
}
|
|
|
|
export function needsMobileDependencies(changedFiles) {
|
|
// Changed-mobile type-aware lint needs mobile/node_modules: root-only install resolves mobile types as error and produces phantom findings.
|
|
return changedFiles.length === 0 || changedFiles.some((file) => file.startsWith('mobile/'))
|
|
}
|
|
|
|
export const DAEMON_INIT_TEST_SUPPORT = 'src/main/daemon/daemon-init-dependency-mocks.ts'
|
|
|
|
const DAEMON_INIT_TEST_SCOPE = new Set([
|
|
DAEMON_INIT_TEST_SUPPORT,
|
|
'src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts',
|
|
'config/scripts/process-host-build-dist.test.mjs'
|
|
])
|
|
|
|
export function isDaemonInitTestSupportChange(files, modifiedOnly = false) {
|
|
return (
|
|
modifiedOnly &&
|
|
files.includes(DAEMON_INIT_TEST_SUPPORT) &&
|
|
new Set(files).size === files.length &&
|
|
files.every((file) => DAEMON_INIT_TEST_SCOPE.has(file))
|
|
)
|
|
}
|
|
|
|
export function classifyPrJobs(
|
|
changedFiles,
|
|
{ allowMobileCiTools = false, allowDaemonInitTestSupport = false } = {}
|
|
) {
|
|
const emptyDiff = changedFiles.length === 0
|
|
const shouldRun = shouldRunPrChecks(changedFiles)
|
|
const forceAll = emptyDiff || changedFiles.some(isGlobalForcePath)
|
|
const bundleFiles = isDaemonInitTestSupportChange(changedFiles, allowDaemonInitTestSupport)
|
|
? changedFiles.filter((file) => file !== DAEMON_INIT_TEST_SUPPORT)
|
|
: changedFiles
|
|
const jobs = Object.fromEntries(
|
|
PR_CHECK_JOBS.map((job) => [
|
|
job,
|
|
shouldRun &&
|
|
(forceAll ||
|
|
ALWAYS_ON_CODE_JOBS.has(job) ||
|
|
jobDetector(job)(
|
|
['package', 'package_windows'].includes(job) ? bundleFiles : changedFiles
|
|
))
|
|
])
|
|
)
|
|
// Keep repo-wide audits outside desktop should_run: mobile-only diffs still need lint; formatted/docs paths also require whole-tree formatting.
|
|
jobs.static_analysis =
|
|
jobs.static_analysis ||
|
|
changedFiles.some((file) => isStaticAnalysisScannedPath(file) || isFormatCheckedPath(file))
|
|
// Page-bundle gate must bypass desktop should_run or mobile-only diffs skip the very build they change.
|
|
jobs.mobile_web_app = jobs.mobile_web_app || changesMobileWebApp(changedFiles)
|
|
const mobileCiTools =
|
|
allowMobileCiTools &&
|
|
!emptyDiff &&
|
|
jobs.test &&
|
|
(jobs.static_analysis || jobs.typecheck) &&
|
|
changedFiles.every((file) => MOBILE_CI_TOOL_FILES.has(file))
|
|
return {
|
|
should_run: shouldRun,
|
|
mobile_ci_tools: mobileCiTools,
|
|
windows_process_probe: shouldRun && (forceAll || changedFiles.some(isWindowsProcessProbePath)),
|
|
// Relay contracts exercise host/cloud code; any input outside the renderer keeps them.
|
|
relay_integration:
|
|
shouldRun &&
|
|
(forceAll ||
|
|
changedFiles.some((file) => !file.startsWith('src/renderer/') && !isDocsOnlyPath(file))),
|
|
native_cache_changed: shouldRun && (emptyDiff || changedFiles.some(isNativeCacheInputPath)),
|
|
mobile_dependencies:
|
|
(shouldRun || jobs.static_analysis) && needsMobileDependencies(changedFiles),
|
|
...jobs
|
|
}
|
|
}
|
|
|
|
function jobDetector(job) {
|
|
switch (job) {
|
|
case 'git_compatibility':
|
|
return (files) => files.some((file) => matchesPrefix(file, GIT_COMPAT_PREFIXES))
|
|
case 'codex_index_heal_contract':
|
|
return (files) =>
|
|
files.some(
|
|
(file) =>
|
|
!/^src\/main\/codex\/codex-app-server-(client|session)\.test\.ts(?!.)/s.test(file) &&
|
|
matchesPrefix(file, CODEX_INDEX_HEAL_CONTRACT_PREFIXES)
|
|
)
|
|
case 'xterm_patch_sync':
|
|
return (files) => files.some((file) => matchesPrefix(file, XTERM_PREFIXES))
|
|
case 'shell_contracts':
|
|
return (files) => files.some((file) => matchesPrefix(file, SHELL_PREFIXES))
|
|
case 'orcad_browser':
|
|
return (files) => files.some((file) => matchesPrefix(file, ORCAD_BROWSER_PREFIXES))
|
|
// Handle inside map too: default detector otherwise runs this job on every desktop-relevant PR.
|
|
case 'mobile_web_app':
|
|
return changesMobileWebApp
|
|
case 'cross-version-wire':
|
|
return (files) => files.some((file) => matchesPrefix(file, CROSS_VERSION_WIRE_PREFIXES))
|
|
case 'managed_hook_node18':
|
|
return (files) => files.some((file) => matchesPrefix(file, MANAGED_HOOK_PREFIXES))
|
|
case 'package':
|
|
return (files) => files.some(isLinuxPackagePath)
|
|
case 'package_windows':
|
|
return (files) => files.some(isWindowsPackagePath)
|
|
case 'layout_oracle':
|
|
return (files) => files.some(isLayoutOracleSource)
|
|
case 'dependency_isolation':
|
|
return (files) => files.some((file) => matchesPrefix(file, DEPENDENCY_ISOLATION_PREFIXES))
|
|
default:
|
|
return () => true
|
|
}
|
|
}
|
|
|
|
function isLinuxPackagePath(file) {
|
|
return LINUX_PACKAGE_TESTS.includes(file) || isProductBundlePath(file, LINUX_PACKAGE_PREFIXES)
|
|
}
|
|
|
|
function isWindowsPackagePath(file) {
|
|
return (
|
|
WINDOWS_PACKAGE_TESTS.includes(file) ||
|
|
isWindowsProcessProbePath(file) ||
|
|
isProductBundlePath(file, WINDOWS_PACKAGE_PREFIXES)
|
|
)
|
|
}
|
|
|
|
function isProductBundlePath(file, extraPrefixes) {
|
|
return !isTestFile(file) && (file.startsWith('src/') || matchesPrefix(file, extraPrefixes))
|
|
}
|
|
|
|
// These shared fixtures are consumed only by unit tests.
|
|
export function isUnitTestSupportSource(file) {
|
|
return (
|
|
file === 'src/renderer/src/store/slices/editor-slice-test-harness.ts' ||
|
|
file === 'src/renderer/src/store/slices/browser-slice-test-harness.ts' ||
|
|
file === 'src/renderer/src/runtime/web-runtime-session-test-harness.ts' ||
|
|
file === 'src/renderer/src/runtime/sync-runtime-graph-test-harness.ts' ||
|
|
file === 'src/renderer/src/runtime/web-session-tabs-sync-test-harness.ts' ||
|
|
file === 'src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx'
|
|
)
|
|
}
|
|
|
|
function isTestFile(file) {
|
|
return (
|
|
isUnitTestSupportSource(file) ||
|
|
/\.(?:test|spec)\.(?:js|cjs|mjs|ts|tsx)$/.test(file) ||
|
|
file.includes('/__tests__/')
|
|
)
|
|
}
|
|
|
|
function isDesktopIrrelevantPath(file) {
|
|
return matchesPrefix(file, DESKTOP_IRRELEVANT_PREFIXES)
|
|
}
|
|
|
|
function isStaticAnalysisScannedPath(file) {
|
|
// Fail closed: roots we failed to parse must keep the gate, not silently drop it.
|
|
return (
|
|
STATIC_ANALYSIS_SCAN_PREFIXES.length === 0 ||
|
|
matchesPrefix(file, STATIC_ANALYSIS_SCAN_PREFIXES) ||
|
|
isCloudTestRunnerInput(file)
|
|
)
|
|
}
|
|
|
|
function isFormatCheckedPath(file) {
|
|
return FORMAT_CHECKED_EXTENSION.test(file) && !matchesPrefix(file, FORMAT_IGNORED_PREFIXES)
|
|
}
|
|
|
|
function isNativeCacheInputPath(file) {
|
|
return NATIVE_CACHE_FILES.has(file) || matchesPrefix(file, NATIVE_CACHE_PREFIXES)
|
|
}
|
|
|
|
function isGlobalForcePath(file) {
|
|
return (
|
|
GLOBAL_FORCE_FILES.has(file) ||
|
|
(matchesPrefix(file, GLOBAL_FORCE_PREFIXES) &&
|
|
!(file.startsWith('config/scripts/') && isTestFile(file)))
|
|
)
|
|
}
|
|
|
|
function matchesPrefix(file, prefixes) {
|
|
return prefixes.some((prefix) => file === prefix || file.startsWith(prefix))
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
// Stream stdin: readFileSync(0) can throw EAGAIN beyond the 64KB pipe buffer on stale-base diffs.
|
|
let input = ''
|
|
process.stdin.setEncoding('utf8')
|
|
for await (const chunk of process.stdin) {
|
|
input += chunk
|
|
}
|
|
const classification = classifyPrJobs(input.split(/\r?\n/).filter(Boolean), {
|
|
allowMobileCiTools: process.argv.includes('--mobile-ci-tools'),
|
|
allowDaemonInitTestSupport: process.argv.includes('--daemon-init-test-support')
|
|
})
|
|
for (const [name, value] of Object.entries(classification)) {
|
|
process.stdout.write(`${name}=${value ? 'true' : 'false'}\n`)
|
|
}
|
|
}
|