mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
* feat(files): show images and PDFs from the byte-range preview path, local and remote ImageViewer and PdfViewer read an orca-media:// URL instead of base64 when the editor opens a file: images load by URL, and pdf.js reads PDFs by range, so a large PDF no longer arrives whole through files.readPreview or pages into memory first. The grant mints only for the editor's viewers; diffs, git blobs, markdown images and the paired web client keep base64. - Minting an image checks its header with the base64 path's rule, so an image proven too large to decode safely never reaches the decoder. - orca-media gains fetch and CORS privileges, and range responses expose Accept-Ranges and Content-Range, so pdf.js can read by range. - Grants report the file size the viewer used to estimate from base64. * fix(files): read PDFs by real range, check served images, keep old hosts' previews - pdf.js fetches ranges only over http(s), so an orca-media URL was read whole; a small range transport now hands it bytes=a-b reads from the grant, and a failed range fails the preview instead of hanging it. - A file can change after mint, so the raster header is checked again on the bytes each image request serves (the same handle locally). - A paired host without files.readChunk falls back to files.readPreview for images and PDFs, as before; only audio and video need the update. * fix(files): mint no local document grant for a renderer gone mid-mint The local image/PDF mint is async now (authorize, open, header check), so a renderer that closed meanwhile could leave a grant no teardown drops. It binds the renderer lifetime first and refuses to mint once it's gone, as the remote mint does. * fix(files): keep the 50 MB cap for URL images, and probe remote headers incrementally An <img> decodes the whole file, so image grants keep the base64 path's 50 MB cap; PDFs, now read by range, don't need one. A remote header probe reads only each wider step's new bytes instead of re-reading from the start. A replaced image of a different size resets its zoom. * fix(files): tear down a PDF load whose range failed, and read 256 KiB ranges A failed range only showed the error and left the pdf.js task, its worker and pages alive; it now destroys that load first. pdf.js asks for one readChunk-sized range at a time instead of 64 KiB, and a Range preflight gets its CORS answer instead of 405. * fix(files): read PDF ranges over IPC so orca-media needs no fetch or CORS orca-media had gained supportFetchAPI and corsEnabled, and its responses sent Access-Control-Allow-Origin: *, so pdf.js could fetch ranges. That let any default-session frame holding a token read the bytes. pdf.js now reads a PDF grant's ranges through fs:readPreviewRange, which main answers only for the renderer that minted the token. The scheme is back to element-load privileges (standard, secure, stream), responses carry no CORS headers, and preflights get 405. Grant bookkeeping moves to preview-grant-registry.ts. An e2e spec proves an orca-media URL can't be fetched from the app page or read from another session. * ci(e2e): route the byte-range preview sources to the viewer specs that exercise them Changes to the orca-media grant path, the image/PDF viewers and the PDF range transport now run the PDF reload, rebuild and find specs, the remote large-file open, media playback and the partition-isolation spec. * fix(files): stop PDF range reads on abort, abort remote reads with the window, fix the CI route - A closed or reloaded viewer schedules no further pieces of a merged range, and a remote range read stops when its window goes away. - A URL without a size is an error rather than a fetch the scheme refuses. - The PR guard job's sparse checkout and its admission test include the new route module, and the route now also covers the mint handler, preload bridge, editor surfaces, scheme registration and read client. * fix(files): classify linked previews by their target, keep range reads off stopped WSL distros A local image or PDF reached through a link is classified by the file it resolves to, at mint and on every read, and a re-mint after the link is retargeted gets a fresh grant of the new type. A paired host's document whose first bytes are another kind defers to files.readPreview, which classifies the target there. Lazy local reads go through the non-waking WSL guard, as fs:readFile and fs:readFileChunk do.
1634 lines
74 KiB
YAML
1634 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
|
||
/.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'
|
||
# Leave headroom for typechecking and overlapping checks.
|
||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||
outputs:
|
||
shards: ${{ steps.unit-plan.outputs.shards }}
|
||
managed_hook_node18: ${{ steps.managed_hook_node18.outcome }}
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
# Fetch 50 for merge HEAD^1, no merge-base graph needed. Full-tree checkout omits blob:none to avoid refetching ~30k blobs (23s versus ~11s).
|
||
fetch-depth: 50
|
||
persist-credentials: false
|
||
|
||
# The dependency action below restores Node 24 before installing or building packages.
|
||
- name: Prepare Node 18 smoke runtime
|
||
id: node18_runtime
|
||
if: needs.code_paths.outputs.managed_hook_node18 == 'true'
|
||
continue-on-error: true
|
||
uses: actions/setup-node@v6
|
||
with:
|
||
node-version: '18'
|
||
package-manager-cache: false
|
||
|
||
- name: Capture Node 18 executable
|
||
id: node18_executable
|
||
if: needs.code_paths.outputs.managed_hook_node18 == 'true' && steps.node18_runtime.outcome == 'success'
|
||
continue-on-error: true
|
||
run: node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT"
|
||
|
||
# Choose one root/mobile install: root-only store is 321MB/4.6s versus mixed 537MB/8.6s, saving unused mobile downloads on most PRs.
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
if: needs.code_paths.outputs.mobile_dependencies != 'true'
|
||
with:
|
||
native-runtime: node
|
||
node-version: '24'
|
||
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
if: needs.code_paths.outputs.mobile_dependencies == 'true'
|
||
with:
|
||
native-runtime: node
|
||
node-version: '24'
|
||
cache-dependency-path: |
|
||
pnpm-lock.yaml
|
||
mobile/pnpm-lock.yaml
|
||
|
||
# Bun 1.4.3 introduces the compiler used by this job.
|
||
- name: Set up Bun for preflight checks
|
||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||
with:
|
||
bun-version-file: config/.bun-version
|
||
no-cache: true
|
||
|
||
# Plan first: its ~10s single-threaded import-graph walk overlaps lint instead of trailing the job.
|
||
- name: Plan unit selection
|
||
if: '!cancelled()'
|
||
id: unit-plan
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
ORCA_UNIT_SELECTION_MODE: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
|
||
ORCA_PULL_REQUEST_DRAFT: ${{ github.event.pull_request.draft }}
|
||
# Five shards use fewer runner minutes; the repository variable can request 2 to 10.
|
||
ORCA_UNIT_FULL_SHARD_COUNT: ${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node config/scripts/ci-unit-plan.mjs
|
||
|
||
# Run gate scripts directly to avoid repeated package-manager startup.
|
||
- name: Check process-host imports
|
||
if: '!cancelled()'
|
||
id: process-host-imports
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
bun config/scripts/check-import-boundaries.mjs
|
||
|
||
- name: Check main-process sync launches
|
||
if: '!cancelled()'
|
||
id: main-sync-spawn-ratchet
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
bun config/scripts/check-main-sync-spawn-ratchet.mjs
|
||
|
||
# Keep each check in its own log while sharing this runner.
|
||
- name: Lint
|
||
if: '!cancelled()'
|
||
id: root-lint
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node node_modules/oxlint/bin/oxlint --format github
|
||
|
||
# JS plugins make Oxlint hand every AST to Node, so they run apart from the native root lint.
|
||
- name: Enforce JS-plugin lint rules
|
||
if: '!cancelled()'
|
||
id: js-plugin-lint
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node node_modules/oxlint/bin/oxlint --config config/oxlint-js-plugins.json --format github src config tests mobile native .github
|
||
|
||
- name: Enforce ast-grep rules
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: |
|
||
node_modules/.bin/ast-grep test --config config/sgconfig.yml --skip-snapshot-tests
|
||
node config/scripts/ast-grep-gates.mjs --format=github
|
||
|
||
# Why the full tree: it takes ~2s, and the pre-commit hook is skippable.
|
||
- name: Check formatting
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node node_modules/oxfmt/bin/oxfmt --check .
|
||
|
||
- name: Reject low-evidence patterns
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: |
|
||
node config/scripts/sync-anti-slop-plugin.mjs
|
||
bun --smol node_modules/oxlint/bin/oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings
|
||
|
||
- wait: root-lint
|
||
|
||
- name: Enforce focused code-quality plugins
|
||
if: '!cancelled()'
|
||
id: native-code-quality
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node node_modules/oxlint/bin/oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings
|
||
|
||
- name: Enforce type-aware code-quality baseline
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node node_modules/oxlint/bin/oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings
|
||
|
||
# Mobile installation changes import resolution for the native cycle check.
|
||
- wait: native-code-quality
|
||
|
||
# Install mobile before type-aware changed-code lint or unresolved mobile types become phantom error-type findings.
|
||
- uses: ./.github/actions/install-mobile-dependencies
|
||
if: needs.code_paths.outputs.static_analysis == 'true' && needs.code_paths.outputs.mobile_dependencies == 'true'
|
||
|
||
- name: Enforce changed-code quality
|
||
if: '!cancelled()'
|
||
id: changed-code-quality
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node config/scripts/check-changed-code-quality.mjs "${{ github.event.pull_request.base.sha }}"
|
||
|
||
- name: Verify locked audit tools
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
env:
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
run: node config/scripts/verify-locked-audit-tools.mjs
|
||
|
||
- name: Enforce React Doctor on changed lines
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/check-react-doctor-changed.mjs "${{ github.event.pull_request.base.sha }}"
|
||
|
||
- wait: changed-code-quality
|
||
|
||
# The compiler overlaps the remaining checks after the type-aware passes release memory.
|
||
- name: Typecheck
|
||
if: '!cancelled()'
|
||
id: typecheck
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
pnpm run typecheck:ci --bun-concurrency=2 --bun-threads=2
|
||
|
||
# Starts with the compiler so its ~16s pass overlaps the serial checks below.
|
||
# Extract sorted evidence to isolated temp paths with one normalized platform-independent AST pass; use preflight’s pinned Bun.
|
||
- name: Verify localization extraction
|
||
if: '!cancelled()'
|
||
id: localization-extraction
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
# Detection failures run the full check; renames retain the removed input path.
|
||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" &&
|
||
scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then
|
||
echo "Localization extraction inputs are unchanged."
|
||
else
|
||
bun config/scripts/verify-localization-extraction.mjs
|
||
fi
|
||
|
||
- name: Check Zustand selector fan-out budget
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check
|
||
|
||
- name: Check reliability gate manifest
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/check-reliability-gates.mjs
|
||
|
||
- name: Enforce dead design-system classes
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node node_modules/oxlint/bin/oxlint --config config/oxlint-dead-classes.json src/renderer
|
||
|
||
- name: Check VM runtime rollback compatibility
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
env:
|
||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||
run: |
|
||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||
if git diff --quiet "$DIFF_BASE" HEAD -- \
|
||
src/shared/ephemeral-vm-runtime-store.ts \
|
||
src/shared/ephemeral-vm-runtime-feature-store.ts \
|
||
src/shared/ephemeral-vm-runtime-rollback-projection.ts \
|
||
src/shared/ephemeral-vm-runtimes.ts \
|
||
src/shared/ephemeral-vm-recipes.ts \
|
||
src/shared/orca-yaml-hook-types.ts \
|
||
config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \
|
||
config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts \
|
||
src/main/ephemeral-vm/runtime-service.ts \
|
||
src/main/ephemeral-vm/runtime-provisioning-persistence.ts \
|
||
src/main/ephemeral-vm/failed-start-cleanup.ts; then
|
||
echo "VM runtime persistence is unchanged."
|
||
exit 0
|
||
fi
|
||
node config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \
|
||
config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts
|
||
|
||
# Runs every config/scripts/check-*-ratchet(s).mjs, so a new ratchet needs no step here.
|
||
- name: Enforce ratchets
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/run-ratchets.mjs
|
||
|
||
- name: Check Node runtime pin
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/check-node-runtime-pin.mjs
|
||
|
||
# A ~7s single-threaded tree walk; it overlaps the remaining checks.
|
||
- name: Check every test file has a runner
|
||
if: '!cancelled()'
|
||
id: test-file-runners
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
node config/scripts/check-test-file-runners.mjs
|
||
|
||
- name: Check root and mobile lock the same shared dependencies
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/check-mobile-shared-dependency-versions.mjs
|
||
|
||
- name: Decide whether the orcad terminal smoke runs
|
||
id: orcad-smoke-scope
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
env:
|
||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||
run: |
|
||
# Detection failures run the smoke; renames retain the removed input path.
|
||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/orcad-smoke-changes" &&
|
||
scope="$(node config/scripts/orcad-terminal-smoke-change-scope.mjs "$RUNNER_TEMP/orcad-smoke-changes")" && [ "$scope" = false ]; then
|
||
echo "Orcad terminal smoke inputs are unchanged."
|
||
echo 'run=false' >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo 'run=true' >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# orcad refuses to build without the page phones load.
|
||
- uses: ./.github/actions/build-mobile-web-bundle
|
||
if: steps.orcad-smoke-scope.outputs.run == 'true'
|
||
|
||
# Pair import-graph electron exclusion with built-Node boot/pair/worktree/PTY smoke; graph purity alone does not prove runtime operation.
|
||
- name: Boot orcad and round-trip a terminal
|
||
if: steps.orcad-smoke-scope.outputs.run == 'true'
|
||
env:
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
run: pnpm run smoke:orcad-terminal
|
||
|
||
- name: Verify the generated RPC params catalog
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/generate-rpc-params-catalog.mjs --check
|
||
|
||
- name: Verify the generated ACP protocol schema
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/acp/generate-protocol.mjs --check
|
||
|
||
- name: Verify bundled skill guides
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/generate-bundled-skill-guides.mjs --check
|
||
|
||
- name: Verify skill freshness manifest
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/generate-skill-bundle-manifest.mjs
|
||
|
||
- name: Verify localization catalogs
|
||
if: '!cancelled()'
|
||
id: localization-catalogs
|
||
background: true
|
||
env:
|
||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||
run: |
|
||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||
bun config/scripts/verify-localization-catalogs.mjs
|
||
|
||
- name: Verify localization coverage
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: bun config/scripts/audit-localization-coverage.mjs --check
|
||
|
||
- wait: [localization-catalogs, localization-extraction]
|
||
|
||
# Keep project declarations in .ts: skipLibCheck silently widened unresolved .d.ts names to any, hiding #1186 IPC breakage. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts.
|
||
- name: Guard against project-owned .d.ts in preload/shared
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: |
|
||
matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true)
|
||
if [ -n "$matches" ]; then
|
||
echo "::error::Project-owned .d.ts files are not allowed under src/preload or src/shared."
|
||
echo "Move type declarations into a .ts file so skipLibCheck does not hide errors."
|
||
echo "See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts."
|
||
echo "Found:"
|
||
echo "$matches"
|
||
exit 1
|
||
fi
|
||
|
||
- name: Check feature wall asset budget
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/check-feature-wall-assets.mjs
|
||
|
||
- name: Verify macOS entitlements
|
||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||
run: node config/scripts/verify-macos-entitlements.mjs
|
||
|
||
- wait: typecheck
|
||
|
||
- wait: process-host-imports
|
||
|
||
- wait: main-sync-spawn-ratchet
|
||
|
||
- wait: js-plugin-lint
|
||
|
||
- wait: unit-plan
|
||
|
||
- wait: test-file-runners
|
||
|
||
- uses: actions/upload-artifact@v7
|
||
if: needs.code_paths.outputs.typecheck == 'true'
|
||
with:
|
||
name: unit-selection-attempt-${{ github.run_attempt }}
|
||
path: ci-shards/unit-selection.json
|
||
retention-days: 14
|
||
|
||
- name: Test Mobile CI tools and runtime routing
|
||
if: needs.code_paths.outputs.mobile_ci_tools == 'true'
|
||
env:
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
ORCA_BALANCE_UNIT_SHARDS: '1'
|
||
run: >-
|
||
node config/scripts/run-vitest.mjs run --config config/vitest.config.ts
|
||
--maxWorkers=4 --isolate --retry=0 --bail=0
|
||
config/scripts/mobile-release-check-scope.test.mjs
|
||
config/scripts/mobile-test-change-scope.test.mjs
|
||
config/scripts/pr-ready-check-reuse.test.mjs
|
||
config/scripts/ci-dependency-download-cache.test.mjs
|
||
config/scripts/win32-test-lane-registration.test.mjs
|
||
config/scripts/vitest-node-runtime-boundary.test.ts
|
||
config/scripts/vitest-sqlite-runtime-boundary.test.ts
|
||
|
||
# Report failure through the separate required check without blocking unrelated consumers.
|
||
- name: Smoke process-host package on Node 18
|
||
id: managed_hook_node18
|
||
if: success() && needs.code_paths.outputs.managed_hook_node18 == 'true'
|
||
continue-on-error: true
|
||
env:
|
||
NODE18_EXECUTABLE: ${{ steps.node18_executable.outputs.executable }}
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
run: |
|
||
test -n "$NODE18_EXECUTABLE"
|
||
"$NODE18_EXECUTABLE" config/scripts/smoke-process-host-node18.mjs
|
||
|
||
git_compatibility:
|
||
name: Git compatibility
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.git_compatibility == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Three parallel lanes (two through Docker) exceed real-Git test timeouts on 4-vCPU hosted runners.
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
|
||
- uses: ./.github/actions/prepare-git-compatibility
|
||
|
||
- name: Verify Git binary compatibility matrix
|
||
env:
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
run: |
|
||
specs=(
|
||
"alpine/git:edge-2.38.1|2.38.1"
|
||
"alpine/git:v2.49.1|2.49.1"
|
||
)
|
||
# Pull before tests so a sibling’s timed lane cannot be stalled by the first image download.
|
||
for spec in "${specs[@]}"; do
|
||
docker pull --quiet "${spec%%|*}"
|
||
done
|
||
|
||
pids=()
|
||
(
|
||
ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \
|
||
GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5" \
|
||
ORCA_GIT_COMPAT_VERSION="2.25.5" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/shared/git-binary-compatibility.test.ts \
|
||
src/main/git/worktree-safety-real-git.test.ts \
|
||
src/main/git/worktree-rebase-update-refs-real-git.test.ts
|
||
) &
|
||
pids+=("$!")
|
||
|
||
for spec in "${specs[@]}"; do
|
||
(
|
||
image="${spec%%|*}"
|
||
version="${spec#*|}"
|
||
ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/shared/git-binary-compatibility.test.ts \
|
||
src/main/git/worktree-safety-real-git.test.ts \
|
||
src/main/git/worktree-rebase-update-refs-real-git.test.ts
|
||
) &
|
||
pids+=("$!")
|
||
done
|
||
|
||
status=0
|
||
for pid in "${pids[@]}"; do
|
||
wait "$pid" || status=1
|
||
done
|
||
exit "$status"
|
||
|
||
# Run pinned real Codex: thread/read must repair an unindexed rollout’s threads row; stub-only units cannot detect upstream removal of this behavior.
|
||
codex_index_heal_contract:
|
||
name: Codex index-heal contract
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.codex_index_heal_contract == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Why ARM: @openai/codex ships @openai/codex-linux-arm64.
|
||
runs-on: ubuntu-24.04-arm
|
||
env:
|
||
CODEX_CLI_VERSION: '0.150.1'
|
||
# Why a second pin: --no-daemon only exists from 0.156, and Orca's codex wrapper relies on it.
|
||
CODEX_NO_DAEMON_CLI_VERSION: '0.158.0'
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
|
||
- name: Install pinned Codex CLI
|
||
run: |
|
||
set -euo pipefail
|
||
npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli" \
|
||
"@openai/codex@$CODEX_CLI_VERSION"
|
||
|
||
- name: Verify Codex index-heal contract
|
||
env:
|
||
# Require installed binary or skip would report green without exercising the contract.
|
||
ORCA_CODEX_CONTRACT_REQUIRED: '1'
|
||
ORCA_CODEX_CONTRACT_VERSION: ${{ env.CODEX_CLI_VERSION }}
|
||
run: |
|
||
set -euo pipefail
|
||
ORCA_CODEX_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli/node_modules/.bin/codex" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/main/codex/codex-index-heal-binary-contract.test.ts
|
||
|
||
- name: Install pinned no-daemon Codex CLI
|
||
run: |
|
||
set -euo pipefail
|
||
npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-no-daemon" \
|
||
"@openai/codex@$CODEX_NO_DAEMON_CLI_VERSION"
|
||
|
||
- name: Verify Codex --no-daemon contract
|
||
env:
|
||
ORCA_CODEX_NO_DAEMON_CONTRACT_REQUIRED: '1'
|
||
ORCA_CODEX_NO_DAEMON_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }}
|
||
run: |
|
||
set -euo pipefail
|
||
ORCA_CODEX_NO_DAEMON_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/main/pty/codex-no-daemon-binary-contract.test.ts
|
||
|
||
# Test pins plus latest because Codex’s managed-home approval hash can change before users encounter review prompts.
|
||
- name: Verify Codex hook file-entry contract (pinned)
|
||
env:
|
||
ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1'
|
||
run: |
|
||
set -euo pipefail
|
||
for install in codex-cli:"$CODEX_CLI_VERSION" codex-cli-no-daemon:"$CODEX_NO_DAEMON_CLI_VERSION"; do
|
||
ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/${install%%:*}/node_modules/.bin/codex" \
|
||
ORCA_CODEX_HOOK_CONTRACT_VERSION="${install#*:}" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/main/codex/codex-hook-file-entry-binary-contract.test.ts
|
||
done
|
||
|
||
- name: Install latest Codex CLI
|
||
run: |
|
||
set -euo pipefail
|
||
npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-latest" "@openai/codex@latest"
|
||
|
||
- name: Verify Codex hook file-entry contract (latest)
|
||
env:
|
||
ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1'
|
||
run: |
|
||
set -euo pipefail
|
||
ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-latest/node_modules/.bin/codex" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/main/codex/codex-hook-file-entry-binary-contract.test.ts
|
||
|
||
# Why the no-daemon install: it is the newest pinned Codex, and its project
|
||
# lookup decides whether a worktree launch stops at the trust prompt.
|
||
- name: Verify Codex project-trust contract
|
||
env:
|
||
ORCA_CODEX_TRUST_CONTRACT_REQUIRED: '1'
|
||
ORCA_CODEX_TRUST_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }}
|
||
run: |
|
||
set -euo pipefail
|
||
ORCA_CODEX_TRUST_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \
|
||
pnpm exec vitest run --config config/vitest.config.ts \
|
||
src/main/agent-trust-presets.test.ts
|
||
|
||
xterm_patch_sync:
|
||
name: xterm patch sync
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.xterm_patch_sync == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Use ARM for four-package pristine/patched rebuild byte comparison: output generated on Linux x64 reproduces on darwin-arm64.
|
||
runs-on: ubuntu-24.04-arm
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
# The generator uses Node built-ins and installs its own upstream toolchain.
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: package.json
|
||
check-latest: true
|
||
package-manager-cache: false
|
||
|
||
# Rebuild both pristine and patched bundles; reuse only the pinned toolchain.
|
||
- uses: ./.github/actions/prepare-xterm-dependencies
|
||
|
||
- name: Verify xterm patches match the pinned upstream build
|
||
env:
|
||
WORK_DIR: ${{ runner.temp }}/xterm-patch-build
|
||
run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR"
|
||
|
||
shell_contracts:
|
||
name: shell contracts
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.shell_contracts == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Exercise Linux ARM with the standalone aarch64 release and Ubuntu's zsh.
|
||
runs-on: ubuntu-24.04-arm
|
||
# Bound stalled prerequisite downloads instead of waiting for GitHub's 6-hour default.
|
||
timeout-minutes: 15
|
||
env:
|
||
# Require live fish instead of developer-friendly skips: this is the only full #9993 guard.
|
||
ORCA_REQUIRE_FISH: '1'
|
||
# Why: the runner image ships pwsh, and the codex wrapper's PowerShell lane must not skip.
|
||
ORCA_REQUIRE_PWSH: '1'
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
# fish 4+ provides DECSET 2031 arming required by live shell contracts (#9993).
|
||
- name: Install zsh and fish
|
||
id: shells
|
||
background: true
|
||
run: |
|
||
# Per-connection limits complement the whole-command apt deadlines below.
|
||
sudo tee /etc/apt/apt.conf.d/99-orca-shell-contracts >/dev/null <<'APTCONF'
|
||
Acquire::http::Timeout "15";
|
||
Acquire::https::Timeout "15";
|
||
Acquire::Retries "1";
|
||
APTCONF
|
||
# Unrelated runner repositories may fail refresh; installing zsh must still succeed.
|
||
timeout 120 sudo apt-get update || true
|
||
timeout 300 sudo apt-get install -y zsh
|
||
# A fixed 4.x release meets the contract without tracking package-repository updates.
|
||
case "$(uname -m)" in
|
||
aarch64) arch=aarch64 sha=57349f0111728c64b703a84e611aa97ff4d1fa8466385d64e67bd5bc3ffa49fc ;;
|
||
*) arch=x86_64 sha=8f643d10ad1abb0ef7072764c01ceb5cf61b2d373fda400cf60f0bad69b7e095 ;;
|
||
esac
|
||
curl -fsSL --retry 2 --connect-timeout 15 --max-time 120 -o "$RUNNER_TEMP/fish.tar.xz" \
|
||
"https://github.com/fish-shell/fish-shell/releases/download/4.9.3/fish-4.9.3-linux-${arch}.tar.xz"
|
||
echo "${sha} $RUNNER_TEMP/fish.tar.xz" | sha256sum -c -
|
||
tar -xJf "$RUNNER_TEMP/fish.tar.xz" -C "$RUNNER_TEMP" fish
|
||
sudo install -m 0755 "$RUNNER_TEMP/fish" /usr/local/bin/fish
|
||
for package in fish fish-common; do
|
||
if ! dpkg-query -W -f='${binary:Package}: ${Status}\n' "$package" 2>/dev/null; then
|
||
echo "$package: absent"
|
||
fi
|
||
done
|
||
# Exclude runner preload while keeping the tests' own XDG vendor snippets reachable.
|
||
quarantine="$(mktemp -d "$RUNNER_TEMP/fish-runner-resources.XXXXXX")"
|
||
for path in /usr/share/fish /etc/fish; do
|
||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||
sudo mv -- "$path" "$quarantine/${path//\//_}"
|
||
echo "$path: quarantined in $quarantine"
|
||
fi
|
||
if [ -e "$path" ]; then echo "$path: present"; else echo "$path: absent"; fi
|
||
done
|
||
printf 'fish selected: %s\n' "$(command -v fish)"
|
||
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
with:
|
||
native-runtime: node
|
||
|
||
- wait: shells
|
||
|
||
# Fail fish prerequisites separately before dependency install; ORCA_REQUIRE_FISH checks again in tests.
|
||
- name: Require fish 4+
|
||
run: |
|
||
version="$(fish --version 2>/dev/null || true)"
|
||
major="${version##*version }"
|
||
major="${major%%.*}"
|
||
case "$major" in '' | *[!0-9]*) major=0 ;; esac
|
||
echo "${version:-<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
|
||
# 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.preflight.outputs.shards }}
|
||
run_shards: ${{ needs.code_paths.outputs.mobile_ci_tools != 'true' }}
|
||
run_relay_integration: ${{ needs.code_paths.outputs.relay_integration != 'false' || needs.code_paths.outputs.orcad_browser == 'true' }}
|
||
browser_contract: ${{ needs.code_paths.outputs.orcad_browser == 'true' }}
|
||
|
||
# Keep advisory evidence sibling to tests and outside verify.needs; reusable-workflow dependency otherwise added ~36s after last shard.
|
||
unit_selection_evidence:
|
||
needs: [test]
|
||
if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }}
|
||
uses: ./.github/workflows/unit-selection-evidence.yml
|
||
|
||
# Isolate two browser/mobile files to avoid every unit shard paying for their installs; test the same shipped bundle plus render/census.
|
||
mobile_web_app:
|
||
name: mobile web app bundle
|
||
needs: [code_paths]
|
||
if: needs.code_paths.outputs.mobile_web_app == 'true'
|
||
runs-on: ubuntu-24.04
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
# Why no native-runtime: the builder is esbuild and the render check is a browser. Nothing
|
||
# in this job loads node-pty.
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
with:
|
||
native-runtime: none
|
||
cache-dependency-path: |
|
||
pnpm-lock.yaml
|
||
mobile/pnpm-lock.yaml
|
||
|
||
# Test drawer with WebKit as iOS WebView uses it; runner Chrome cannot substitute.
|
||
- name: Install WebKit for the drawer check
|
||
id: webkit
|
||
background: true
|
||
run: |
|
||
bash .github/scripts/prepare-ubuntu-apt.sh
|
||
pnpm exec playwright-core install --with-deps webkit
|
||
|
||
# The entry lives in mobile/ so one React resolves; without this every RN import is nothing.
|
||
- uses: ./.github/actions/install-mobile-dependencies
|
||
|
||
- name: Prepare mobile route snapshot
|
||
id: mobile-routes
|
||
background: true
|
||
run: node config/scripts/run-mobile-web-app-checks.mjs --prepare-route-snapshot "$RUNNER_TEMP/mobile-routes.json"
|
||
|
||
# Require runner Google Chrome: Ubuntu’s AppArmor userns profile covers its .deb, unlike downloaded Chromium; missing render checks must fail.
|
||
- name: Resolve Chrome for the render check
|
||
run: |
|
||
set -euo pipefail
|
||
chrome="$(command -v google-chrome || command -v google-chrome-stable || true)"
|
||
if [ -z "$chrome" ]; then
|
||
echo "::error::No Google Chrome on the runner; the render check would silently skip."
|
||
exit 1
|
||
fi
|
||
"$chrome" --version
|
||
echo "ORCA_MOBILE_WEB_RENDER_BROWSER=$chrome" >> "$GITHUB_ENV"
|
||
|
||
- name: Build and verify the app bundle
|
||
run: pnpm run build:mobile-web
|
||
|
||
# Browser checks must observe both a finished install and the built bundle.
|
||
- wait: [webkit, mobile-routes]
|
||
|
||
# Require mobile install and select census family by quoted filename prefix, matching routing; stale file lists/env-gated skips previously hid missing coverage.
|
||
- name: Builder, override census and render checks
|
||
env:
|
||
ORCA_MOBILE_WEB_APP_DEPS_REQUIRED: '1'
|
||
ORCA_MOBILE_WEB_PREPARED_ROUTE_SNAPSHOT: ${{ runner.temp }}/mobile-routes.json
|
||
run: |
|
||
node config/scripts/run-mobile-web-app-checks.mjs
|
||
|
||
cross-version-wire:
|
||
name: cross-version wire compatibility
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.cross-version-wire == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Why ARM: source-only: tagged checkout plus in-process vitest, no docker or browser.
|
||
runs-on: ubuntu-24.04-arm
|
||
|
||
steps:
|
||
# Fetch tags/full history for newest-release skew; shallow unit shards cannot supply it.
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
# Use blob:none for merge-base history because blobs are ~89% of pack; fetch rare historical contents on demand.
|
||
fetch-depth: 0
|
||
filter: blob:none
|
||
persist-credentials: false
|
||
|
||
- uses: ./.github/actions/install-node-dependencies
|
||
with:
|
||
native-runtime: node
|
||
|
||
# R1: an upgrade from the newest release must adopt its live terminal daemon.
|
||
- name: Daemon protocol crossing against newest release
|
||
run: pnpm run check:daemon-protocol-crossing
|
||
|
||
# R3: a runtime launcher swap must not also bump the daemon protocol (D7.1).
|
||
# The label is read from the event payload, so push again after applying it.
|
||
- name: Runtime launcher change does not bump daemon protocol
|
||
env:
|
||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||
ORCA_ALLOW_RUNTIME_LAUNCHER_PROTOCOL_BUMP: ${{ contains(github.event.pull_request.labels.*.name, 'allow-runtime-launcher-protocol-bump') }}
|
||
run: |
|
||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||
pnpm run check:runtime-launcher-protocol-ratchet -- --base "$DIFF_BASE"
|
||
|
||
# Run the entire directory; zero matches fail so new contracts cannot be omitted silently.
|
||
- name: Old/new client and server compatibility journeys
|
||
run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/
|
||
|
||
managed_hook_node18:
|
||
name: managed hooks on Node 18
|
||
needs: [code_paths, preflight]
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.code_paths.result == 'success' &&
|
||
needs.code_paths.outputs.managed_hook_node18 == 'true' &&
|
||
needs.preflight.result == 'success'
|
||
# Retain the required check while sharing preflight's checkout and built packages.
|
||
runs-on: ubuntu-slim
|
||
timeout-minutes: 5
|
||
|
||
steps:
|
||
- name: Require process-host package smoke on Node 18
|
||
env:
|
||
MANAGED_HOOK_NODE18: ${{ needs.preflight.outputs.managed_hook_node18 }}
|
||
run: |
|
||
if [ "$MANAGED_HOOK_NODE18" != success ]; then
|
||
echo "::error::Node 18 process-host smoke: expected success, got $MANAGED_HOOK_NODE18"
|
||
exit 1
|
||
fi
|
||
|
||
package:
|
||
name: package
|
||
needs: [code_paths, preflight]
|
||
if: needs.code_paths.outputs.package == 'true'
|
||
runs-on: ubuntu-24.04
|
||
# Let the serial Docker gates reach their own deadlines and report cleanup failures.
|
||
timeout-minutes: 90
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
# 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/popup-origin-chrome.electron.test.ts
|
||
src/main/browser/browser-link-materialization.electron.test.ts
|
||
src/main/browser/browser-popup-compatibility.electron.test.ts
|
||
src/main/browser/browser-popup-adapter-compatibility.electron.test.ts
|
||
src/main/browser/browser-popup-native-compatibility.electron.test.ts
|
||
src/main/browser/browser-popup-background-compatibility.electron.test.ts
|
||
src/main/browser/extensions/browser-extension-api.electron.test.ts
|
||
src/main/browser/extensions/browser-extension-persistence.electron.test.ts
|
||
src/main/browser/extensions/browser-extension-lifecycle.electron.test.ts
|
||
src/main/browser/extensions/browser-extension-toolbar.electron.test.ts
|
||
src/main/browser/extensions/browser-extension-native-messaging.electron.test.ts
|
||
|
||
- name: Restore native components
|
||
id: linux-native-restore
|
||
uses: ./.github/actions/prepare-wayland-clipboard
|
||
with:
|
||
build: 'false'
|
||
|
||
# The isolated glibc-floor build writes only the Wayland helper, so JavaScript can overlap.
|
||
- name: Build native components
|
||
id: linux-native
|
||
background: true
|
||
env:
|
||
CACHE_HIT: ${{ steps.linux-native-restore.outputs.cache-hit }}
|
||
run: bash .github/actions/prepare-wayland-clipboard/build-or-validate.sh
|
||
|
||
# Load independent fixtures during builds; keep native lifecycle probes serial.
|
||
- uses: ./.github/actions/prepare-linux-package-fixture
|
||
id: shutdown-fixture-cache-restore
|
||
with:
|
||
fixture: headless-serve-shutdown
|
||
load: 'false'
|
||
|
||
- name: Load headless serve shutdown fixture image
|
||
id: shutdown-fixture-cache
|
||
background: true
|
||
env:
|
||
FIXTURE: headless-serve-shutdown
|
||
CACHE_HIT: ${{ steps.shutdown-fixture-cache-restore.outputs.cache-hit }}
|
||
run: bash .github/actions/prepare-linux-package-fixture/load-fixture.sh
|
||
|
||
- uses: ./.github/actions/prepare-linux-package-fixture
|
||
id: cli-fixture-cache-restore
|
||
with:
|
||
fixture: cli-launch-contract
|
||
load: 'false'
|
||
|
||
- name: Load CLI launch contract fixture image
|
||
id: cli-fixture-cache
|
||
background: true
|
||
env:
|
||
FIXTURE: cli-launch-contract
|
||
CACHE_HIT: ${{ steps.cli-fixture-cache-restore.outputs.cache-hit }}
|
||
run: bash .github/actions/prepare-linux-package-fixture/load-fixture.sh
|
||
|
||
- name: Install Linux package tooling
|
||
id: linux-package-tools
|
||
background: true
|
||
run: |
|
||
# Use the shell lane's bounded refresh; required tooling installation stays fatal.
|
||
sudo tee /etc/apt/apt.conf.d/99-orca-package-tools >/dev/null <<'APTCONF'
|
||
Acquire::http::Timeout "15";
|
||
Acquire::https::Timeout "15";
|
||
Acquire::Retries "1";
|
||
APTCONF
|
||
timeout 120 sudo apt-get update || true
|
||
timeout 300 sudo apt-get install -y cpio rpm
|
||
|
||
- name: Build package inputs
|
||
env:
|
||
ORCA_CI_MAIN_SOURCEMAPS: '0'
|
||
run: |
|
||
status=0
|
||
pnpm run build:cli || status=1
|
||
|
||
scripts=(build:wsl-guest build:electron-vite:parallel)
|
||
pids=()
|
||
for script in "${scripts[@]}"; do
|
||
pnpm run "$script" &
|
||
pids+=("$!")
|
||
done
|
||
|
||
for pid in "${pids[@]}"; do
|
||
wait "$pid" || status=1
|
||
done
|
||
exit "$status"
|
||
|
||
- name: Project web client from renderer build
|
||
id: web-client
|
||
background: true
|
||
run: pnpm run build:web-from-renderer
|
||
|
||
# Build the real ~8MB mobile page because stepwise package assembly bypasses build:release and beforePack refuses its absence.
|
||
- name: Build mobile web bundle
|
||
run: pnpm run build:mobile-web
|
||
|
||
- wait: [linux-native, linux-package-tools, web-client]
|
||
|
||
- name: Package unpacked app
|
||
env:
|
||
ORCA_BACKGROUND_LAUNCH: '1'
|
||
ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1'
|
||
# Prepare once with every hook, then isolate the format-specific mutations.
|
||
run: |
|
||
pnpm exec electron-builder --config config/electron-builder.config.cjs --linux dir --x64 --publish never
|
||
node config/scripts/package-linux-formats.mjs
|
||
|
||
- name: Verify root-package marker payloads
|
||
run: |
|
||
set -euo pipefail
|
||
version="$(node -p "require('./package.json').version")"
|
||
deb="dist/orca-ide_${version}_amd64.deb"
|
||
rpm="dist/orca-ide-${version}.x86_64.rpm"
|
||
test -s "$deb"
|
||
test -s "$rpm"
|
||
deb_marker="$(dpkg-deb --fsys-tarfile "$deb" | tar -xOf - ./opt/Orca/resources/package-type)"
|
||
rpm_marker="$(rpm2cpio "$rpm" | cpio --quiet --extract --to-stdout ./opt/Orca/resources/package-type)"
|
||
[[ "$deb_marker" == deb ]] || { echo "Expected deb marker, got: $deb_marker"; exit 1; }
|
||
[[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; }
|
||
# Only the AppImage carries the pinned Node (config/scripts/appimage-node-runtime.cjs).
|
||
if dpkg-deb --fsys-tarfile "$deb" | tar -t | grep '/resources/node-runtime' >/dev/null; then
|
||
echo "The deb must not carry the AppImage's Node"; exit 1
|
||
fi
|
||
if rpm2cpio "$rpm" | cpio --quiet -t | grep '/resources/node-runtime' >/dev/null; then
|
||
echo "The rpm must not carry the AppImage's Node"; exit 1
|
||
fi
|
||
appimage_root="$(mktemp -d)"
|
||
(cd "$appimage_root" && "$GITHUB_WORKSPACE/dist/orca-linux.AppImage" --appimage-extract 'resources/node-runtime/bin/node' >/dev/null)
|
||
test -x "$appimage_root/squashfs-root/resources/node-runtime/bin/node"
|
||
"$appimage_root/squashfs-root/resources/node-runtime/bin/node" --version
|
||
rm -rf "$appimage_root"
|
||
ls -l dist/orca-linux.AppImage "$deb" "$rpm"
|
||
|
||
# Kept briefly so a reviewer can run this exact AppImage on a real Linux desktop.
|
||
- uses: actions/upload-artifact@v7
|
||
with:
|
||
name: orca-linux-appimage-${{ github.event.pull_request.head.sha }}
|
||
path: dist/orca-linux.AppImage
|
||
retention-days: 3
|
||
compression-level: 0
|
||
|
||
- wait: shutdown-fixture-cache
|
||
|
||
- name: Verify headless serve signal shutdown
|
||
env:
|
||
ORCA_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.shutdown-fixture-cache.outputs.image }}
|
||
run: >-
|
||
node config/scripts/run-headless-serve-shutdown-docker.mjs
|
||
--appimage dist/orca-linux.AppImage --all-entrypoints
|
||
|
||
# A default container reproduces the hostile AppImage launch environment.
|
||
- wait: cli-fixture-cache
|
||
|
||
- name: Verify Linux CLI launch contract
|
||
env:
|
||
ORCA_CLI_FIXTURE_CACHE_IMAGE: ${{ steps.cli-fixture-cache.outputs.image }}
|
||
run: node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage
|
||
|
||
- name: Smoke packaged CLI
|
||
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked
|
||
|
||
- name: Smoke packaged hang watchdog worker
|
||
run: xvfb-run --auto-servernum node config/scripts/smoke-packaged-hang-watchdog-worker.mjs --app-dir=dist/linux-unpacked
|
||
|
||
package_windows:
|
||
name: package (windows)
|
||
needs: [code_paths, preflight]
|
||
if: needs.code_paths.outputs.package_windows == 'true'
|
||
runs-on: windows-2022
|
||
timeout-minutes: 30
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
persist-credentials: false
|
||
|
||
# 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, 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"]'
|
||
# 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
|
||
fast_runners: true
|
||
|
||
# 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"
|