mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* ci: fail when a checked-in test file has no runner Two node:test files under .github/actions lost their runner when #31792 trimmed cloud pretest, and the two monaco .bench.ts budgets were never run by any CI job. Wire both back in and add a static check that every test-like file is collected by a declared runner or imported by a collected entry (the src .spec.* suite-module convention). * ci: ignore type-only imports and quote the lease glob for cmd.exe * ci: parse type-only imports, cover UNIT_EXCLUDE, route cloud runner edits Type-only imports are now dropped with the TypeScript parser, so a type alias before a runtime re-export no longer hides it. UNIT_EXCLUDE files must be named by a pr.yml or unit-tests.yml step. A cloud-only diff that adds a test or edits a runner config now runs the static job. The monaco benchmarks stay manual (pnpm test:perf:benchmarks) until a CI runner proves their wall-clock budgets. * ci: require UNIT_INCLUDE names in dedicated jobs and erase typeof import() * ci: resolve each runtime import to the one file Vite loads The unit dependency graph keeps every plausible candidate so it can invalidate conservatively, which cannot prove a suite runs: an import of ./case.spec counted both case.spec.ts and case.spec.tsx. Resolve with Vite's extension order instead. * ci: take runtime imports from transpiled output Imports used only as types are erased without a type modifier, so parsing for type-only syntax still followed them. Derive edges from ts.transpileModule output, which erases what Vitest's transform erases, and only transpile files that can reach an uncollected candidate. * ci: fix the agent-status benchmark config and route all cloud code The agent-status benchmark config inherited the unit projects, whose includes override its own, so it ran the whole unit suite and never the benchmark. Clear projects as the performance config does. Route every cloud code file to the static job, since content-detected tests can have any name.
1680 lines
78 KiB
YAML
1680 lines
78 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:
|
|
# Why: a README/docs-only PR used to start the full matrix (test shards,
|
|
# two package jobs, typecheck, git compat, xterm, shell contracts). Path
|
|
# filters on `on.pull_request` would drop the `verify` check entirely; this
|
|
# detector keeps verify as the required aggregate and skips the expensive jobs.
|
|
# Per-job outputs also skip git-compat/xterm/packaging/shell when those
|
|
# inputs are unchanged; empty diffs fail closed and run everything.
|
|
code_paths:
|
|
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
|
|
name: detect changes and check repository guards
|
|
# Start routing without waiting behind the GitHub-hosted test queue.
|
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
|
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:
|
|
# Why depth 50 and not 0: every diff below resolves to HEAD^1, so only the merge commit
|
|
# and a little slack are needed. Fetching all 8127 refs' commit graph cost ~20s here and
|
|
# is charged to the start of all 22 jobs, since they all need this one.
|
|
# Why blob:none stays: the sparse tree below is ~7 files, so there are no blobs to
|
|
# materialize and no promisor refetch. Measured 9.5s -> 1.6s against 20.7s today.
|
|
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/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
|
|
# Why HEAD^1 and not --merge-base: HEAD is the pull request merge commit, so its first
|
|
# parent is the base side already. Computing a merge base instead would require the
|
|
# payload base SHA to be in the graph, which is what forced a full-history checkout.
|
|
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
|
# Why --no-renames: name-only rename detection can report only the destination.
|
|
# A code file moved under docs/ must still expose its code-side deletion.
|
|
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
|
|
# Source routes are executable contracts so a test can prove exact
|
|
# authorities, exclusions, and sentinels without evaluating workflow 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"
|
|
# Why its own signal: the real-IME lane is a whole ibus session, not a spec, so it must
|
|
# trigger on IME source rather than on a spec name in some route's list.
|
|
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:
|
|
# Why depth 50: the gates below diff against HEAD^1, so no merge base is computed and
|
|
# the payload base SHA need not be in the graph.
|
|
# Why no blob:none here, unlike code_paths: this job checks out all 30,226 files, and
|
|
# the filter then forces a second promisor fetch of nearly every blob. Measured 23s
|
|
# blobless against ~11s without it.
|
|
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"
|
|
|
|
# Why two guarded installs: the mixed root+mobile store entry is 537 MB against
|
|
# 321 MB for root alone, and restoring it costs 8.6s against 4.6s. Most PRs skip the
|
|
# mobile install below, so they were paying 216 MB for packages they never link. The
|
|
# root-only key is also the one the hourly warmer reseeds. Only one of these runs.
|
|
- 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# Why here: the changed-code gate lints mobile files too, and its type-aware pass
|
|
# resolves types from mobile/node_modules. Without the install every mobile type
|
|
# degrades to an `error` type — reported as phantom findings against the changed lines.
|
|
- 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
|
|
|
|
- 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
|
|
|
|
- name: Check every test file has a runner
|
|
if: needs.code_paths.outputs.static_analysis == 'true'
|
|
run: 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
|
|
|
|
# Why: extraction writes sorted evidence to an isolated temporary path,
|
|
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
|
|
# Use preflight's pinned Bun even when the optional local pin differs.
|
|
- 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: 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'
|
|
|
|
# Why both: the ratchet proves nothing reachable from the runtime imports electron,
|
|
# which is a property of the import graph. This proves the Node artifact it enables
|
|
# actually boots, pairs, creates a worktree and round-trips a real PTY.
|
|
- 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]
|
|
|
|
# Why: project-owned type declarations must live in .ts so tsc
|
|
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
|
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
|
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
|
|
# past typecheck. 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
|
|
|
|
# Planning shares setup and stays off the compiler's critical path.
|
|
- 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 still request ten.
|
|
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
|
|
|
|
- wait: typecheck
|
|
|
|
- wait: process-host-imports
|
|
|
|
- wait: main-sync-spawn-ratchet
|
|
|
|
- wait: unit-plan
|
|
|
|
- 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'
|
|
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"
|
|
)
|
|
# Why pull up front: a lane's first `docker run` otherwise pulls its image
|
|
# while the sibling lane is mid-test, and that stall is charged to the test.
|
|
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"
|
|
|
|
# Why this job: Orca's session index-heal depends on a Codex behavior — a
|
|
# `thread/read` of an unindexed rollout performs a read-repair that inserts the
|
|
# `threads` row. Every unit test drives a stub app-server and asserts only that the
|
|
# call did not error, so if Codex dropped the repair they would all stay green while
|
|
# the subsystem went inert. This runs the pinned real binary and fails when the
|
|
# repair stops happening. Pinned because the binary is the thing expected to drift.
|
|
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: blacksmith-4vcpu-ubuntu-2404-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:
|
|
# Why REQUIRED: without a binary the suite skips, and a job that skips
|
|
# reports success. This turns a failed or missing install into a red test
|
|
# instead of a green no-op.
|
|
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
|
|
|
|
# Why both pins and latest: Orca approves its managed-home hook entry with a hash
|
|
# Codex computes, so a release that changes it must redden here before users
|
|
# meet a review screen.
|
|
- 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'
|
|
# Why ARM: the patch check rebuilds 4 packages x 2 builds and byte-compares against the
|
|
# checked-in bundles. Those were generated on Linux x64 and reproduce byte-for-byte on
|
|
# darwin-arm64, so the output is neither host-arch nor host-OS dependent.
|
|
runs-on: blacksmith-4vcpu-ubuntu-2404-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: blacksmith-4vcpu-ubuntu-2404-arm
|
|
# Bound stalled prerequisite downloads instead of waiting for GitHub's 6-hour default.
|
|
timeout-minutes: 15
|
|
env:
|
|
# Why: the suites below gate their live fish tests on the binary, which is
|
|
# right on a developer machine and wrong here — this job is a required check
|
|
# and its fish lane is the only end-to-end guard for #9993, so a skip would
|
|
# report green with nothing exercised. Turns those skips into failures.
|
|
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
|
|
|
|
# Separate from the install so the failure names the contract, not an apt error.
|
|
# ORCA_REQUIRE_FISH re-checks this at test time; this step just fails in seconds
|
|
# instead of after a full dependency install.
|
|
- 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/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' }}
|
|
|
|
# Why a sibling and not part of the test workflow: it is advisory, so it must not delay the
|
|
# gate. Inside unit-tests.yml a caller's `needs: test` waited for it, holding verify ~36s
|
|
# after the last shard. Deliberately absent from verify's needs for the same reason.
|
|
unit_selection_evidence:
|
|
needs: [test]
|
|
if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }}
|
|
uses: ./.github/workflows/unit-selection-evidence.yml
|
|
|
|
# Why its own job: it needs mobile/node_modules and a real browser, and the sharded `test`
|
|
# matrix would pay for both on every shard to run two files. It builds the same bundle the
|
|
# package job ships, and adds the render and census checks packaging does not run.
|
|
mobile_web_app:
|
|
name: mobile web app bundle
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.mobile_web_app == 'true'
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
|
|
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
|
|
|
|
# The drawer check runs on WebKit as well as Chrome, because the shell's iOS WebView is
|
|
# WebKit and the Chrome above cannot stand in for it. Downloaded rather than resolved from
|
|
# the runner: Ubuntu ships no WebKit build to point at.
|
|
- 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"
|
|
|
|
# Why the runner's Google Chrome and not a downloaded chromium: same reason as the orcad
|
|
# browser job -- Ubuntu 24.04 only ships an AppArmor userns profile for the Chrome .deb.
|
|
# Why fail instead of skip: a silently skipped render check is the failure this job exists
|
|
# to prevent.
|
|
- 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]
|
|
|
|
# The bundling tests skip themselves where mobile dependencies are absent, which is how they
|
|
# stay green in the sharded `test` job. This is the job that installs them, so here a missing
|
|
# install has to fail rather than skip everything the job exists to run.
|
|
# Why a prefix and not a file list: the list this replaces had gone stale twice without
|
|
# anyone noticing, because a census whose closure block skips without the env flag below is
|
|
# green in the sharded `test` job whether or not it ever runs here. The prefix is the same
|
|
# one `pr-code-change-scope.mjs` fires this job on, so naming a test into the family is all
|
|
# it takes to have it run. Quoted because these are vitest filename filters, matched as
|
|
# substrings against the discovered files, and the shell must not touch them.
|
|
# Route censuses share fresh dependency lists for this invocation; scratch builds stay independent.
|
|
- 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: blacksmith-4vcpu-ubuntu-2404-arm
|
|
|
|
steps:
|
|
# Why fetch-depth 0: the harness extracts the newest release tag to skew
|
|
# current code against it. The default shallow clone has no tags, which is
|
|
# why this cannot ride along in the sharded `test` job.
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Why blob:none: full history is needed for the merge-base diff, but historical
|
|
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
|
# few this job actually reads 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"
|
|
|
|
# Runs the whole directory so a new file there cannot be left out; an empty match
|
|
# exits 1 ("No test files found"), so this lane cannot pass while running zero tests.
|
|
- 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: blacksmith-2vcpu-ubuntu-2404
|
|
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: blacksmith-4vcpu-ubuntu-2404
|
|
# 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
|
|
|
|
# Why --no-file-parallelism: every file here launches a full Electron stack twice, and each
|
|
# probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other
|
|
# past those deadlines; serial, every probe owns the runner.
|
|
- 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-tcp-egress.electron.test.ts
|
|
src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
|
src/main/browser/browser-route-h3-egress.electron.test.ts
|
|
src/main/browser/browser-route-dns-prefetch.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
|
|
|
|
- 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
|
|
|
|
# Why here and not inside "Build package inputs": this job assembles packaging inputs step by
|
|
# step instead of calling build:release, and electron-builder's beforePack guard hard-fails
|
|
# without out/mobile-web. This is the real page, ~8 MB, not a bootstrap document.
|
|
- 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-
|
|
|
|
# Why persist-native-cache false: this job later rebuilds the same path for
|
|
# Electron. A post-job save would store the Electron ABI under the Node key.
|
|
- 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:
|
|
# Why the cargo directories ride along: a hit on .build skips the build
|
|
# entirely, but a miss otherwise recompiles the resource crate from a
|
|
# cold registry.
|
|
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') }}
|
|
|
|
# Why an explicit step rather than trusting the runner image: the packaged
|
|
# CLI launcher is a native Rust binary, and a host without cargo fails deep
|
|
# inside electron-builder's native hook instead of here.
|
|
- 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
|
|
|
|
# Why before the tests: the orca-agent-hook.exe suites then share one prebuilt binary rather
|
|
# than each starting its own build mid-run.
|
|
- 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
|
|
|
|
# Why its own step: the CPU burners that make taskkill slow would starve
|
|
# every other test file sharing the runner.
|
|
- 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
|
|
|
|
# Why the :parallel variant: identical to build:release except the three
|
|
# electron-vite targets overlap instead of running back to back. The Linux package
|
|
# job already packages and smoke-tests an AppImage built that way.
|
|
- 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"]' }}
|
|
|
|
# Why its own required job, apart from `e2e`: the rest of the suite is red on main, but the
|
|
# layout oracle is green there, and its known-on-main list absorbs today's findings. Only the two
|
|
# oracle specs run, through the same reusable workflow: the window spec in the changed-spec lane,
|
|
# the headless spec in the orcad lane on both 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
|
|
|
|
# Why this is not in verify's needs: it is the first PR-gate run of a harness whose reliability
|
|
# is only known from nightly main runs (20/20 green, 2026-08-09..2026-08-29, p50 3m25s). It
|
|
# reports a red X on the PR without blocking, exactly like `e2e` above. Deliberately no
|
|
# continue-on-error: that renders the check green and hides the signal it exists to give. To
|
|
# make it blocking, add it to verify.needs, add TERMINAL_IME_NATIVE to the env below, and
|
|
# require `success || skipped` outside the strict loop — see the note on `e2e`.
|
|
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 }}
|
|
|
|
# Why: pnpm's global virtual store links each package to only its declared dependencies, which
|
|
# is how developers install. Hoisting hides an undeclared one everywhere else, so install that way
|
|
# here with HOME and the store in an empty temp dir, where no stray node_modules can mask it.
|
|
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: blacksmith-4vcpu-ubuntu-2404
|
|
timeout-minutes: 30
|
|
env:
|
|
ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI: '1'
|
|
|
|
steps:
|
|
# Job-wide (via GITHUB_ENV, since job env cannot read runner.temp) so every pnpm command,
|
|
# including pnpm run's own install check, keeps this layout.
|
|
- 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
|
|
# Report settled results without competing with the GitHub-hosted test queue.
|
|
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
timeout-minutes: 5
|
|
|
|
steps:
|
|
# Why: e2e is deliberately absent from needs. The suite is currently red on
|
|
# main (every scheduled run), so gating merges on it would block any PR that
|
|
# touches tests/e2e/** — including the ones fixing the suite. Until it is
|
|
# green the job runs and reports for E2E-path PRs without blocking. To flip
|
|
# it on: add `e2e` to needs, add E2E to the env below, and require
|
|
# `"$E2E" = success || skipped` after the loop — skipped is the normal
|
|
# result for a path-filtered job and must keep passing, so it has to be
|
|
# checked outside the loop or it would excuse the jobs above.
|
|
- 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"
|