name: PR Checks run-name: "PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}" on: pull_request: types: - opened - synchronize - reopened - ready_for_review - converted_to_draft concurrency: group: pr-checks-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: contents: read jobs: # Keep required verify present for docs-only PRs while routing expensive jobs by inputs; empty diffs fail closed to full checks. code_paths: if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: detect changes and check repository guards # Hosted runners start faster than Blacksmith's (~5s vs ~13s) and this sparse checkout fits one vCPU. runs-on: ubuntu-slim timeout-minutes: 5 permissions: contents: read actions: read outputs: should_run: ${{ steps.filter.outputs.should_run }} reused_run_id: ${{ steps.readiness.outputs.run_id }} # A proven success masks required work only; advisory routing still uses the full diff. mobile_dependencies: ${{ steps.filter.outputs.mobile_dependencies }} mobile_web_app: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.mobile_web_app }} static_analysis: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.static_analysis }} typecheck: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.typecheck }} mobile_ci_tools: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.mobile_ci_tools }} git_compatibility: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.git_compatibility }} codex_index_heal_contract: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.codex_index_heal_contract }} xterm_patch_sync: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.xterm_patch_sync }} shell_contracts: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.shell_contracts }} test: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.test }} relay_integration: ${{ steps.filter.outputs.relay_integration }} orcad_browser: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.orcad_browser }} cross-version-wire: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.cross-version-wire }} managed_hook_node18: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node18 }} package: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package }} windows_process_probe: ${{ steps.filter.outputs.windows_process_probe }} package_windows: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package_windows }} layout_oracle: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.layout_oracle }} dependency_isolation: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.dependency_isolation }} e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }} test_files: ${{ steps.e2e_filter.outputs.test_files }} e2e_run_changed: ${{ steps.e2e_filter.outputs.e2e_run_changed }} e2e_needs_build: ${{ steps.e2e_filter.outputs.e2e_needs_build }} e2e_ssh_shards: ${{ steps.e2e_filter.outputs.e2e_ssh_shards }} native_ime_source_changed: ${{ steps.e2e_filter.outputs.native_ime_source_changed }} wsl_source_changed: ${{ steps.e2e_filter.outputs.wsl_source_changed }} steps: - name: Checkout uses: actions/checkout@v6 with: # Fetch 50 commits for merge HEAD^1 diffs instead of all 8127 refs (~20s); sparse ~7-file blob:none checkout avoids materialization/refetch (9.5s→1.6s). fetch-depth: 50 filter: blob:none sparse-checkout: | /config/scripts/git-pull-request-diff-base.mjs /package.json /README.md /docs/readme/ /.github/scripts/check-root-directory-entries.mjs /.github/scripts/check-required-pr-context.mjs /.github/workflows/ /config/.bun-version /config/scripts/check-readme-local-links.mjs /config/scripts/pr-code-change-scope.mjs /.oxfmtrc.json /config/scripts/pr-package-test-paths.mjs /config/scripts/pr-layout-oracle-route.mjs /config/scripts/pr-e2e-source-routing.mjs /config/scripts/pr-browser-e2e-source-routes.mjs /config/scripts/ci-e2e-job-selection.mjs /config/scripts/pr-ready-check-reuse.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Reject new root-level files and folders env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" node .github/scripts/check-root-directory-entries.mjs "$DIFF_BASE" HEAD # The full Git index retains link targets outside the sparse working tree. - name: Check README local links run: node config/scripts/check-readme-local-links.mjs - name: Set up Bun for required workflow validation uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version - name: Check required PR context ownership run: bun .github/scripts/check-required-pr-context.mjs # Readiness changes eligibility for advisory tests, not the already-tested source. - name: Find identical successful required checks id: readiness if: github.event.action == 'ready_for_review' env: GH_TOKEN: ${{ github.token }} PR_CHECK_WORKFLOW_SHA: ${{ github.workflow_sha }} run: node config/scripts/pr-ready-check-reuse.mjs - name: Classify changed paths id: filter env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail # PR merge HEAD’s first parent is the base; --merge-base would require the payload SHA/full history. DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" # --no-renames retains code-side deletions when files move into docs. CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)" echo "Changed paths:" printf '%s\n' "$CHANGED" MOBILE_CI_ARGS=() if git diff --quiet --no-renames --diff-filter=ACDRTUXB "$DIFF_BASE" HEAD; then MOBILE_CI_ARGS=(--mobile-ci-tools --daemon-init-test-support) fi printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs "${MOBILE_CI_ARGS[@]}" | tee -a "$GITHUB_OUTPUT" # Reuse the path-detector checkout instead of queuing another runner. - name: Filter changed E2E specs id: e2e_filter if: github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true' run: | set -euo pipefail BASE="${{ github.event.pull_request.base.sha }}" DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE")" CHANGED="$(git diff --name-only --diff-filter=AMCR "$DIFF_BASE" HEAD)" E2E_SCOPE_ARGS=() if git diff --quiet --no-renames --diff-filter=ACDRTUXB "$DIFF_BASE" HEAD; then E2E_SCOPE_ARGS=(--daemon-init-test-support) fi # Executable routes let tests prove authorities/exclusions/sentinels without evaluating shell. TEST_FILES_JSON="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs "${E2E_SCOPE_ARGS[@]}")" echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT" printf '%s\n' "$TEST_FILES_JSON" | node config/scripts/ci-e2e-job-selection.mjs --job-outputs >> "$GITHUB_OUTPUT" # Real IME runs an entire IBus session; route it by IME source, independently of spec lists. NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)" echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT" WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)" WSL_SOURCE_CHANGED="$(printf '%s\n' "$WSL_CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --wsl-source)" echo "wsl_source_changed=$WSL_SOURCE_CHANGED" >> "$GITHUB_OUTPUT" echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED" SHOULD_RUN="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --reusable-workflow "${E2E_SCOPE_ARGS[@]}")" if [ "$SHOULD_RUN" = true ]; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "Changed E2E specs: $TEST_FILES_JSON" else echo "should_run=false" >> "$GITHUB_OUTPUT" echo "No specs requiring the reusable E2E workflow" fi preflight: name: static analysis and typecheck needs: [code_paths] if: needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true' # Leave headroom for typechecking and overlapping checks. runs-on: blacksmith-8vcpu-ubuntu-2404 outputs: shards: ${{ steps.unit-plan.outputs.shards }} managed_hook_node18: ${{ steps.managed_hook_node18.outcome }} steps: - name: Checkout uses: actions/checkout@v6 with: # Fetch 50 for merge HEAD^1, no merge-base graph needed. Full-tree checkout omits blob:none to avoid refetching ~30k blobs (23s versus ~11s). fetch-depth: 50 persist-credentials: false # The dependency action below restores Node 24 before installing or building packages. - name: Prepare Node 18 smoke runtime id: node18_runtime if: needs.code_paths.outputs.managed_hook_node18 == 'true' continue-on-error: true uses: actions/setup-node@v6 with: node-version: '18' package-manager-cache: false - name: Capture Node 18 executable id: node18_executable if: needs.code_paths.outputs.managed_hook_node18 == 'true' && steps.node18_runtime.outcome == 'success' continue-on-error: true run: node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT" # Choose one root/mobile install: root-only store is 321MB/4.6s versus mixed 537MB/8.6s, saving unused mobile downloads on most PRs. - uses: ./.github/actions/install-node-dependencies if: needs.code_paths.outputs.mobile_dependencies != 'true' with: native-runtime: node node-version: '24' - uses: ./.github/actions/install-node-dependencies if: needs.code_paths.outputs.mobile_dependencies == 'true' with: native-runtime: node node-version: '24' cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml # Bun 1.4.3 introduces the compiler used by this job. - name: Set up Bun for preflight checks uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: config/.bun-version # Plan first: its ~10s single-threaded import-graph walk overlaps lint instead of trailing the job. - name: Plan unit selection if: '!cancelled()' id: unit-plan background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} ORCA_UNIT_SELECTION_MODE: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }} ORCA_PULL_REQUEST_DRAFT: ${{ github.event.pull_request.draft }} # Five shards use fewer runner minutes; the repository variable can request 2 to 10. ORCA_UNIT_FULL_SHARD_COUNT: ${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node config/scripts/ci-unit-plan.mjs # Run gate scripts directly to avoid repeated package-manager startup. - name: Check process-host imports if: '!cancelled()' id: process-host-imports background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi bun config/scripts/check-import-boundaries.mjs - name: Check main-process sync launches if: '!cancelled()' id: main-sync-spawn-ratchet background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi bun config/scripts/check-main-sync-spawn-ratchet.mjs # Keep each check in its own log while sharing this runner. - name: Lint if: '!cancelled()' id: root-lint background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node node_modules/oxlint/bin/oxlint --format github # JS plugins make Oxlint hand every AST to Node, so they run apart from the native root lint. - name: Enforce JS-plugin lint rules if: '!cancelled()' id: js-plugin-lint background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node node_modules/oxlint/bin/oxlint --config config/oxlint-js-plugins.json --format github src config tests mobile native .github - name: Enforce ast-grep rules if: needs.code_paths.outputs.static_analysis == 'true' run: | node_modules/.bin/ast-grep test --config config/sgconfig.yml --skip-snapshot-tests node config/scripts/ast-grep-gates.mjs --format=github # Why the full tree: it takes ~2s, and the pre-commit hook is skippable. - name: Check formatting if: needs.code_paths.outputs.static_analysis == 'true' run: node node_modules/oxfmt/bin/oxfmt --check . - name: Reject low-evidence patterns if: needs.code_paths.outputs.static_analysis == 'true' run: | node config/scripts/sync-anti-slop-plugin.mjs bun --smol node_modules/oxlint/bin/oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings - wait: root-lint - name: Enforce focused code-quality plugins if: '!cancelled()' id: native-code-quality background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node node_modules/oxlint/bin/oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings - name: Enforce type-aware code-quality baseline if: needs.code_paths.outputs.static_analysis == 'true' run: node node_modules/oxlint/bin/oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings # Mobile installation changes import resolution for the native cycle check. - wait: native-code-quality # Install mobile before type-aware changed-code lint or unresolved mobile types become phantom error-type findings. - uses: ./.github/actions/install-mobile-dependencies if: needs.code_paths.outputs.static_analysis == 'true' && needs.code_paths.outputs.mobile_dependencies == 'true' - name: Enforce changed-code quality if: '!cancelled()' id: changed-code-quality background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node config/scripts/check-changed-code-quality.mjs "${{ github.event.pull_request.base.sha }}" - name: Verify locked audit tools if: needs.code_paths.outputs.static_analysis == 'true' env: ORCA_BACKGROUND_LAUNCH: '1' run: node config/scripts/verify-locked-audit-tools.mjs - name: Enforce React Doctor on changed lines if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-react-doctor-changed.mjs "${{ github.event.pull_request.base.sha }}" - wait: changed-code-quality # The compiler overlaps the remaining checks after the type-aware passes release memory. - name: Typecheck if: '!cancelled()' id: typecheck background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi pnpm run typecheck:ci --bun-concurrency=2 --bun-threads=2 # Starts with the compiler so its ~16s pass overlaps the serial checks below. # Extract sorted evidence to isolated temp paths with one normalized platform-independent AST pass; use preflight’s pinned Bun. - name: Verify localization extraction if: '!cancelled()' id: localization-extraction background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi # Detection failures run the full check; renames retain the removed input path. DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" && scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then echo "Localization extraction inputs are unchanged." else bun config/scripts/verify-localization-extraction.mjs fi - name: Check Zustand selector fan-out budget if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check - name: Check reliability gate manifest if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-reliability-gates.mjs - name: Enforce dead design-system classes if: needs.code_paths.outputs.static_analysis == 'true' run: node node_modules/oxlint/bin/oxlint --config config/oxlint-dead-classes.json src/renderer - name: Check VM runtime rollback compatibility if: needs.code_paths.outputs.static_analysis == 'true' env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" if git diff --quiet "$DIFF_BASE" HEAD -- \ src/shared/ephemeral-vm-runtime-store.ts \ src/shared/ephemeral-vm-runtime-feature-store.ts \ src/shared/ephemeral-vm-runtime-rollback-projection.ts \ src/shared/ephemeral-vm-runtimes.ts \ src/shared/ephemeral-vm-recipes.ts \ src/shared/orca-yaml-hook-types.ts \ config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \ config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts \ src/main/ephemeral-vm/runtime-service.ts \ src/main/ephemeral-vm/runtime-provisioning-persistence.ts \ src/main/ephemeral-vm/failed-start-cleanup.ts; then echo "VM runtime persistence is unchanged." exit 0 fi node config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \ config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts # Runs every config/scripts/check-*-ratchet(s).mjs, so a new ratchet needs no step here. - name: Enforce ratchets if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/run-ratchets.mjs - name: Check Node runtime pin if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-node-runtime-pin.mjs # A ~7s single-threaded tree walk; it overlaps the remaining checks. - name: Check every test file has a runner if: '!cancelled()' id: test-file-runners background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node config/scripts/check-test-file-runners.mjs - name: Check root and mobile lock the same shared dependencies if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-mobile-shared-dependency-versions.mjs - name: Decide whether the orcad terminal smoke runs id: orcad-smoke-scope if: needs.code_paths.outputs.static_analysis == 'true' env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | # Detection failures run the smoke; renames retain the removed input path. DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/orcad-smoke-changes" && scope="$(node config/scripts/orcad-terminal-smoke-change-scope.mjs "$RUNNER_TEMP/orcad-smoke-changes")" && [ "$scope" = false ]; then echo "Orcad terminal smoke inputs are unchanged." echo 'run=false' >> "$GITHUB_OUTPUT" else echo 'run=true' >> "$GITHUB_OUTPUT" fi # orcad refuses to build without the page phones load. - uses: ./.github/actions/build-mobile-web-bundle if: steps.orcad-smoke-scope.outputs.run == 'true' # Pair import-graph electron exclusion with built-Node boot/pair/worktree/PTY smoke; graph purity alone does not prove runtime operation. - name: Boot orcad and round-trip a terminal if: steps.orcad-smoke-scope.outputs.run == 'true' env: ORCA_BACKGROUND_LAUNCH: '1' run: pnpm run smoke:orcad-terminal - name: Verify the generated RPC params catalog if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/generate-rpc-params-catalog.mjs --check - name: Verify the generated ACP protocol schema if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/acp/generate-protocol.mjs --check - name: Verify bundled skill guides if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/generate-bundled-skill-guides.mjs --check - name: Verify skill freshness manifest if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/generate-skill-bundle-manifest.mjs - name: Verify localization catalogs if: '!cancelled()' id: localization-catalogs background: true env: PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi bun config/scripts/verify-localization-catalogs.mjs - name: Verify localization coverage if: needs.code_paths.outputs.static_analysis == 'true' run: bun config/scripts/audit-localization-coverage.mjs --check - wait: [localization-catalogs, localization-extraction] # Keep project declarations in .ts: skipLibCheck silently widened unresolved .d.ts names to any, hiding #1186 IPC breakage. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts. - name: Guard against project-owned .d.ts in preload/shared if: needs.code_paths.outputs.static_analysis == 'true' run: | matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true) if [ -n "$matches" ]; then echo "::error::Project-owned .d.ts files are not allowed under src/preload or src/shared." echo "Move type declarations into a .ts file so skipLibCheck does not hide errors." echo "See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts." echo "Found:" echo "$matches" exit 1 fi - name: Check feature wall asset budget if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-feature-wall-assets.mjs - name: Verify macOS entitlements if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/verify-macos-entitlements.mjs - wait: typecheck - wait: process-host-imports - wait: main-sync-spawn-ratchet - wait: js-plugin-lint - wait: unit-plan - wait: test-file-runners - uses: actions/upload-artifact@v7 if: needs.code_paths.outputs.typecheck == 'true' with: name: unit-selection-attempt-${{ github.run_attempt }} path: ci-shards/unit-selection.json retention-days: 14 - name: Test Mobile CI tools and runtime routing if: needs.code_paths.outputs.mobile_ci_tools == 'true' env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_BALANCE_UNIT_SHARDS: '1' run: >- node config/scripts/run-vitest.mjs run --config config/vitest.config.ts --maxWorkers=4 --isolate --retry=0 --bail=0 config/scripts/mobile-release-check-scope.test.mjs config/scripts/mobile-test-change-scope.test.mjs config/scripts/pr-ready-check-reuse.test.mjs config/scripts/ci-dependency-download-cache.test.mjs config/scripts/win32-test-lane-registration.test.mjs config/scripts/vitest-node-runtime-boundary.test.ts config/scripts/vitest-sqlite-runtime-boundary.test.ts # Report failure through the separate required check without blocking unrelated consumers. - name: Smoke process-host package on Node 18 id: managed_hook_node18 if: success() && needs.code_paths.outputs.managed_hook_node18 == 'true' continue-on-error: true env: NODE18_EXECUTABLE: ${{ steps.node18_executable.outputs.executable }} ORCA_BACKGROUND_LAUNCH: '1' run: | test -n "$NODE18_EXECUTABLE" "$NODE18_EXECUTABLE" config/scripts/smoke-process-host-node18.mjs git_compatibility: name: Git compatibility needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.git_compatibility == 'true' && needs.preflight.result == 'success' # Three parallel lanes (two through Docker) exceed real-Git test timeouts on 4-vCPU hosted runners. runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies - uses: ./.github/actions/prepare-git-compatibility - name: Verify Git binary compatibility matrix env: ORCA_BACKGROUND_LAUNCH: '1' run: | specs=( "alpine/git:edge-2.38.1|2.38.1" "alpine/git:v2.49.1|2.49.1" ) # Pull before tests so a sibling’s timed lane cannot be stalled by the first image download. for spec in "${specs[@]}"; do docker pull --quiet "${spec%%|*}" done pids=() ( ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \ GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5" \ ORCA_GIT_COMPAT_VERSION="2.25.5" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts \ src/main/git/worktree-safety-real-git.test.ts \ src/main/git/worktree-rebase-update-refs-real-git.test.ts ) & pids+=("$!") for spec in "${specs[@]}"; do ( image="${spec%%|*}" version="${spec#*|}" ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts \ src/main/git/worktree-safety-real-git.test.ts \ src/main/git/worktree-rebase-update-refs-real-git.test.ts ) & pids+=("$!") done status=0 for pid in "${pids[@]}"; do wait "$pid" || status=1 done exit "$status" # Run pinned real Codex: thread/read must repair an unindexed rollout’s threads row; stub-only units cannot detect upstream removal of this behavior. codex_index_heal_contract: name: Codex index-heal contract needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.codex_index_heal_contract == 'true' && needs.preflight.result == 'success' # Why ARM: @openai/codex ships @openai/codex-linux-arm64. runs-on: ubuntu-24.04-arm env: CODEX_CLI_VERSION: '0.150.1' # Why a second pin: --no-daemon only exists from 0.156, and Orca's codex wrapper relies on it. CODEX_NO_DAEMON_CLI_VERSION: '0.158.0' steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies - name: Install pinned Codex CLI run: | set -euo pipefail npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli" \ "@openai/codex@$CODEX_CLI_VERSION" - name: Verify Codex index-heal contract env: # Require installed binary or skip would report green without exercising the contract. ORCA_CODEX_CONTRACT_REQUIRED: '1' ORCA_CODEX_CONTRACT_VERSION: ${{ env.CODEX_CLI_VERSION }} run: | set -euo pipefail ORCA_CODEX_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli/node_modules/.bin/codex" \ pnpm exec vitest run --config config/vitest.config.ts \ src/main/codex/codex-index-heal-binary-contract.test.ts - name: Install pinned no-daemon Codex CLI run: | set -euo pipefail npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-no-daemon" \ "@openai/codex@$CODEX_NO_DAEMON_CLI_VERSION" - name: Verify Codex --no-daemon contract env: ORCA_CODEX_NO_DAEMON_CONTRACT_REQUIRED: '1' ORCA_CODEX_NO_DAEMON_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }} run: | set -euo pipefail ORCA_CODEX_NO_DAEMON_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \ pnpm exec vitest run --config config/vitest.config.ts \ src/main/pty/codex-no-daemon-binary-contract.test.ts # Test pins plus latest because Codex’s managed-home approval hash can change before users encounter review prompts. - name: Verify Codex hook file-entry contract (pinned) env: ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1' run: | set -euo pipefail for install in codex-cli:"$CODEX_CLI_VERSION" codex-cli-no-daemon:"$CODEX_NO_DAEMON_CLI_VERSION"; do ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/${install%%:*}/node_modules/.bin/codex" \ ORCA_CODEX_HOOK_CONTRACT_VERSION="${install#*:}" \ pnpm exec vitest run --config config/vitest.config.ts \ src/main/codex/codex-hook-file-entry-binary-contract.test.ts done - name: Install latest Codex CLI run: | set -euo pipefail npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-latest" "@openai/codex@latest" - name: Verify Codex hook file-entry contract (latest) env: ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1' run: | set -euo pipefail ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-latest/node_modules/.bin/codex" \ pnpm exec vitest run --config config/vitest.config.ts \ src/main/codex/codex-hook-file-entry-binary-contract.test.ts # Why the no-daemon install: it is the newest pinned Codex, and its project # lookup decides whether a worktree launch stops at the trust prompt. - name: Verify Codex project-trust contract env: ORCA_CODEX_TRUST_CONTRACT_REQUIRED: '1' ORCA_CODEX_TRUST_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }} run: | set -euo pipefail ORCA_CODEX_TRUST_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \ pnpm exec vitest run --config config/vitest.config.ts \ src/main/agent-trust-presets.test.ts xterm_patch_sync: name: xterm patch sync needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.xterm_patch_sync == 'true' && needs.preflight.result == 'success' # Use ARM for four-package pristine/patched rebuild byte comparison: output generated on Linux x64 reproduces on darwin-arm64. runs-on: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false # The generator uses Node built-ins and installs its own upstream toolchain. - uses: actions/setup-node@v6 with: node-version-file: package.json check-latest: true package-manager-cache: false # Rebuild both pristine and patched bundles; reuse only the pinned toolchain. - uses: ./.github/actions/prepare-xterm-dependencies - name: Verify xterm patches match the pinned upstream build env: WORK_DIR: ${{ runner.temp }}/xterm-patch-build run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR" shell_contracts: name: shell contracts needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.shell_contracts == 'true' && needs.preflight.result == 'success' # Exercise Linux ARM with the standalone aarch64 release and Ubuntu's zsh. runs-on: ubuntu-24.04-arm # Bound stalled prerequisite downloads instead of waiting for GitHub's 6-hour default. timeout-minutes: 15 env: # Require live fish instead of developer-friendly skips: this is the only full #9993 guard. ORCA_REQUIRE_FISH: '1' # Why: the runner image ships pwsh, and the codex wrapper's PowerShell lane must not skip. ORCA_REQUIRE_PWSH: '1' steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false # fish 4+ provides DECSET 2031 arming required by live shell contracts (#9993). - name: Install zsh and fish id: shells background: true run: | # Per-connection limits complement the whole-command apt deadlines below. sudo tee /etc/apt/apt.conf.d/99-orca-shell-contracts >/dev/null <<'APTCONF' Acquire::http::Timeout "15"; Acquire::https::Timeout "15"; Acquire::Retries "1"; APTCONF # Unrelated runner repositories may fail refresh; installing zsh must still succeed. timeout 120 sudo apt-get update || true timeout 300 sudo apt-get install -y zsh # A fixed 4.x release meets the contract without tracking package-repository updates. case "$(uname -m)" in aarch64) arch=aarch64 sha=57349f0111728c64b703a84e611aa97ff4d1fa8466385d64e67bd5bc3ffa49fc ;; *) arch=x86_64 sha=8f643d10ad1abb0ef7072764c01ceb5cf61b2d373fda400cf60f0bad69b7e095 ;; esac curl -fsSL --retry 2 --connect-timeout 15 --max-time 120 -o "$RUNNER_TEMP/fish.tar.xz" \ "https://github.com/fish-shell/fish-shell/releases/download/4.9.3/fish-4.9.3-linux-${arch}.tar.xz" echo "${sha} $RUNNER_TEMP/fish.tar.xz" | sha256sum -c - tar -xJf "$RUNNER_TEMP/fish.tar.xz" -C "$RUNNER_TEMP" fish sudo install -m 0755 "$RUNNER_TEMP/fish" /usr/local/bin/fish for package in fish fish-common; do if ! dpkg-query -W -f='${binary:Package}: ${Status}\n' "$package" 2>/dev/null; then echo "$package: absent" fi done # Exclude runner preload while keeping the tests' own XDG vendor snippets reachable. quarantine="$(mktemp -d "$RUNNER_TEMP/fish-runner-resources.XXXXXX")" for path in /usr/share/fish /etc/fish; do if [ -e "$path" ] || [ -L "$path" ]; then sudo mv -- "$path" "$quarantine/${path//\//_}" echo "$path: quarantined in $quarantine" fi if [ -e "$path" ]; then echo "$path: present"; else echo "$path: absent"; fi done printf 'fish selected: %s\n' "$(command -v fish)" - uses: ./.github/actions/install-node-dependencies with: native-runtime: node - wait: shells # Fail fish prerequisites separately before dependency install; ORCA_REQUIRE_FISH checks again in tests. - name: Require fish 4+ run: | version="$(fish --version 2>/dev/null || true)" major="${version##*version }" major="${major%%.*}" case "$major" in '' | *[!0-9]*) major=0 ;; esac echo "${version:-}" if [ "$major" -lt 4 ]; then echo "::error::shell contracts needs fish 4+ (DECSET 2031 arming, #9993) but got '${version:-none}'. Fix the direct GitHub release install rather than letting the fish lane skip." >&2 exit 1 fi - name: Test real shell contracts run: | pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \ src/main/daemon/shell-ready.test.ts \ src/main/daemon/node-pty-fd-leak.test.ts \ src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \ src/main/providers/__tests__/shell-ready-framework-example.test.ts \ src/main/pty/codex-shell-launch-preflight.test.ts \ src/main/pty/codex-shell-no-daemon.test.ts \ src/main/pty/omp-shell-wrapper-alias-safety.test.ts \ src/main/pty/omp-shell-wrapper.node-pty.test.ts \ src/main/fish-xdg-data-dirs-handoff.test.ts \ src/main/shell-startup-feature-channel.test.ts \ src/main/zsh-deferred-startup-line-init.live-shell.test.ts \ src/main/terminal-history-fish-session.node-pty.test.ts \ src/main/zsh-scoped-histfile.live-shell.test.ts \ src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \ src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts \ src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \ src/shared/fish-query-reply-child-stdin.node-pty.test.ts \ src/shared/pty-reply-echo-shapes.node-pty.test.ts \ src/shared/startup-shell-portability.live-shell.test.ts \ src/shared/posix-command-path-lookup.test.ts # Preflight saves the Node cache and publishes the plan before tests fan out. test: needs: [code_paths, preflight] # Cancellation and failed prerequisites stop the expensive matrix. if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.test == 'true' && needs.preflight.result == 'success' uses: ./.github/workflows/unit-tests.yml with: node_versions: '["24"]' runner: blacksmith-8vcpu-ubuntu-2404 phase_diagnostics: true shards: ${{ needs.preflight.outputs.shards }} run_shards: ${{ needs.code_paths.outputs.mobile_ci_tools != 'true' }} run_relay_integration: ${{ needs.code_paths.outputs.relay_integration != 'false' || needs.code_paths.outputs.orcad_browser == 'true' }} browser_contract: ${{ needs.code_paths.outputs.orcad_browser == 'true' }} # Keep advisory evidence sibling to tests and outside verify.needs; reusable-workflow dependency otherwise added ~36s after last shard. unit_selection_evidence: needs: [test] if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }} uses: ./.github/workflows/unit-selection-evidence.yml # Isolate two browser/mobile files to avoid every unit shard paying for their installs; test the same shipped bundle plus render/census. mobile_web_app: name: mobile web app bundle needs: [code_paths] if: needs.code_paths.outputs.mobile_web_app == 'true' runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false # Why no native-runtime: the builder is esbuild and the render check is a browser. Nothing # in this job loads node-pty. - uses: ./.github/actions/install-node-dependencies with: native-runtime: none cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml # Test drawer with WebKit as iOS WebView uses it; runner Chrome cannot substitute. - name: Install WebKit for the drawer check id: webkit background: true run: | bash .github/scripts/prepare-ubuntu-apt.sh pnpm exec playwright-core install --with-deps webkit # The entry lives in mobile/ so one React resolves; without this every RN import is nothing. - uses: ./.github/actions/install-mobile-dependencies - name: Prepare mobile route snapshot id: mobile-routes background: true run: node config/scripts/run-mobile-web-app-checks.mjs --prepare-route-snapshot "$RUNNER_TEMP/mobile-routes.json" # Require runner Google Chrome: Ubuntu’s AppArmor userns profile covers its .deb, unlike downloaded Chromium; missing render checks must fail. - name: Resolve Chrome for the render check run: | set -euo pipefail chrome="$(command -v google-chrome || command -v google-chrome-stable || true)" if [ -z "$chrome" ]; then echo "::error::No Google Chrome on the runner; the render check would silently skip." exit 1 fi "$chrome" --version echo "ORCA_MOBILE_WEB_RENDER_BROWSER=$chrome" >> "$GITHUB_ENV" - name: Build and verify the app bundle run: pnpm run build:mobile-web # Browser checks must observe both a finished install and the built bundle. - wait: [webkit, mobile-routes] # Require mobile install and select census family by quoted filename prefix, matching routing; stale file lists/env-gated skips previously hid missing coverage. - name: Builder, override census and render checks env: ORCA_MOBILE_WEB_APP_DEPS_REQUIRED: '1' ORCA_MOBILE_WEB_PREPARED_ROUTE_SNAPSHOT: ${{ runner.temp }}/mobile-routes.json run: | node config/scripts/run-mobile-web-app-checks.mjs cross-version-wire: name: cross-version wire compatibility needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.cross-version-wire == 'true' && needs.preflight.result == 'success' # Why ARM: source-only: tagged checkout plus in-process vitest, no docker or browser. runs-on: ubuntu-24.04-arm steps: # Fetch tags/full history for newest-release skew; shallow unit shards cannot supply it. - name: Checkout uses: actions/checkout@v6 with: # Use blob:none for merge-base history because blobs are ~89% of pack; fetch rare historical contents on demand. fetch-depth: 0 filter: blob:none persist-credentials: false - uses: ./.github/actions/install-node-dependencies with: native-runtime: node # R1: an upgrade from the newest release must adopt its live terminal daemon. - name: Daemon protocol crossing against newest release run: pnpm run check:daemon-protocol-crossing # R3: a runtime launcher swap must not also bump the daemon protocol (D7.1). # The label is read from the event payload, so push again after applying it. - name: Runtime launcher change does not bump daemon protocol env: BASE_SHA: ${{ github.event.pull_request.base.sha }} ORCA_ALLOW_RUNTIME_LAUNCHER_PROTOCOL_BUMP: ${{ contains(github.event.pull_request.labels.*.name, 'allow-runtime-launcher-protocol-bump') }} run: | DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" pnpm run check:runtime-launcher-protocol-ratchet -- --base "$DIFF_BASE" # Run the entire directory; zero matches fail so new contracts cannot be omitted silently. - name: Old/new client and server compatibility journeys run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/ managed_hook_node18: name: managed hooks on Node 18 needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.managed_hook_node18 == 'true' && needs.preflight.result == 'success' # Retain the required check while sharing preflight's checkout and built packages. runs-on: ubuntu-slim timeout-minutes: 5 steps: - name: Require process-host package smoke on Node 18 env: MANAGED_HOOK_NODE18: ${{ needs.preflight.outputs.managed_hook_node18 }} run: | if [ "$MANAGED_HOOK_NODE18" != success ]; then echo "::error::Node 18 process-host smoke: expected success, got $MANAGED_HOOK_NODE18" exit 1 fi package: name: package needs: [code_paths, preflight] if: needs.code_paths.outputs.package == 'true' runs-on: ubuntu-24.04 # Let the serial Docker gates reach their own deadlines and report cleanup failures. timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - name: Cache electron-builder downloads uses: actions/cache/restore@v5 with: path: ~/.cache/electron-builder key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | electron-builder-linux- # Restore in the foreground: background steps may only be single run steps (ci-background-steps.test.mjs). - uses: ./.github/actions/prepare-linux-package-fixture id: daemon-fixture-cache-restore with: fixture: daemon-shutdown-descendants load: 'false' - name: Load daemon shutdown fixture image id: daemon-fixture-cache background: true env: FIXTURE: daemon-shutdown-descendants CACHE_HIT: ${{ steps.daemon-fixture-cache-restore.outputs.cache-hit }} run: bash .github/actions/prepare-linux-package-fixture/load-fixture.sh - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml # Why here: electron-builder's beforePack requires out/mobile-web, and the bundle # build resolves React Native and Expo from mobile/node_modules. - uses: ./.github/actions/install-mobile-dependencies # The checkout must pass without depending on a historical baseline that leaks. - wait: daemon-fixture-cache - name: Verify Linux daemon shutdown descendant cleanup env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_DAEMON_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.daemon-fixture-cache.outputs.image }} run: node config/scripts/run-daemon-shutdown-descendants-docker.mjs # Serialize dual-Electron lifecycle files: four concurrent probes on four vCPUs starve each other past their own deadlines. - name: Test Linux Electron lifecycle boundary env: ORCA_BACKGROUND_LAUNCH: '1' run: >- xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts --no-file-parallelism src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts src/main/browser/browser-route-webrtc-egress.electron.test.ts src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts src/main/browser/popup-origin-chrome.electron.test.ts src/main/browser/browser-link-materialization.electron.test.ts src/main/browser/browser-popup-compatibility.electron.test.ts src/main/browser/browser-popup-adapter-compatibility.electron.test.ts src/main/browser/browser-popup-native-compatibility.electron.test.ts src/main/browser/browser-popup-background-compatibility.electron.test.ts src/main/browser/extensions/browser-extension-api.electron.test.ts src/main/browser/extensions/browser-extension-persistence.electron.test.ts src/main/browser/extensions/browser-extension-lifecycle.electron.test.ts src/main/browser/extensions/browser-extension-toolbar.electron.test.ts src/main/browser/extensions/browser-extension-native-messaging.electron.test.ts - name: Restore native components id: linux-native-restore uses: ./.github/actions/prepare-wayland-clipboard with: build: 'false' # The isolated glibc-floor build writes only the Wayland helper, so JavaScript can overlap. - name: Build native components id: linux-native background: true env: CACHE_HIT: ${{ steps.linux-native-restore.outputs.cache-hit }} run: bash .github/actions/prepare-wayland-clipboard/build-or-validate.sh # Load independent fixtures during builds; keep native lifecycle probes serial. - uses: ./.github/actions/prepare-linux-package-fixture id: shutdown-fixture-cache-restore with: fixture: headless-serve-shutdown load: 'false' - name: Load headless serve shutdown fixture image id: shutdown-fixture-cache background: true env: FIXTURE: headless-serve-shutdown CACHE_HIT: ${{ steps.shutdown-fixture-cache-restore.outputs.cache-hit }} run: bash .github/actions/prepare-linux-package-fixture/load-fixture.sh - uses: ./.github/actions/prepare-linux-package-fixture id: cli-fixture-cache-restore with: fixture: cli-launch-contract load: 'false' - name: Load CLI launch contract fixture image id: cli-fixture-cache background: true env: FIXTURE: cli-launch-contract CACHE_HIT: ${{ steps.cli-fixture-cache-restore.outputs.cache-hit }} run: bash .github/actions/prepare-linux-package-fixture/load-fixture.sh - name: Install Linux package tooling id: linux-package-tools background: true run: | # Use the shell lane's bounded refresh; required tooling installation stays fatal. sudo tee /etc/apt/apt.conf.d/99-orca-package-tools >/dev/null <<'APTCONF' Acquire::http::Timeout "15"; Acquire::https::Timeout "15"; Acquire::Retries "1"; APTCONF timeout 120 sudo apt-get update || true timeout 300 sudo apt-get install -y cpio rpm - name: Build package inputs env: ORCA_CI_MAIN_SOURCEMAPS: '0' run: | status=0 pnpm run build:cli || status=1 scripts=(build:wsl-guest build:electron-vite:parallel) pids=() for script in "${scripts[@]}"; do pnpm run "$script" & pids+=("$!") done for pid in "${pids[@]}"; do wait "$pid" || status=1 done exit "$status" - name: Project web client from renderer build id: web-client background: true run: pnpm run build:web-from-renderer # Build the real ~8MB mobile page because stepwise package assembly bypasses build:release and beforePack refuses its absence. - name: Build mobile web bundle run: pnpm run build:mobile-web - wait: [linux-native, linux-package-tools, web-client] - name: Package unpacked app env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1' # Prepare once with every hook, then isolate the format-specific mutations. run: | pnpm exec electron-builder --config config/electron-builder.config.cjs --linux dir --x64 --publish never node config/scripts/package-linux-formats.mjs - name: Verify root-package marker payloads run: | set -euo pipefail version="$(node -p "require('./package.json').version")" deb="dist/orca-ide_${version}_amd64.deb" rpm="dist/orca-ide-${version}.x86_64.rpm" test -s "$deb" test -s "$rpm" deb_marker="$(dpkg-deb --fsys-tarfile "$deb" | tar -xOf - ./opt/Orca/resources/package-type)" rpm_marker="$(rpm2cpio "$rpm" | cpio --quiet --extract --to-stdout ./opt/Orca/resources/package-type)" [[ "$deb_marker" == deb ]] || { echo "Expected deb marker, got: $deb_marker"; exit 1; } [[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; } # Only the AppImage carries the pinned Node (config/scripts/appimage-node-runtime.cjs). if dpkg-deb --fsys-tarfile "$deb" | tar -t | grep '/resources/node-runtime' >/dev/null; then echo "The deb must not carry the AppImage's Node"; exit 1 fi if rpm2cpio "$rpm" | cpio --quiet -t | grep '/resources/node-runtime' >/dev/null; then echo "The rpm must not carry the AppImage's Node"; exit 1 fi appimage_root="$(mktemp -d)" (cd "$appimage_root" && "$GITHUB_WORKSPACE/dist/orca-linux.AppImage" --appimage-extract 'resources/node-runtime/bin/node' >/dev/null) test -x "$appimage_root/squashfs-root/resources/node-runtime/bin/node" "$appimage_root/squashfs-root/resources/node-runtime/bin/node" --version rm -rf "$appimage_root" ls -l dist/orca-linux.AppImage "$deb" "$rpm" # Kept briefly so a reviewer can run this exact AppImage on a real Linux desktop. - uses: actions/upload-artifact@v7 with: name: orca-linux-appimage-${{ github.event.pull_request.head.sha }} path: dist/orca-linux.AppImage retention-days: 3 compression-level: 0 - wait: shutdown-fixture-cache - name: Verify headless serve signal shutdown env: ORCA_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.shutdown-fixture-cache.outputs.image }} run: >- node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage --all-entrypoints # A default container reproduces the hostile AppImage launch environment. - wait: cli-fixture-cache - name: Verify Linux CLI launch contract env: ORCA_CLI_FIXTURE_CACHE_IMAGE: ${{ steps.cli-fixture-cache.outputs.image }} run: node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage - name: Smoke packaged CLI run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked - name: Smoke packaged hang watchdog worker run: xvfb-run --auto-servernum node config/scripts/smoke-packaged-hang-watchdog-worker.mjs --app-dir=dist/linux-unpacked package_windows: name: package (windows) needs: [code_paths, preflight] if: needs.code_paths.outputs.package_windows == 'true' runs-on: windows-2022 timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - name: Cache electron-builder downloads uses: actions/cache/restore@v5 with: path: | ~\AppData\Local\electron\Cache ~\AppData\Local\electron-builder\Cache # Release builds seed this exact path set; PR-local copies cannot serve other PRs. key: electron-builder-win-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | electron-builder-win- # Do not save Node cache post-job: later Electron rebuild mutates the same paths to another ABI. - uses: ./.github/actions/install-node-dependencies id: deps with: native-runtime: node persist-native-cache: 'false' cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml # Why here: electron-builder's beforePack requires out/mobile-web, and the bundle # build resolves React Native and Expo from mobile/node_modules. - uses: ./.github/actions/install-mobile-dependencies - name: Save compiled Node native modules if: steps.deps.outputs.native-cache-hit != 'true' uses: actions/cache/save@v5 with: path: ${{ steps.deps.outputs.native-cache-path }} key: ${{ steps.deps.outputs.native-cache-key }} # vitest runs here directly rather than through `pnpm test`, so the addon # assertions only hold once install-node-dependencies has rebuilt natives. - name: Test Windows installer process probe if: needs.code_paths.outputs.windows_process_probe != 'false' env: ORCA_BACKGROUND_LAUNCH: '1' # Serialize the two suites so their cold PowerShell/CIM starts cannot contend. run: >- pnpm exec vitest run --config config/vitest.config.ts --no-file-parallelism config/scripts/nsis-process-check.test.mjs config/scripts/idle-cpu-process-sampling.test.mjs - name: Cache Windows CLI launcher uses: actions/cache@v5 with: # Cache cargo registry with .build so misses do not rebuild resource crates cold. path: | native/windows-cli-launcher/.build native/windows-cli-launcher/target native/windows-agent-hook/.build native/windows-agent-hook/target ~/.cargo/registry key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} # Require cargo early; otherwise packaged Rust launcher fails deep inside builder native hooks. - name: Ensure the Rust toolchain for the Windows CLI launcher shell: pwsh run: | if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { rustup toolchain install stable --profile minimal rustup default stable } cargo --version # Build agent-hook once before tests so suites share the binary instead of compiling concurrently. - name: Build the Windows agent hook run: node config/scripts/build-windows-agent-hook.mjs - name: Build the Windows CLI launcher run: node config/scripts/build-windows-cli-launcher.mjs # Files come from WINDOWS_BOUNDARY_TESTS, the same list that decides whether this job runs. - name: Test Windows-specific boundaries env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_REAL_WINDOWS_SKILL_TEST: '1' run: | $files = node config/scripts/pr-package-test-paths.mjs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts @files # Isolate slow-taskkill CPU burners so they cannot starve unrelated test files. - name: Test Windows tree kill under CPU load env: ORCA_WINDOWS_TREE_KILL_LOAD: '1' run: >- pnpm exec vitest run --config config/vitest.config.ts src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts # Use parallel Vite targets as already packaged/smoked on Linux; release behavior is otherwise identical. - name: Build package inputs env: ORCA_CI_MAIN_SOURCEMAPS: '0' run: pnpm run build:release:parallel - name: Prepare Electron native runtime uses: ./.github/actions/prepare-native-runtime with: native-runtime: electron node-version: ${{ steps.deps.outputs.node-version }} - name: Package unpacked app env: ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1' run: pnpm exec electron-builder --config config/electron-builder.config.cjs --dir - name: Smoke packaged Windows PTY native capability run: pnpm run smoke:windows-pty-native-capability -- --exe=dist/win-unpacked/Orca.exe - name: Smoke packaged CLI run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/win-unpacked e2e: name: e2e needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.e2e_should_run == 'true' && ( needs.preflight.result == 'success' || ( needs.preflight.result == 'skipped' && needs.code_paths.outputs.static_analysis == 'false' && needs.code_paths.outputs.typecheck == 'false' ) ) # Why: reusable e2e.yml only checkouts, builds, and uploads artifacts. permissions: contents: read uses: ./.github/workflows/e2e.yml with: # The synthetic pull-request merge ref can disappear while this reusable # workflow is queued. The head SHA is immutable and works for every PR. ref: ${{ github.event.pull_request.head.sha }} test_files: ${{ needs.code_paths.outputs.test_files }} run_changed_e2e: ${{ needs.code_paths.outputs.e2e_run_changed != 'false' }} needs_build: ${{ needs.code_paths.outputs.e2e_needs_build != 'false' }} managed_ssh_shards: ${{ needs.code_paths.outputs.e2e_ssh_shards || '["1/2","2/2"]' }} # Gate the green layout oracles separately from main’s red broader E2E suite: changed-spec window plus both orcad serve hosts. layout_oracle: name: layout oracle needs: [code_paths, preflight] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.layout_oracle == 'true' && needs.preflight.result == 'success' permissions: contents: read uses: ./.github/workflows/e2e.yml with: # Why the merge commit, unlike `e2e`: a gate must test what lands on main, not a stale head. ref: ${{ github.sha }} test_files: '["tests/e2e/workspace-layout-oracle-headless.spec.ts","tests/e2e/workspace-layout-oracle.spec.ts"]' run_changed_e2e: true needs_build: true artifact_suffix: -layout-oracle # Keep native IME advisory while PR reliability is established (nightly 20/20, 2026-08-09..29, p50 3m25s); do not hide red via continue-on-error. To gate, add verify.needs, TERMINAL_IME_NATIVE env and separate success-or-skipped validation. terminal_ime_native: name: real IME needs: code_paths if: needs.code_paths.outputs.native_ime_source_changed == 'true' # Why: the reusable workflow only checks out, builds, and uploads artifacts. permissions: contents: read uses: ./.github/workflows/terminal-ime-e2e.yml windows_wsl: name: real WSL terminal needs: code_paths if: needs.code_paths.outputs.wsl_source_changed == 'true' permissions: contents: read uses: ./.github/workflows/windows-wsl-e2e.yml with: ref: ${{ github.event.pull_request.head.sha }} # Install like developers’ global virtual store in empty HOME/store; hoisting or stray dependencies can mask undeclared imports. dependency_isolation: name: dependency isolation under the global virtual store needs: [code_paths] if: >- !cancelled() && needs.code_paths.result == 'success' && needs.code_paths.outputs.dependency_isolation == 'true' runs-on: ubuntu-24.04 timeout-minutes: 30 env: ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI: '1' steps: # Persist layout via GITHUB_ENV (job env cannot read runner.temp) for every pnpm command/install check. - name: Point HOME and the pnpm store at empty temp dirs run: | mkdir -p "$RUNNER_TEMP/empty-home" { echo "HOME=$RUNNER_TEMP/empty-home" echo "pnpm_config_store_dir=$RUNNER_TEMP/isolated-store" } >> "$GITHUB_ENV" - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version-file: package.json check-latest: true package-manager-cache: false - name: Setup pnpm uses: ./.github/actions/setup-pnpm - name: Install with every package isolated in its own store slot run: | set -euo pipefail pnpm install --frozen-lockfile --ignore-scripts if [ -d node_modules/bun ]; then pnpm rebuild bun fi - name: Typecheck and check undeclared dependency resolution run: | set -euo pipefail pnpm tc pnpm exec vitest run --config config/vitest.config.ts config/scripts/undeclared-dependency-resolution.test.ts # Pointless if any pnpm command above fell back to the hoisted layout or another store. case "$(realpath node_modules/zod)" in "$(realpath "$pnpm_config_store_dir")"/*) ;; *) echo "node_modules/zod is not in the isolated store"; exit 1 ;; esac verify: name: PR checks passed # Publish an explicit result after every required dependency settles. if: ${{ !cancelled() && github.event.pull_request.draft != true }} needs: - code_paths - preflight - git_compatibility - codex_index_heal_contract - xterm_patch_sync - shell_contracts - test - mobile_web_app - cross-version-wire - managed_hook_node18 - package - package_windows - layout_oracle - dependency_isolation runs-on: ubuntu-slim timeout-minutes: 5 steps: # Keep E2E advisory while main’s scheduled suite is red, allowing fixes to merge. To gate, add e2e to needs/E2E env and check success-or-skipped outside the strict required-job loop. - name: Require successful checks env: CODE_PATHS: ${{ needs.code_paths.result }} SHOULD_RUN: ${{ needs.code_paths.outputs.should_run }} PREFLIGHT: ${{ needs.preflight.result }} STATIC_ANALYSIS_SHOULD_RUN: ${{ needs.code_paths.outputs.static_analysis }} TYPECHECK_SHOULD_RUN: ${{ needs.code_paths.outputs.typecheck }} PREFLIGHT_SHOULD_RUN: ${{ needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true' }} GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }} GIT_COMPATIBILITY_SHOULD_RUN: ${{ needs.code_paths.outputs.git_compatibility }} CODEX_INDEX_HEAL_CONTRACT: ${{ needs.codex_index_heal_contract.result }} CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN: ${{ needs.code_paths.outputs.codex_index_heal_contract }} XTERM_PATCH_SYNC: ${{ needs.xterm_patch_sync.result }} XTERM_PATCH_SYNC_SHOULD_RUN: ${{ needs.code_paths.outputs.xterm_patch_sync }} SHELL_CONTRACTS: ${{ needs.shell_contracts.result }} SHELL_CONTRACTS_SHOULD_RUN: ${{ needs.code_paths.outputs.shell_contracts }} TEST: ${{ needs.test.result }} TEST_SHOULD_RUN: ${{ needs.code_paths.outputs.test }} MOBILE_WEB_APP: ${{ needs.mobile_web_app.result }} MOBILE_WEB_APP_SHOULD_RUN: ${{ needs.code_paths.outputs.mobile_web_app }} CROSS_VERSION_WIRE: ${{ needs.cross-version-wire.result }} CROSS_VERSION_WIRE_SHOULD_RUN: ${{ needs.code_paths.outputs.cross-version-wire }} MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }} MANAGED_HOOK_NODE18_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node18 }} PACKAGE: ${{ needs.package.result }} PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }} PACKAGE_WINDOWS: ${{ needs.package_windows.result }} PACKAGE_WINDOWS_SHOULD_RUN: ${{ needs.code_paths.outputs.package_windows }} LAYOUT_ORACLE: ${{ needs.layout_oracle.result }} LAYOUT_ORACLE_SHOULD_RUN: ${{ needs.code_paths.outputs.layout_oracle }} DEPENDENCY_ISOLATION: ${{ needs.dependency_isolation.result }} DEPENDENCY_ISOLATION_SHOULD_RUN: ${{ needs.code_paths.outputs.dependency_isolation }} run: | if [ "$CODE_PATHS" != "success" ]; then exit 1 fi case "$STATIC_ANALYSIS_SHOULD_RUN:$TYPECHECK_SHOULD_RUN" in true:true|true:false|false:true|false:false) ;; *) echo "preflight: missing or invalid selection"; exit 1 ;; esac if [ "$SHOULD_RUN" != "true" ]; then echo "Docs-only change; expensive PR checks skipped." fi failed=0 check_job() { local name="$1" result="$2" should="$3" if [ "$should" = "true" ]; then if [ "$result" != "success" ]; then echo "$name: expected success, got $result" failed=1 fi elif [ "$should" = "false" ]; then if [ "$result" != "skipped" ]; then echo "$name: expected skipped, got $result" failed=1 fi else echo "$name: missing or invalid selection" failed=1 fi } # Require success when the PR has code-relevant changes check_job preflight "$PREFLIGHT" "$PREFLIGHT_SHOULD_RUN" check_job git_compatibility "$GIT_COMPATIBILITY" "$GIT_COMPATIBILITY_SHOULD_RUN" check_job codex_index_heal_contract "$CODEX_INDEX_HEAL_CONTRACT" "$CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN" check_job xterm_patch_sync "$XTERM_PATCH_SYNC" "$XTERM_PATCH_SYNC_SHOULD_RUN" check_job shell_contracts "$SHELL_CONTRACTS" "$SHELL_CONTRACTS_SHOULD_RUN" check_job test "$TEST" "$TEST_SHOULD_RUN" check_job mobile_web_app "$MOBILE_WEB_APP" "$MOBILE_WEB_APP_SHOULD_RUN" check_job cross-version-wire "$CROSS_VERSION_WIRE" "$CROSS_VERSION_WIRE_SHOULD_RUN" check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN" check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN" check_job package_windows "$PACKAGE_WINDOWS" "$PACKAGE_WINDOWS_SHOULD_RUN" check_job layout_oracle "$LAYOUT_ORACLE" "$LAYOUT_ORACLE_SHOULD_RUN" check_job dependency_isolation "$DEPENDENCY_ISOLATION" "$DEPENDENCY_ISOLATION_SHOULD_RUN" exit "$failed"