name: E2E run-name: E2E ${{ inputs.ref || github.ref }} # Why: checkout + artifact upload only; callers can only further restrict. permissions: contents: read on: workflow_call: inputs: ref: description: Ref to check out (defaults to the calling workflow's ref) required: false type: string test_files: description: JSON array of changed specs; empty runs the full suite required: false type: string run_changed_e2e: description: PR detector found specs requiring the general changed-spec lane. required: false type: boolean default: true needs_build: description: PR detector found an Electron consumer requiring build and native cache. required: false type: boolean default: true # Why: artifact names are unique per workflow run, and pr.yml calls this workflow twice. artifact_suffix: description: Appended to the build and trace artifact names so two calls in one run do not collide required: false type: string default: '' workflow_dispatch: inputs: ref: description: Ref to check out (defaults to the workflow ref) required: false type: string test_files: description: JSON array of specs to run; empty runs the full suite required: false type: string schedule: # One complete daily reference run; targeted PR coverage remains unchanged. - cron: '0 17 * * *' jobs: build: name: build e2e app if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build runs-on: ubuntu-latest timeout-minutes: 15 env: ORCA_BACKGROUND_LAUNCH: '1' steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Install native build tools env: ORCA_E2E_APT_PACKAGES: build-essential python3 run: &install_e2e_tools | if [ "$ORCA_E2E_APT_PACKAGES" = 'build-essential python3' ] && command -v make >/dev/null && command -v g++ >/dev/null && command -v python3 >/dev/null; then exit 0 fi read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES" # Why apt-mirrors.txt: runner images resolve mirror+file: sources through it, so editing the lists alone leaves Azure in use. for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do if [ -f "$source" ]; then sudo sed -i 's|https*://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source" fi done sudo tee /etc/apt/apt.conf.d/99-orca-e2e >/dev/null <<'APTCONF' Acquire::http::Timeout "15"; Acquire::https::Timeout "15"; Acquire::Retries "1"; APTCONF timeout 120 sudo apt-get update timeout 300 sudo apt-get install -y "${packages[@]}" || timeout 300 sudo apt-get install -y --fix-missing "${packages[@]}" # Why: the build's plain-Node daemon smoke load resolves node-pty. - uses: ./.github/actions/install-node-dependencies id: deps with: native-runtime: node persist-native-cache: 'false' cache-electron-package: 'true' # Save Node's ABI before the Electron preparation replaces the same build paths. - name: Save compiled Node native modules if: steps.deps.outputs.native-cache-key != '' && 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 }} # Why: building here avoids parallel builds inside Playwright globalSetup; # paired-browser specs also need the standalone web bundle. - name: Build E2E outputs env: VITE_EXPOSE_STORE: 'true' run: pnpm run build:electron-vite:parallel --mode e2e # The CLI emits into out/main too; start only after Electron finishes clearing that tree. - name: Build shared E2E CLI id: e2e-cli background: true run: | if node -e 'process.exit(require("./package.json").scripts["prepare:cli-output"] ? 0 : 1)'; then pnpm run build:cli else echo "Older ref: let each consumer build and install its CLI." fi - name: Project shared E2E web client run: pnpm run build:web-from-renderer - wait: e2e-cli - name: Check shared native runtime action id: native-runtime-action run: | if [ -f .github/actions/prepare-native-runtime/action.yml ]; then echo 'available=true' >> "$GITHUB_OUTPUT" else echo 'available=false' >> "$GITHUB_OUTPUT" fi # Only the completed output upload overlaps the change from Node to Electron ABI. - name: Upload E2E build output id: e2e-build-upload background: true uses: actions/upload-artifact@v7 with: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ include-hidden-files: true retention-days: 1 if-no-files-found: error - name: Prepare shared Electron native runtime id: electron-native-runtime if: steps.native-runtime-action.outputs.available == 'true' && steps.deps.outputs.node-version != '' uses: ./.github/actions/prepare-native-runtime with: native-runtime: electron node-version: ${{ steps.deps.outputs.node-version }} - name: Prepare Electron natives for older refs if: steps.native-runtime-action.outputs.available != 'true' || steps.deps.outputs.node-version == '' uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - wait: e2e-build-upload e2e: name: e2e ${{ matrix.shard_name }} needs: [build] if: inputs.test_files == '' runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: include: # Fourteen scheduled runs averaged 24.6 minutes per shard; shards 4 # and 9 repeatedly hit the 30-minute cap. A 12-way trial still left # one 30-minute shard, so 14 gives the suite enough failure headroom. - shard: '1/14' shard_name: 1-of-14 - shard: '2/14' shard_name: 2-of-14 - shard: '3/14' shard_name: 3-of-14 - shard: '4/14' shard_name: 4-of-14 - shard: '5/14' shard_name: 5-of-14 - shard: '6/14' shard_name: 6-of-14 - shard: '7/14' shard_name: 7-of-14 - shard: '8/14' shard_name: 8-of-14 - shard: '9/14' shard_name: 9-of-14 - shard: '10/14' shard_name: 10-of-14 - shard: '11/14' shard_name: 11-of-14 - shard: '12/14' shard_name: 12-of-14 - shard: '13/14' shard_name: 13-of-14 - shard: '14/14' shard_name: 14-of-14 steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} # Native cache misses need the compiler, Electron needs Xvfb, and paired # Quick Open needs ripgrep. Install them in one apt transaction per shard. - name: Install native build and headless UI tools # The Azure archive took 16 minutes for one font package; bound setup # separately so a slow mirror cannot consume the shard's test budget. env: &e2e_tool_packages ORCA_E2E_APT_PACKAGES: build-essential fonts-noto-cjk openssh-client python3 ripgrep xvfb zsh openbox x11-utils xdotool run: *install_e2e_tools - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - name: Download E2E build output uses: actions/download-artifact@v8 with: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ # Why: the Electron suite is wall-clock constrained on OSS runners, but # multiple Electron apps on one Xvfb VM contend on git/Chromium resources. # Sharding keeps each VM at one Playwright worker while splitting the # headless suite across separate runners. # SKIP_BUILD makes Playwright globalSetup reuse the single build job's # artifact instead of starting five concurrent electron-vite builds. # ORCA_E2E_FORWARD_APP_LOGS keeps startup failures visible when Electron # launches but never creates a BrowserWindow. - name: Balance E2E shard from timing evidence env: ORCA_BACKGROUND_LAUNCH: '1' SKIP_BUILD: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' ORCA_E2E_WEB_CLIENT: '1' run: | mkdir -p ci-shards pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --list --reporter=json > ci-shards/discovery.json export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)" node config/scripts/ci-e2e-shard-plan.mjs ci-shards/discovery.json '${{ matrix.shard }}' ci-shards pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --test-list=ci-shards/selected.txt --list --reporter=json > ci-shards/selected-discovery.json node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards/assignment.json ci-shards/selected-discovery.json - name: Run E2E tests (${{ matrix.shard_name }}) env: PLAYWRIGHT_JSON_OUTPUT_FILE: ci-shards/results.json run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 pnpm run test:e2e --test-list=ci-shards/selected.txt --reporter=list,json - name: Summarize E2E failures if: always() continue-on-error: true run: node config/scripts/ci-e2e-failure-summary.mjs ci-shards/results.json - name: Upload E2E shard assignment if: always() # Diagnostic upload outages must not change the test verdict. continue-on-error: true uses: actions/upload-artifact@v7 with: name: e2e-shard-${{ matrix.shard_name }}-attempt-${{ github.run_attempt }} path: ci-shards/ retention-days: 14 if-no-files-found: warn # The frame benchmark needs a mapped window, which the headless shards exclude. - name: Run worktree first-paint benchmark if: matrix.shard == '1/14' run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/worktree-switch-first-paint.spec.ts --config tests/playwright.config.ts --project=electron-headful --workers=1 # Why: Playwright retains traces/screenshots only on failure. Uploading # them as an artifact makes post-mortem debugging on CI possible without # re-running locally. - name: Upload Playwright traces if: failure() || cancelled() uses: actions/upload-artifact@v7 with: name: playwright-traces-${{ matrix.shard_name }} path: test-results/ retention-days: 7 if-no-files-found: ignore changed-e2e: name: changed e2e specs needs: [build] if: inputs.test_files != '' && (github.event_name != 'pull_request' || inputs.run_changed_e2e) runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Install native build and headless UI tools # Why ripgrep: Quick Open's bounded host-side search requires rg instead of an # unbounded inventory fallback; the paired fixture exercises that real boundary. env: *e2e_tool_packages run: *install_e2e_tools - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - name: Download E2E build output uses: actions/download-artifact@v8 with: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ - name: Run changed E2E specs env: TEST_FILES_JSON: ${{ inputs.test_files }} run: | if [ -f config/scripts/ci-e2e-job-selection.mjs ]; then printf '%s\n' "$TEST_FILES_JSON" | node config/scripts/ci-e2e-job-selection.mjs > "$RUNNER_TEMP/general-e2e-specs" else # Dispatching an older ref retains its existing dedicated-owner exclusions. jq -r '.[] | select( . != "tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts" and . != "tests/e2e/terminal-ibus-hangul-native.spec.ts" and . != "tests/e2e/orcad-serve-mode-switch.spec.ts" and . != "tests/e2e/workspace-layout-oracle-headless.spec.ts" and . != "tests/e2e/windows-missing-appdata-startup.spec.ts" and . != "tests/e2e/file-explorer-copy-file-windows.spec.ts" and (startswith("tests/e2e/ssh-orcad-") | not) )' <<<"$TEST_FILES_JSON" > "$RUNNER_TEMP/general-e2e-specs" fi mapfile -t TEST_FILES < "$RUNNER_TEMP/general-e2e-specs" if [ "${#TEST_FILES[@]}" -eq 0 ]; then echo "Changed specs are all owned by dedicated lanes." exit 0 fi E2E_ENV=(SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1) E2E_PROJECT_ARGS=() if grep -l '@headful' "${TEST_FILES[@]}" >/dev/null; then E2E_PROJECT_ARGS+=(--project=electron-headful) fi xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env "${E2E_ENV[@]}" \ pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}" - name: Upload Playwright traces if: failure() || cancelled() uses: actions/upload-artifact@v7 with: name: playwright-traces-changed${{ inputs.artifact_suffix }} path: test-results/ retention-days: 7 if-no-files-found: ignore ssh-browser-network-route: name: ssh browser network route if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts') runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - uses: ./.github/actions/install-node-dependencies with: native-runtime: node - name: Install SSH client run: sudo apt-get update && sudo apt-get install -y openssh-client - name: Run Docker SSH browser network route journeys env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_RUN_DOCKER_SSH_BROWSER_E2E: '1' run: node_modules/.bin/vitest run --config config/vitest.config.ts tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts orcad-serve-mode-switch: name: orca serve Electron/orcad mode switch (D7) needs: [build] if: >- inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') || contains(inputs.test_files, 'tests/e2e/workspace-layout-oracle-headless.spec.ts') runs-on: ubuntu-latest timeout-minutes: 45 env: ORCA_BACKGROUND_LAUNCH: '1' steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} persist-credentials: false - name: Install headless tools run: sudo apt-get update && sudo apt-get install -y build-essential ripgrep xvfb openbox x11-utils - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - uses: actions/download-artifact@v8 with: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ # The packaged orcad slot the T6-11 launcher runs: its own node-pty under the pinned Node. # The template is what `orca serve`'s default selection materializes that slot from. - name: Build this runner's orcad slot and template run: | slot="$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" pnpm build:orcad-prebuilds pnpm build:orcad-prebuilds --require-slots "$slot" pnpm build:orcad node config/scripts/build-orcad-template.mjs --targets "$slot" - name: Switch serve hosts on one profile if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') env: SKIP_BUILD: '1' ORCA_E2E_ORCAD_SERVE: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/orcad-serve-mode-switch.spec.ts --project=electron-headless --workers=1 # The layout oracle's headless scenarios run on both serve hosts, so they need this job's orcad. - name: Layout oracle on headless serve (orcad and Electron) if: >- !cancelled() && (inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/workspace-layout-oracle-headless.spec.ts')) env: SKIP_BUILD: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/workspace-layout-oracle-headless.spec.ts --project=electron-headless --workers=1 - uses: actions/upload-artifact@v7 if: failure() with: name: orcad-serve-mode-switch-traces${{ inputs.artifact_suffix }} path: test-results/ retention-days: 7 if-no-files-found: ignore # Every managed-orcad SSH spec on a Docker host, including #24979's relay-era conversion. Needs the # orcad template for the fixture's target (Debian, linux-x64-glibc), which only this job builds. orcad-ssh-docker: name: ssh on managed orcad (Docker ${{ matrix.shard }}) needs: [build] if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-') runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false matrix: shard: ['1/2', '2/2'] env: ORCA_BACKGROUND_LAUNCH: '1' steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} persist-credentials: false # COMPAT(legacy-relay-bridge): the bridge spec builds 1.4.224's relay from its tagged sources. - name: Fetch the 1.4.224 relay sources run: git fetch --no-tags --depth=1 origin +refs/tags/v1.4.224:refs/tags/v1.4.224 - name: Install headless tools run: sudo apt-get update && sudo apt-get install -y build-essential ripgrep xvfb openbox x11-utils - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - uses: actions/download-artifact@v8 with: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ # Built to a path the app does not look at by default, so the relay-era phase has no template. - name: Build the linux-x64-glibc orcad template run: | pnpm build:orcad-prebuilds pnpm build:orcad-prebuilds --require-slots linux-x64-glibc pnpm build:orcad node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc mv out/orcad-template "$RUNNER_TEMP/orcad-convert-template" # The Windows CLI matrix collects here too and skips: it needs a Windows host descriptor. - name: Run managed SSH specs on a Docker host env: SKIP_BUILD: '1' ORCA_E2E_SSH_DOCKER: '1' ORCA_E2E_ORCAD_CONVERT_HOST: docker ORCA_E2E_FORWARD_APP_LOGS: '1' SHARD: ${{ matrix.shard }} run: | export ORCA_E2E_ORCAD_CONVERT_TEMPLATE="$RUNNER_TEMP/orcad-convert-template" xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad- --project=electron-headless --workers=1 --shard="$SHARD" - uses: actions/upload-artifact@v7 if: failure() with: name: orcad-ssh-docker-traces-${{ strategy.job-index }}${{ inputs.artifact_suffix }} path: test-results/ retention-days: 7 if-no-files-found: ignore # D7 on Windows: both hosts share \daemon and so one daemon pipe. Built here rather than # from the Linux e2e build because the slot, template and addons are Windows-native. Also runs the # missing-AppData startup check and the explorer file-copy check, which need the same Windows e2e build. orcad-serve-mode-switch-windows: name: orca serve on Windows (D7 mode switch, missing AppData) if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') || contains(inputs.test_files, 'tests/e2e/windows-missing-appdata-startup.spec.ts') || contains(inputs.test_files, 'tests/e2e/file-explorer-copy-file-windows.spec.ts') runs-on: windows-2022 timeout-minutes: 45 env: NODE_OPTIONS: --max-old-space-size=4096 ORCA_BACKGROUND_LAUNCH: '1' steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} persist-credentials: false - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - uses: ./.github/actions/prepare-orcad-prebuilds with: resolve-windows-cache: 'true' restore-windows-cache: 'true' - uses: ./.github/actions/prepare-windows-cli-launcher with: arches: x64 - name: Build the e2e app, CLI, orcad slot and template shell: bash run: | pnpm run build:wsl-guest pnpm exec electron-vite build --mode e2e pnpm run build:cli pnpm build:orcad node config/scripts/build-windows-process-tree-addon.mjs --arch=x64 node config/scripts/build-orcad-template.mjs --targets win32-x64 # A profile-less Windows session has no roaming AppData; Electron 43 used to crash natively there. - name: Start serve with no AppData folder if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/windows-missing-appdata-startup.spec.ts') env: SKIP_BUILD: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/windows-missing-appdata-startup.spec.ts --project=electron-headless --workers=1 # Explorer pastes only a real CF_HDROP, which needs the Windows clipboard and shell. - name: Copy a file from the explorer if: ${{ !cancelled() && (inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/file-explorer-copy-file-windows.spec.ts')) }} env: SKIP_BUILD: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/file-explorer-copy-file-windows.spec.ts --project=electron-headless --workers=1 - name: Switch serve hosts on one profile if: ${{ !cancelled() && (inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts')) }} env: SKIP_BUILD: '1' ORCA_E2E_ORCAD_SERVE: '1' ORCA_E2E_FORWARD_APP_LOGS: '1' ORCA_E2E_PRESERVE_PROFILE_LOGS_DIR: ${{ github.workspace }}\test-results\profile-logs run: pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/orcad-serve-mode-switch.spec.ts --project=electron-headless --workers=1 - uses: actions/upload-artifact@v7 if: failure() with: name: orcad-serve-mode-switch-windows-traces${{ inputs.artifact_suffix }} path: test-results/ retention-days: 7 if-no-files-found: ignore