mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
1625 lines
74 KiB
YAML
1625 lines
74 KiB
YAML
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
|
||
/config/scripts/pr-shell-contract-prefixes.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/pr-editor-preview-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
|
||
no-cache: true
|
||
|
||
- 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'
|
||
# Free hosted ARM: no job on the gate's critical path waits for preflight.
|
||
runs-on: ubuntu-24.04-arm
|
||
outputs:
|
||
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
|
||
no-cache: true
|
||
|
||
# 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
|
||
|
||
- name: Verify mobile localization
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: bun config/scripts/verify-mobile-localization.mjs
|
||
|
||
- name: Verify mobile localization coverage
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: bun run verify:mobile-localization-coverage
|
||
|
||
- 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: test-file-runners
|
||
|
||
- 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:-<fish not installed>}"
|
||
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/fish-wsl-cwd-report.node-pty.test.ts \
|
||
src/main/wsl-sh-cwd-report.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
|
||
|
||
# Shards wait only for the plan, not preflight, which moves them about two minutes earlier.
|
||
unit_plan:
|
||
needs: [code_paths]
|
||
if: needs.code_paths.outputs.test == 'true'
|
||
uses: ./.github/workflows/unit-plan.yml
|
||
with:
|
||
selection_mode: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
|
||
|
||
test:
|
||
needs: [code_paths, unit_plan]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.test == 'true' &&
|
||
needs.unit_plan.result == 'success'
|
||
uses: ./.github/workflows/unit-tests.yml
|
||
with:
|
||
node_versions: '["24"]'
|
||
# Free hosted ARM: 12 shards measured 361s median cold vs ~390s for 5 warm Blacksmith 8-vCPU shards.
|
||
runner: ubuntu-24.04-arm
|
||
phase_diagnostics: true
|
||
# Restore main's transform cache; the `ci:cold-unit-cache` label or ORCA_UNIT_MODULE_CACHE=off forces cold.
|
||
module_cache: ${{ (vars.ORCA_UNIT_MODULE_CACHE == 'off' || contains(github.event.pull_request.labels.*.name, 'ci:cold-unit-cache')) && 'off' || 'warm' }}
|
||
shards: ${{ needs.unit_plan.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]
|
||
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
|
||
|
||
# Electron and builder releases fix what this downloads; the lockfile churns far more often.
|
||
- name: Resolve electron-builder download versions
|
||
id: eb-versions
|
||
shell: bash
|
||
run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT"
|
||
|
||
# The platform warmup seeds this on main.
|
||
- name: Cache electron-builder downloads
|
||
uses: actions/cache/restore@v5
|
||
with:
|
||
path: ~/.cache/electron-builder
|
||
key: electron-builder-linux-${{ steps.eb-versions.outputs.versions }}
|
||
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/cookies/browser-google-cookie-clear.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]
|
||
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
|
||
|
||
# Electron and builder releases fix what this downloads; the lockfile churns far more often.
|
||
- name: Resolve electron-builder download versions
|
||
id: eb-versions
|
||
shell: bash
|
||
run: node -p "const d = require('./package.json').devDependencies; 'versions=electron' + d.electron + '-builder' + d['electron-builder']" >> "$GITHUB_OUTPUT"
|
||
|
||
- 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-${{ steps.eb-versions.outputs.versions }}
|
||
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
|
||
|
||
# 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: Resolve the Windows Rust build scope
|
||
id: rust-scope
|
||
shell: bash
|
||
run: node config/scripts/windows-rust-build-scope.mjs >> "$GITHUB_OUTPUT"
|
||
|
||
# Real inputs only: older restores are safe because the build re-checks each exe's fingerprint.
|
||
- 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-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}-${{ steps.rust-scope.outputs.app-version }}-${{ hashFiles('native/windows-cli-launcher/src/**', 'native/windows-cli-launcher/build.rs', 'native/windows-cli-launcher/Cargo.toml', 'native/windows-cli-launcher/Cargo.lock', 'native/windows-cli-launcher/app.manifest', 'native/windows-cli-launcher/.cargo/**', 'native/windows-agent-hook/src/**', 'native/windows-agent-hook/build.rs', 'native/windows-agent-hook/Cargo.toml', 'native/windows-agent-hook/Cargo.lock', 'native/windows-agent-hook/app.manifest', 'native/windows-agent-hook/.cargo/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'config/scripts/windows-pe-imports.mjs', 'resources/build/icon.ico') }}
|
||
restore-keys: |
|
||
windows-cli-launcher-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust-scope.outputs.toolchain }}-
|
||
|
||
# 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]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.layout_oracle == 'true'
|
||
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"]'
|
||
# Two ~3.6 min halves of the 21 window scenarios keep this gate off the PR's critical path.
|
||
changed_e2e_test_shards: '["1/2","2/2"]'
|
||
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"
|