chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
name: Computer-use e2e
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/computer-e2e.yml'
|
||||
- 'config/electron-builder.config.cjs'
|
||||
- 'config/scripts/build-computer-macos.mjs'
|
||||
- 'config/scripts/computer-e2e-workflow.test.mjs'
|
||||
- 'config/scripts/computer-use-skill-guidance.test.mjs'
|
||||
- 'config/scripts/computer-use-smoke.mjs'
|
||||
- 'config/scripts/computer-use-smoke.test.mjs'
|
||||
- 'config/scripts/daemon-boot-smoke.mjs'
|
||||
- 'config/scripts/windows-daemon-workspace-close-repro.mjs'
|
||||
- 'config/scripts/verify-computer-native.mjs'
|
||||
# Why: the native-smoke job boots the built terminal daemon under plain
|
||||
# Node, so any change to the daemon bundle graph or the main build must
|
||||
# re-run it (the v1.4.129-rc.1 daemon outage shipped with green CI).
|
||||
- 'electron.vite.config.ts'
|
||||
- 'build-plugins/**'
|
||||
- 'src/main/daemon/**'
|
||||
- 'native/computer-use-macos/**'
|
||||
- 'native/computer-use-linux/**'
|
||||
- 'native/computer-use-windows/**'
|
||||
- 'skills/computer-use/SKILL.md'
|
||||
- 'src/cli/**'
|
||||
- 'src/main/computer/**'
|
||||
- 'src/main/runtime/rpc/dispatcher.ts'
|
||||
- 'src/main/runtime/rpc/errors.ts'
|
||||
- 'src/main/runtime/rpc/methods/computer*.ts'
|
||||
- 'src/shared/computer-use-*.ts'
|
||||
- 'tests/e2e/computer-linux.e2e.ts'
|
||||
- 'tests/e2e/computer-mac.e2e.ts'
|
||||
- 'tests/e2e/computer-mac-safari.e2e.ts'
|
||||
- 'tests/e2e/computer-windows.e2e.ts'
|
||||
- 'tests/e2e/computer-windows-store.e2e.ts'
|
||||
- 'tests/e2e/helpers/computer-cli-driver.ts'
|
||||
- 'tests/e2e/helpers/computer-driver.ts'
|
||||
- 'tests/e2e/vitest.config.ts'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
jobs:
|
||||
native-smoke:
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
- if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y python3 python3-gi gir1.2-atspi-2.0 at-spi2-core gedit xvfb xclip xdotool
|
||||
# Why: pnpm's bundled node-gyp can ship gyp_main.py without execute
|
||||
# permission on Linux runners; node-pty's install fallback then fails
|
||||
# before this smoke job can exercise the native package.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: >-
|
||||
pnpm vitest run
|
||||
config/scripts/build-windows-cli-launcher.test.mjs
|
||||
config/scripts/computer-e2e-workflow.test.mjs
|
||||
config/scripts/computer-use-skill-guidance.test.mjs
|
||||
config/scripts/computer-use-smoke.test.mjs
|
||||
src/main/computer/computer-provider-lifecycle.test.ts
|
||||
src/main/computer/computer-provider-unavailable-message.test.ts
|
||||
src/main/computer/sidecar-client.test.ts
|
||||
src/main/computer/macos-native-provider-client.test.ts
|
||||
src/main/computer/macos-native-provider-socket.test.ts
|
||||
src/main/computer/macos-computer-use-permissions.test.ts
|
||||
src/main/computer/macos-computer-use-permission-status.test.ts
|
||||
src/main/computer/desktop-script-provider-client.test.ts
|
||||
src/main/computer/desktop-script-provider-cache.test.ts
|
||||
src/main/computer/desktop-script-provider-actions.test.ts
|
||||
src/main/computer/desktop-script-provider-cache-lifecycle.test.ts
|
||||
src/main/computer/desktop-script-provider-errors.test.ts
|
||||
src/main/computer/desktop-script-provider-action-errors.test.ts
|
||||
src/shared/computer-use-error-recovery.test.ts
|
||||
src/shared/computer-use-key-spec.test.ts
|
||||
src/cli/format.test.ts
|
||||
src/cli/handlers/computer.test.ts
|
||||
src/cli/handlers/computer-action-routing.test.ts
|
||||
src/cli/handlers/computer-action-validation.test.ts
|
||||
src/cli/handlers/computer-state-formatting.test.ts
|
||||
src/cli/specs/computer.test.ts
|
||||
src/cli/index.test.ts
|
||||
src/main/runtime/rpc/dispatcher-computer-errors.test.ts
|
||||
src/main/runtime/rpc/errors.test.ts
|
||||
src/main/runtime/rpc/methods/computer.test.ts
|
||||
src/main/runtime/rpc/methods/computer-actions.test.ts
|
||||
src/cli/runtime/envelope-schema.test.ts
|
||||
src/shared/remote-runtime-client.test.ts
|
||||
- run: pnpm verify:computer-native
|
||||
- run: pnpm build:cli
|
||||
- run: pnpm build:electron-vite
|
||||
# Why: boot the BUILT daemon-entry under plain Node the way production
|
||||
# forks it. v1.4.129-rc.1 shipped a daemon that exited at module load
|
||||
# (leaked electron require) while every other check passed; this fails
|
||||
# the PR when the built daemon cannot start on ubuntu-22.04 / windows.
|
||||
- name: Daemon boot smoke
|
||||
run: node config/scripts/daemon-boot-smoke.mjs
|
||||
# Why: workspace removal overlaps graceful renderer teardown with the
|
||||
# forced main-process sweep; exercise that exact pair against real ConPTY.
|
||||
- name: Windows daemon workspace-close repro
|
||||
if: runner.os == 'Windows'
|
||||
run: node config/scripts/windows-daemon-workspace-close-repro.mjs
|
||||
- if: runner.os == 'Linux'
|
||||
env:
|
||||
ORCA_COMPUTER_E2E: '1'
|
||||
ACCESSIBILITY_ENABLED: '1'
|
||||
run: xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts
|
||||
- if: runner.os == 'Windows'
|
||||
env:
|
||||
ORCA_COMPUTER_E2E: '1'
|
||||
run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts
|
||||
|
||||
mac:
|
||||
# macOS Accessibility and Screen Recording require user-granted TCC entries.
|
||||
# Keep this on manual/scheduled permission-bearing runners instead of PR CI.
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: macos-15
|
||||
env:
|
||||
ORCA_COMPUTER_E2E: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build:computer-macos
|
||||
- run: pnpm verify:computer-native
|
||||
- run: pnpm build:cli
|
||||
- run: pnpm build:electron-vite
|
||||
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts
|
||||
|
||||
linux:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
ORCA_COMPUTER_E2E: '1'
|
||||
ACCESSIBILITY_ENABLED: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential python3 python3-gi gir1.2-atspi-2.0 gedit at-spi2-core xvfb xclip xdotool
|
||||
# Why: keep scheduled Linux e2e on the same native install path as PR
|
||||
# smoke and pr.yml's verify job.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm verify:computer-native
|
||||
- run: pnpm build:cli
|
||||
- run: pnpm build:electron-vite
|
||||
- run: xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts
|
||||
|
||||
windows:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
ORCA_COMPUTER_E2E: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm verify:computer-native
|
||||
- run: pnpm build:cli
|
||||
- run: pnpm build:electron-vite
|
||||
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-store.e2e.ts
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Daemon Relocation Spike
|
||||
|
||||
# Why: Phase 1 of the Windows update-survival work relocates the terminal
|
||||
# daemon's file closure out of the install dir so it survives an update. Before
|
||||
# implementing it in the app, this spike finds the MINIMAL set of packaged files
|
||||
# that a copied Orca.exe (run as node) needs to start the daemon and drive a
|
||||
# ConPTY session from a relocated directory, holding no install-dir file locks.
|
||||
# Runs from the feature branch via push (workflow_dispatch only works on the
|
||||
# default branch); main is never touched.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- Jinwoo-H/windows-update-survival
|
||||
paths:
|
||||
- 'tools/daemon-relocation-spike/**'
|
||||
- '.github/workflows/daemon-relocation-spike.yml'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: daemon-relocation-spike-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
spike:
|
||||
name: relocation file-set spike
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# This job only reads the repo to build/run the spike; it never pushes.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: cache the unpacked build keyed on the inputs that affect it, so
|
||||
# spike-script-only edits skip the ~15 min electron-builder build.
|
||||
- name: Cache unpacked build
|
||||
id: cache-unpacked
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: dist/win-unpacked
|
||||
key: win-unpacked-${{ hashFiles('src/**', 'config/**', 'package.json', 'pnpm-lock.yaml') }}
|
||||
|
||||
- name: Build unpacked app
|
||||
if: steps.cache-unpacked.outputs.cache-hit != 'true'
|
||||
run: pnpm run build:unpack
|
||||
|
||||
- name: Run relocation spike (all tiers)
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force artifacts | Out-Null
|
||||
$log = "artifacts/spike-output.log"
|
||||
$tiers = @('full', 'no-gpu', 'minimal')
|
||||
$any = $false
|
||||
foreach ($tier in $tiers) {
|
||||
"===== TIER: $tier =====" | Tee-Object -FilePath $log -Append
|
||||
$work = Join-Path $env:RUNNER_TEMP "spike-$tier"
|
||||
# --keep-work-dir so the per-tier daemon stdout/stderr logs survive
|
||||
# for the artifact upload (the spike otherwise removes work-dir),
|
||||
# which is what diagnoses a tier that fails to reach ready.
|
||||
node tools/daemon-relocation-spike/spike.mjs `
|
||||
--app-dir dist/win-unpacked `
|
||||
--work-dir "$work" `
|
||||
--tier $tier `
|
||||
--keep-work-dir 2>&1 | Tee-Object -FilePath $log -Append
|
||||
if ($LASTEXITCODE -eq 0) { $any = $true }
|
||||
}
|
||||
if (-not $any) { throw "No tier passed the relocation spike" }
|
||||
|
||||
- name: Upload spike output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: daemon-relocation-spike-output
|
||||
path: |
|
||||
artifacts/spike-output.log
|
||||
${{ runner.temp }}/spike-*/**/*.log
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,175 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ref to check out (defaults to the calling workflow's ref)
|
||||
required: false
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ref to check out (defaults to the workflow ref)
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
# Why: GitHub cron uses UTC; these slots map to 10am and 3pm
|
||||
# America/Phoenix for the default-branch E2E run.
|
||||
- cron: '0 17,22 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build e2e app
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
# Why: the E2E build compiles native modules via node-gyp. Mirrors the
|
||||
# install step in pr.yml's verify job so E2E doesn't hit missing-toolchain
|
||||
# errors.
|
||||
- name: Install native build tools
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: this job runs the same pnpm install path as pr.yml's verify
|
||||
# job, so it needs the same pinned node-gyp override to avoid pnpm's
|
||||
# broken bundled gyp_main.py on Linux.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: building once avoids five parallel electron-vite builds inside
|
||||
# Playwright globalSetup, which otherwise contends for CPU/RAM on OSS
|
||||
# runners before the sharded tests even start.
|
||||
- name: Build Electron app for E2E
|
||||
run: npx electron-vite build --mode e2e
|
||||
|
||||
- name: Upload E2E build output
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e-build-out
|
||||
path: out/
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
e2e:
|
||||
name: e2e ${{ matrix.shard_name }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- shard: '1/10'
|
||||
shard_name: 1-of-10
|
||||
- shard: '2/10'
|
||||
shard_name: 2-of-10
|
||||
- shard: '3/10'
|
||||
shard_name: 3-of-10
|
||||
- shard: '4/10'
|
||||
shard_name: 4-of-10
|
||||
- shard: '5/10'
|
||||
shard_name: 5-of-10
|
||||
- shard: '6/10'
|
||||
shard_name: 6-of-10
|
||||
- shard: '7/10'
|
||||
shard_name: 7-of-10
|
||||
- shard: '8/10'
|
||||
shard_name: 8-of-10
|
||||
- shard: '9/10'
|
||||
shard_name: 9-of-10
|
||||
- shard: '10/10'
|
||||
shard_name: 10-of-10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
# Why: pnpm install rebuilds native modules, and those postinstall
|
||||
# scripts still need the Linux toolchain even though this shard reuses
|
||||
# the prebuilt Electron output.
|
||||
- name: Install native build tools
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
|
||||
# Why: Electron on Linux needs an X display even when the app
|
||||
# suppresses mainWindow.show() via ORCA_E2E_HEADLESS. xvfb provides a
|
||||
# virtual framebuffer so Chromium can initialize without a real display.
|
||||
- name: Install xvfb
|
||||
run: sudo apt-get install -y xvfb
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: this job runs the same pnpm install path as pr.yml's verify
|
||||
# job, so it needs the same pinned node-gyp override to avoid pnpm's
|
||||
# broken bundled gyp_main.py on Linux. Gate on runner.os matches
|
||||
# release.yml so the invariant "this workaround is Linux-only" is
|
||||
# consistent across all three workflows, even though this job
|
||||
# currently pins runs-on: ubuntu-latest.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download E2E build output
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: e2e-build-out
|
||||
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: Run E2E tests (${{ matrix.shard_name }})
|
||||
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e --shard=${{ matrix.shard }}
|
||||
|
||||
# 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()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-traces-${{ matrix.shard_name }}
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Golden E2E Experiment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/golden-e2e-experiment.yml'
|
||||
- 'package.json'
|
||||
- 'tests/e2e/golden-core-flows.spec.ts'
|
||||
- 'tests/e2e/fixtures/terminal-emoji-table.md'
|
||||
- 'tests/e2e/helpers/**'
|
||||
- 'tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts'
|
||||
- 'src/renderer/src/components/sidebar/SidebarHeader.tsx'
|
||||
- 'src/renderer/src/lib/pane-manager/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ref to check out (defaults to the workflow ref)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
golden-e2e:
|
||||
name: golden e2e ${{ matrix.platform }} experiment
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
- os: macos-15
|
||||
platform: mac
|
||||
# Why: Windows golden E2E is temporarily disabled on CI while its
|
||||
# flaky runner-only failures are investigated.
|
||||
# - os: windows-latest
|
||||
# platform: windows
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why: pull_request merge refs disappear after merge, but experiment
|
||||
# reruns still need a stable commit to checkout.
|
||||
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Install native build tools
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: Linux golden E2E uses the same native install path as PR/release CI,
|
||||
# which needs pnpm to bypass its non-executable bundled gyp_main.py.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Electron app for E2E
|
||||
run: npx electron-vite build --mode e2e
|
||||
|
||||
- name: Run golden E2E tests on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
|
||||
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
|
||||
|
||||
- name: Run golden E2E tests on macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
|
||||
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
|
||||
|
||||
- name: Upload Playwright traces
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: golden-${{ matrix.platform }}-playwright-traces
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,199 @@
|
||||
name: Homebrew Cask Bump
|
||||
|
||||
# Why: keep the stablyai/homebrew-orca tap in sync with GitHub Releases.
|
||||
# Triggers:
|
||||
# - workflow_call → release-cut.yml finished publishing a stable tag
|
||||
# - workflow_dispatch → manual backfill for a specific tag
|
||||
#
|
||||
# The job computes the sha256 of orca-macos-arm64.dmg + orca-macos-x64.dmg
|
||||
# from the release, templates the channel-specific cask in this repo with the
|
||||
# new version and hashes, then PRs the result into stablyai/homebrew-orca.
|
||||
# Stable tags update Casks/orca.rb; RC tags update Casks/orca@rc.rb.
|
||||
#
|
||||
# Why no `release.published` trigger: other products shipped from this repo
|
||||
# (e.g. mobile under `mobile-v*`) also publish GitHub Releases. A blanket
|
||||
# `release.published` trigger would fire this workflow for any of them and
|
||||
# attempt to download `orca-macos-*.dmg` from a non-desktop release, at
|
||||
# best 404ing and at worst (if the filter were weaker) rewriting the cask
|
||||
# to a non-desktop version. The tap is only ever bumped by the desktop
|
||||
# release pipeline calling this workflow directly.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to sync (e.g. v1.3.25)
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to sync (e.g. v1.3.25)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-cask:
|
||||
runs-on: ubuntu-latest
|
||||
# Why: only desktop tags have the macOS DMGs this workflow downloads.
|
||||
# A mobile tag such as `mobile-v0.0.1` must never rewrite either cask.
|
||||
if: >
|
||||
inputs.tag != '' &&
|
||||
startsWith(inputs.tag, 'v')
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout orca
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Resolve tag and version
|
||||
id: tag
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$INPUT_TAG" ]]; then
|
||||
tag="$INPUT_TAG"
|
||||
else
|
||||
tag="$RELEASE_TAG"
|
||||
fi
|
||||
version="${tag#v}"
|
||||
echo "tag=$tag" >>"$GITHUB_OUTPUT"
|
||||
echo "version=$version" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve cask target
|
||||
id: cask
|
||||
env:
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
token="orca@rc"
|
||||
branch_token="orca-rc"
|
||||
elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
token="orca"
|
||||
branch_token="orca"
|
||||
else
|
||||
echo "::error::Unsupported desktop tag shape: $TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
path="Casks/${token}.rb"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "::error::Missing cask template: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "token=$token" >>"$GITHUB_OUTPUT"
|
||||
echo "path=$path" >>"$GITHUB_OUTPUT"
|
||||
echo "branch=bump/${branch_token}-${VERSION}" >>"$GITHUB_OUTPUT"
|
||||
echo "title=${token} ${VERSION}" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download DMGs and compute sha256
|
||||
id: hash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dmgs
|
||||
gh release download "$TAG" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--pattern 'orca-macos-arm64.dmg' \
|
||||
--pattern 'orca-macos-x64.dmg' \
|
||||
--dir dmgs
|
||||
arm=$(sha256sum dmgs/orca-macos-arm64.dmg | awk '{print $1}')
|
||||
intel=$(sha256sum dmgs/orca-macos-x64.dmg | awk '{print $1}')
|
||||
echo "arm=$arm" >>"$GITHUB_OUTPUT"
|
||||
echo "intel=$intel" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Render updated cask file
|
||||
env:
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
ARM_SHA: ${{ steps.hash.outputs.arm }}
|
||||
INTEL_SHA: ${{ steps.hash.outputs.intel }}
|
||||
CASK_PATH: ${{ steps.cask.outputs.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import os, re, pathlib
|
||||
p = pathlib.Path(os.environ["CASK_PATH"])
|
||||
src = p.read_text()
|
||||
src = re.sub(r'version "[^"]+"',
|
||||
f'version "{os.environ["VERSION"]}"', src, count=1)
|
||||
src = re.sub(r'sha256 arm:\s*"[0-9a-f]+",\s*\n\s*intel:\s*"[0-9a-f]+"',
|
||||
'sha256 arm: "{arm}",\n intel: "{intel}"'.format(
|
||||
arm=os.environ["ARM_SHA"],
|
||||
intel=os.environ["INTEL_SHA"]),
|
||||
src, count=1)
|
||||
p.write_text(src)
|
||||
print(src)
|
||||
PY
|
||||
|
||||
# Why: reuse the existing buf0-bot GitHub App (also used by
|
||||
# track-community-prs.yaml) instead of minting a new PAT. The app is
|
||||
# installed org-wide, so it has access to stablyai/homebrew-orca
|
||||
# automatically. GITHUB_TOKEN cannot push cross-repo; a short-lived
|
||||
# installation token can.
|
||||
- name: Generate buf0-bot token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: 2590194
|
||||
private-key: ${{ secrets.BUFO_BOT_PRIVATE_KEY }}
|
||||
owner: stablyai
|
||||
repositories: homebrew-orca
|
||||
|
||||
- name: Checkout tap
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: stablyai/homebrew-orca
|
||||
path: tap
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Copy cask into tap and open PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
CASK_PATH: ${{ steps.cask.outputs.path }}
|
||||
BRANCH: ${{ steps.cask.outputs.branch }}
|
||||
TITLE: ${{ steps.cask.outputs.title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "tap/$(dirname "$CASK_PATH")"
|
||||
cp "$CASK_PATH" "tap/$CASK_PATH"
|
||||
|
||||
cd tap
|
||||
# Why: commit author must match the buf0-bot app so commits are
|
||||
# attributed to the bot in the tap's history / PR UI.
|
||||
git config user.name "buf0-bot[bot]"
|
||||
git config user.email "252831055+buf0-bot[bot]@users.noreply.github.com"
|
||||
|
||||
if git diff --quiet; then
|
||||
echo "Cask already up-to-date for $TAG; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
branch="$BRANCH"
|
||||
git checkout -b "$branch"
|
||||
git add "$CASK_PATH"
|
||||
git commit -m "$TITLE"
|
||||
git push -u origin "$branch" --force
|
||||
|
||||
# Why: repeated backfills can race an existing PR branch, so edit
|
||||
# the PR title if creation reports that one already exists.
|
||||
gh pr create \
|
||||
--title "$TITLE" \
|
||||
--body "Automated bump of $CASK_PATH from stablyai/orca release $TAG." \
|
||||
--base main \
|
||||
--head "$branch" \
|
||||
|| gh pr edit "$branch" --title "$TITLE"
|
||||
|
||||
# Why: squash-merge immediately rather than --auto. The tap has no
|
||||
# required checks or branch protection, and `gh pr merge --auto`
|
||||
# only activates when there's something to wait on — on a repo
|
||||
# with no gates it silently no-ops, leaving the PR open forever.
|
||||
gh pr merge "$branch" --squash --delete-branch
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Label Issues by OS
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
apply-os-label:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Apply OS label from issue form
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const body = issue.body || "";
|
||||
const osMatch = body.match(/^### Operating system\s+(.+)$/m);
|
||||
|
||||
if (!osMatch) {
|
||||
core.info("No operating system field found. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOs = osMatch[1].trim().toLowerCase();
|
||||
const osLabelMap = {
|
||||
macos: "os:macos",
|
||||
windows: "os:Windows",
|
||||
linux: "os:linux",
|
||||
};
|
||||
|
||||
const desiredLabel = osLabelMap[selectedOs];
|
||||
if (!desiredLabel) {
|
||||
core.info(`No OS label configured for "${selectedOs}". Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const managedLabels = Object.values(osLabelMap);
|
||||
const existingLabels = issue.labels.map((label) =>
|
||||
typeof label === "string" ? label : label.name,
|
||||
);
|
||||
const labelsToKeep = existingLabels.filter(
|
||||
(label) => !managedLabels.includes(label),
|
||||
);
|
||||
const nextLabels = [...new Set([...labelsToKeep, desiredLabel])];
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: nextLabels,
|
||||
});
|
||||
|
||||
core.info(`Applied label ${desiredLabel} to issue #${issue.number}.`);
|
||||
@@ -0,0 +1,138 @@
|
||||
name: Linux Wayland GPU Sandbox
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/linux-wayland-gpu-sandbox.yml
|
||||
- config/scripts/linux-wayland-renderer-diagnostics.mjs
|
||||
- config/scripts/linux-wayland-terminal-exercise.mjs
|
||||
- config/scripts/linux-wayland-validation-watchdog.mjs
|
||||
- config/scripts/verify-linux-wayland-gpu-sandbox.mjs
|
||||
- src/main/startup/configure-process.ts
|
||||
- src/main/startup/configure-process.test.ts
|
||||
- src/preload/api-types.ts
|
||||
- src/preload/index.ts
|
||||
- src/renderer/src/components/terminal-pane/pty-connection.ts
|
||||
- src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.ts
|
||||
- src/renderer/src/web/web-preload-api.ts
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: wayland terminal input
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install native build tools and Wayland compositor
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3 zsh weston
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: mirrors pr.yml so native module rebuilds do not use pnpm's
|
||||
# non-executable bundled gyp_main.py on Linux runners.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare dependency install
|
||||
run: |
|
||||
if [ -e node_modules ]; then
|
||||
ls -ld node_modules
|
||||
rm -rf node_modules
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
# Why: pnpm 10.24's frozen headless fast path can fail on fresh Ubuntu
|
||||
# runners while creating the root node_modules. Use the normal resolver
|
||||
# path, then verify package metadata stayed unchanged.
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile --prefer-frozen-lockfile=false
|
||||
git diff --exit-code package.json pnpm-lock.yaml
|
||||
|
||||
- name: Start Weston
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export XDG_RUNTIME_DIR="$RUNNER_TEMP/xdg-runtime"
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
chmod 700 "$XDG_RUNTIME_DIR"
|
||||
unset DISPLAY
|
||||
weston --backend=headless-backend.so --socket=wayland-1 --idle-time=0 --log="$RUNNER_TEMP/weston.log" &
|
||||
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV"
|
||||
echo "WAYLAND_DISPLAY=wayland-1" >> "$GITHUB_ENV"
|
||||
echo "XDG_SESSION_TYPE=wayland" >> "$GITHUB_ENV"
|
||||
echo "ELECTRON_OZONE_PLATFORM_HINT=wayland" >> "$GITHUB_ENV"
|
||||
echo "ORCA_WAYLAND_GPU_VERBOSE=1" >> "$GITHUB_ENV"
|
||||
for _ in $(seq 1 50); do
|
||||
if [ -S "$XDG_RUNTIME_DIR/wayland-1" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
test -S "$XDG_RUNTIME_DIR/wayland-1"
|
||||
|
||||
- name: Reproduce terminal input freeze without the workaround
|
||||
id: reproduce_base
|
||||
if: github.event_name == 'pull_request'
|
||||
# Why: still collect fixed-path Wayland evidence when the base repro
|
||||
# stops reproducing; the final gate below fails the job in that case.
|
||||
continue-on-error: true
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}"
|
||||
# Why: once the PR base includes this workaround, the partial
|
||||
# checkout cannot reconstruct an unfixed Wayland negative control.
|
||||
if git show "${{ github.event.pull_request.base.sha }}:src/main/startup/configure-process.ts" | grep -Eq "appendSwitch\\(['\"]disable-gpu-sandbox['\"]\\)"; then
|
||||
echo "The base build already contains the Linux Wayland GPU sandbox workaround; skipping unfixed-path reproduction."
|
||||
exit 0
|
||||
fi
|
||||
# Why: keep the new verifier scripts, but run them against the
|
||||
# unfixed production terminal/GPU path instead of a hybrid checkout.
|
||||
git checkout "${{ github.event.pull_request.base.sha }}" -- \
|
||||
src/main/startup/configure-process.ts \
|
||||
src/preload/api-types.ts \
|
||||
src/preload/index.ts \
|
||||
src/renderer/src/components/terminal-pane/pty-connection.ts \
|
||||
src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.ts \
|
||||
src/renderer/src/web/web-preload-api.ts
|
||||
rm -rf out
|
||||
node config/scripts/verify-linux-wayland-gpu-sandbox.mjs --mode=expect-repro
|
||||
|
||||
- name: Verify terminal input under Wayland GPU sandbox workaround
|
||||
if: ${{ !cancelled() }}
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git checkout --force "$GITHUB_SHA"
|
||||
rm -rf out
|
||||
node config/scripts/verify-linux-wayland-gpu-sandbox.mjs
|
||||
|
||||
- name: Require base reproduction
|
||||
if: ${{ github.event_name == 'pull_request' && steps.reproduce_base.outcome != 'success' }}
|
||||
run: |
|
||||
echo "The unfixed base build did not reproduce the Wayland terminal input failure."
|
||||
exit 1
|
||||
|
||||
- name: Upload Weston log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: weston-log
|
||||
path: ${{ runner.temp }}/weston.log
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Mobile Android Release
|
||||
|
||||
# Why a separate workflow from iOS: iOS releases go through App Store review,
|
||||
# which can take days. Decoupling the triggers lets an Android release ship
|
||||
# immediately without waiting on iOS, and vice versa.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'mobile-android-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_version:
|
||||
description: 'Optional exact mobile marketing version assertion, e.g. 0.0.22'
|
||||
required: false
|
||||
type: string
|
||||
publish_github_release:
|
||||
description: 'Create or update the mobile-android-v<version> GitHub Release'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
android-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Why: the "Create GitHub Release" step below uses the default GITHUB_TOKEN
|
||||
# to call `gh release create`, which requires contents:write. Without this,
|
||||
# the job builds the APK fine but fails at release time with
|
||||
# "HTTP 403: Resource not accessible by integration".
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Resolve release version and version code
|
||||
id: release
|
||||
env:
|
||||
MOBILE_ANDROID_RELEASE_VERSION: ${{ github.event.inputs.release_version }}
|
||||
MOBILE_ANDROID_PUBLISH_RELEASE: ${{ github.event.inputs.publish_github_release }}
|
||||
run: node scripts/prepare-android-release.mjs
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Expo prebuild
|
||||
run: npx expo prebuild --platform android --no-install
|
||||
|
||||
- name: Build Android release APK
|
||||
run: cd android && ./gradlew assembleRelease
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: orca-mobile-apk
|
||||
path: mobile/android/app/build/outputs/apk/release/*.apk
|
||||
|
||||
- name: Ensure GitHub release tag
|
||||
if: steps.release.outputs.publish_release == 'true' && !startsWith(github.ref, 'refs/tags/mobile-android-v')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${{ steps.release.outputs.tag }}"
|
||||
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
|
||||
echo "Tag $tag already exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git tag "$tag" "$GITHUB_SHA"
|
||||
git push origin "refs/tags/$tag"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.release.outputs.publish_release == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${{ steps.release.outputs.tag }}"
|
||||
|
||||
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release upload "$tag" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber \
|
||||
android/app/build/outputs/apk/release/*.apk
|
||||
else
|
||||
gh release create "$tag" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "Orca Mobile Android $tag" \
|
||||
--prerelease \
|
||||
--latest=false \
|
||||
--generate-notes \
|
||||
android/app/build/outputs/apk/release/*.apk
|
||||
fi
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Mobile iOS Release
|
||||
|
||||
# Why a separate workflow from Android: iOS releases go through App Store
|
||||
# review, which can take days. Decoupling the triggers lets an Android release
|
||||
# ship immediately without waiting on iOS, and vice versa.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'mobile-ios-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump_patch_version:
|
||||
description: 'Bump the iOS marketing version patch number before release. Tick this after a version has shipped to the App Store (the release fails fast if the current version''s train is already closed).'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
release_version:
|
||||
description: 'Optional exact iOS marketing version override, e.g. 0.0.15'
|
||||
required: false
|
||||
type: string
|
||||
testflight_changelog:
|
||||
description: 'Optional TestFlight external tester changelog'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
ios-build:
|
||||
# GitHub-hosted macOS runner: required for Xcode. Expo SDK 55's
|
||||
# expo-modules-core declares swift_version 6.0 and uses Swift 6 syntax
|
||||
# (@MainActor isolation). Xcode 16.x (macos-15) can't even parse it
|
||||
# ("unknown attribute 'MainActor'"), so we need Xcode 26.x → macos-26.
|
||||
runs-on: macos-26
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Select Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
# Xcode 26.x ships the Swift 6.x toolchain Expo SDK 55 requires.
|
||||
xcode-version: '26.5'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Ruby and fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
working-directory: mobile
|
||||
|
||||
- name: Resolve release version and build number
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
ASC_API_KEY_P8: ${{ secrets.ASC_API_KEY_P8 }}
|
||||
MOBILE_IOS_RELEASE_VERSION: ${{ github.event.inputs.release_version }}
|
||||
MOBILE_IOS_BUMP_PATCH_VERSION: ${{ github.event.inputs.bump_patch_version }}
|
||||
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
||||
FASTLANE_HIDE_CHANGELOG: '1'
|
||||
run: bundle exec fastlane ios prepare_release_version version:"$MOBILE_IOS_RELEASE_VERSION" bump_patch:"$MOBILE_IOS_BUMP_PATCH_VERSION"
|
||||
|
||||
- name: Expo prebuild
|
||||
run: npx expo prebuild --platform ios --no-install
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: npx pod-install ios
|
||||
|
||||
# Why: `-allowProvisioningUpdates` + the App Store Connect API key can
|
||||
# create/refresh provisioning profiles, but it cannot recreate the
|
||||
# distribution certificate's PRIVATE KEY across runs. So we import a
|
||||
# pre-exported distribution .p12 (created once via Apple Developer) into a
|
||||
# throwaway keychain. The keychain is ephemeral to the runner and torn
|
||||
# down with the VM; nothing secret is written to the repo.
|
||||
- name: Import distribution certificate
|
||||
env:
|
||||
IOS_DIST_CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }}
|
||||
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/orca-signing.keychain-db"
|
||||
# Random per-run keychain password; never persisted.
|
||||
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
|
||||
CERT_PATH="$RUNNER_TEMP/orca-dist-cert.p12"
|
||||
|
||||
echo "$IOS_DIST_CERT_P12" | base64 --decode > "$CERT_PATH"
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security import "$CERT_PATH" -P "$IOS_DIST_CERT_PASSWORD" \
|
||||
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
||||
# Allow codesign/xcodebuild to use the key without an interactive prompt.
|
||||
security set-key-partition-list -S apple-tool:,apple: \
|
||||
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
|
||||
# Put our keychain in the search list so xcodebuild can find the identity.
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
|
||||
rm -f "$CERT_PATH"
|
||||
|
||||
- name: Build and upload to TestFlight
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
ASC_API_KEY_P8: ${{ secrets.ASC_API_KEY_P8 }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
TESTFLIGHT_CHANGELOG: ${{ github.event.inputs.testflight_changelog }}
|
||||
# Keep fastlane non-interactive and quiet about analytics in CI.
|
||||
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
||||
FASTLANE_HIDE_CHANGELOG: '1'
|
||||
run: bundle exec fastlane ios release
|
||||
|
||||
- name: Upload .ipa artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: orca-mobile-ipa
|
||||
path: mobile/build/*.ipa
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Mobile Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
# Why: the mobile terminal link parsers are conformance-tested against
|
||||
# these shared fixtures; desktop-side fixture edits must re-run this suite.
|
||||
- 'src/shared/terminal-file-link-conformance.ts'
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: the mobile typecheck imports shared types from ../src/shared, and
|
||||
# some of those files import runtime deps (tweetnacl, ws) resolved from
|
||||
# the repo-root node_modules. Without a root install, tsc fails with
|
||||
# "Cannot find module 'tweetnacl'/'ws'". Mobile is a separate pnpm project
|
||||
# (not in the root workspace), so this is a distinct install.
|
||||
# --ignore-scripts skips the root postinstall (Electron native-module
|
||||
# rebuild) which is irrelevant to a type-only check and would only add
|
||||
# time and failure surface on this ubuntu mobile runner.
|
||||
- name: Install root dependencies
|
||||
working-directory: .
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
@@ -0,0 +1,154 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install native build tools
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3 zlib1g-dev zsh
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: pnpm's bundled node-gyp ships gyp_main.py without execute
|
||||
# permission, which breaks native module builds (e.g. node-pty's
|
||||
# postinstall) with "/bin/sh: gyp_main.py: Permission denied".
|
||||
# Pin the fallback to the lockfile's node-gyp version so CI stays
|
||||
# reproducible while forcing pnpm to bypass its broken bundled copy.
|
||||
# Gate on runner.os == 'Linux' to match release.yml — the
|
||||
# npm-global path layout this step assumes is POSIX-shaped, and the
|
||||
# failure has only been observed on Linux runners. Today this job
|
||||
# pins runs-on: ubuntu-latest so the guard is a no-op, but it
|
||||
# prevents a silent break if a Windows/macOS matrix is added later.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare dependency install
|
||||
run: |
|
||||
if [ -e node_modules ]; then
|
||||
ls -ld node_modules
|
||||
rm -rf node_modules
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
# Why: pnpm 10.24's frozen headless fast path can fail on fresh Ubuntu
|
||||
# runners while creating the root node_modules. Use the normal resolver
|
||||
# path, then verify package metadata stayed unchanged.
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile --prefer-frozen-lockfile=false
|
||||
git diff --exit-code package.json pnpm-lock.yaml
|
||||
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
|
||||
- name: Check styled scrollbars
|
||||
run: pnpm check:styled-scrollbars
|
||||
|
||||
- name: Check reliability gate manifest
|
||||
run: pnpm run check:reliability-gates
|
||||
|
||||
# Why: oxlint fails any file over max-lines that is NOT suppressed, so this
|
||||
# ratchet forbids ADDING a new suppression (inline disable or mobile max
|
||||
# bump). Existing oversized files are grandfathered in
|
||||
# config/max-lines-baseline.txt, which may only shrink — new bypasses fail
|
||||
# here with a clear message instead of silently growing the debt.
|
||||
- name: Enforce max-lines ratchet (no new bypasses)
|
||||
run: pnpm run check:max-lines-ratchet
|
||||
|
||||
# Why: project-owned type declarations must live in .ts so tsc
|
||||
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
||||
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
||||
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
|
||||
# past typecheck. See docs/preload-typecheck-hole.md.
|
||||
- name: Guard against project-owned .d.ts in preload/shared
|
||||
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 docs/preload-typecheck-hole.md."
|
||||
echo "Found:"
|
||||
echo "$matches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check feature wall asset budget
|
||||
run: pnpm check:feature-wall-assets
|
||||
|
||||
- name: Verify macOS entitlements
|
||||
run: pnpm verify:macos-entitlements
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
# Why: real old Git diagnostics differ from mocked errors. Keep the
|
||||
# fallback predicates executable across the baseline, transition, and
|
||||
# current command shapes so a newly added flag cannot silently regress.
|
||||
- name: Verify Git binary compatibility matrix
|
||||
run: |
|
||||
archive="$RUNNER_TEMP/git-2.25.5.tar.gz"
|
||||
source="$RUNNER_TEMP/git-2.25.5"
|
||||
curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive"
|
||||
echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \
|
||||
| sha256sum --check
|
||||
mkdir -p "$source"
|
||||
tar -xzf "$archive" -C "$source" --strip-components=1
|
||||
make -C "$source" -j2 NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git
|
||||
ORCA_GIT_COMPAT_BINARY="$source/git" ORCA_GIT_COMPAT_VERSION="2.25.5" \
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
src/shared/git-binary-compatibility.test.ts
|
||||
|
||||
for spec in \
|
||||
"alpine/git:edge-2.38.1|2.38.1" \
|
||||
"alpine/git:v2.49.1|2.49.1"; 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
|
||||
done
|
||||
|
||||
# Why: postinstall rebuilds better-sqlite3 for Electron's ABI via
|
||||
# @electron/rebuild, but vitest runs under system Node.js. Rebuild
|
||||
# it for Node so orchestration tests can load the native module.
|
||||
- name: Rebuild better-sqlite3 for Node
|
||||
run: pnpm rebuild better-sqlite3
|
||||
|
||||
# Why: install intentionally blocks Electron's package postinstall, but
|
||||
# some unit tests import `electron` under Node and require path.txt.
|
||||
- name: Install Electron package binary for tests
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build unpacked app
|
||||
run: pnpm build:unpack
|
||||
|
||||
# Why: the packaged CLI runs outside Electron's asar integration, so
|
||||
# bare runtime imports must be present in the package itself. Run from a
|
||||
# temp copy so Node cannot mask missing package deps with repo node_modules.
|
||||
- name: Smoke packaged CLI
|
||||
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked
|
||||
@@ -0,0 +1,57 @@
|
||||
# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED
|
||||
name: Pullfrog
|
||||
run-name: ${{ inputs.name || github.workflow }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
prompt:
|
||||
type: string
|
||||
description: Agent prompt
|
||||
name:
|
||||
type: string
|
||||
description: Run name
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pullfrog:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Run agent
|
||||
uses: pullfrog/pullfrog@v0
|
||||
with:
|
||||
prompt: ${{ inputs.prompt }}
|
||||
env:
|
||||
# add at least one provider API key
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY:
|
||||
${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
|
||||
# for Amazon Bedrock (https://docs.pullfrog.com/bedrock)
|
||||
# AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# AWS_REGION: us-east-1
|
||||
# BEDROCK_MODEL_ID: <bedrock-model-id>
|
||||
|
||||
# for Google Vertex AI (https://docs.pullfrog.com/vertex)
|
||||
# VERTEX_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_SERVICE_ACCOUNT_JSON }}
|
||||
# GOOGLE_CLOUD_PROJECT: my-project
|
||||
# VERTEX_LOCATION: global
|
||||
# VERTEX_MODEL_ID: <vertex-model-id>
|
||||
@@ -0,0 +1,48 @@
|
||||
name: README Downloads Badge
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 */6 * * *'
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- edited
|
||||
- deleted
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: readme-downloads-badge
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update:
|
||||
# Why: this workflow commits to main, so forks should not create divergent badge commits.
|
||||
if: github.repository == 'stablyai/orca'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Render downloads badge
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: node .github/scripts/render-readme-downloads-badge.mjs
|
||||
|
||||
- name: Commit badge update
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet -- docs/assets/readme-downloads.svg; then
|
||||
echo "Downloads badge is already current."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add docs/assets/readme-downloads.svg
|
||||
git commit -m "Update README downloads badge"
|
||||
git push origin main
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
name: Release macOS Build
|
||||
|
||||
run-name: Mac release build ${{ inputs.tag }} (${{ inputs.release_run_id }})
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag whose draft should receive macOS artifacts
|
||||
required: true
|
||||
type: string
|
||||
release_run_id:
|
||||
description: release-cut workflow run that requested this build
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release-mac-build-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
if: github.repository == 'stablyai/orca'
|
||||
# Why: this workflow is outside the SignPath signing run, so Blacksmith
|
||||
# cannot enter Windows artifact provenance while mac notarization gets the
|
||||
# faster runner.
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
||||
# Cache the Electron binary + electron-builder tool downloads (notarytool,
|
||||
# winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job, incl. mac.
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/electron
|
||||
~/Library/Caches/electron-builder
|
||||
key: electron-builder-mac-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
electron-builder-mac-
|
||||
|
||||
# Why: pnpm install triggers electron's postinstall, which downloads the
|
||||
# Electron binary from GitHub release assets. GitHub's download CDN
|
||||
# occasionally returns 504s that fail the whole release. Retry on
|
||||
# failure so transient network errors don't require a manual re-run.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Verify macOS signing environment
|
||||
run: node config/scripts/verify-macos-release-env.mjs
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# Why: `plutil -lint` accepts duplicate plist keys, but `codesign`
|
||||
# rejects duplicate entitlements after the expensive app build.
|
||||
- name: Verify macOS entitlements
|
||||
run: pnpm verify:macos-entitlements
|
||||
|
||||
# Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`)
|
||||
# requires the build identity to be the literal string `stable` or `rc`,
|
||||
# substituted by electron-vite's `define` block at build time.
|
||||
- name: Classify release tag for telemetry build identity
|
||||
id: tag-classify
|
||||
shell: bash
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why the optional trailing identifier: suffixed side-branch RCs
|
||||
# (vX.Y.Z-rc.N.perf) are rc-channel prerelease builds — same telemetry
|
||||
# identity as plain RCs.
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+(\.[0-9A-Za-z]+)?$ ]]; then
|
||||
identity=rc
|
||||
elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
identity=stable
|
||||
else
|
||||
echo "::error::Tag $TAG does not match stable or rc pattern; refusing to build official artifact"
|
||||
exit 1
|
||||
fi
|
||||
echo "identity=$identity" >>"$GITHUB_OUTPUT"
|
||||
echo "Classified $TAG as $identity"
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build:release
|
||||
env:
|
||||
# Why: Vite's web build crossed Node's default old-space ceiling on
|
||||
# the macOS release runner, leaving v1.4.2-rc.8 as an incomplete draft.
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
ORCA_BUILD_IDENTITY: ${{ steps.tag-classify.outputs.identity }}
|
||||
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
|
||||
ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }}
|
||||
|
||||
- name: Gate runtime file-watcher process isolation
|
||||
run: |
|
||||
# Why: #8212 is a native-process crash contract. Prove both the Node
|
||||
# host and the exact Electron runtime survive SIGSEGV before packaging.
|
||||
node config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
ELECTRON_RUN_AS_NODE=1 pnpm exec electron config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
|
||||
- name: Gate SSH relay watcher process isolation
|
||||
run: |
|
||||
# Why: the remote native watcher shares a daemon with live PTYs.
|
||||
# Kill only its child and require both PTY and watch recovery before packaging.
|
||||
node config/scripts/relay-watcher-fault-harness.mjs
|
||||
|
||||
- name: Publish release artifacts (macOS)
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 30
|
||||
command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Verify release remains draft after artifact upload
|
||||
# Why: the macOS build must never be the actor that exposes a partial
|
||||
# release. If an uploader or GitHub transition flips draft early, fail
|
||||
# this job so release-cut never publishes the release.
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
releases_json="$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100")"
|
||||
# Why: release upload must validate the draft before it is publicly visible.
|
||||
draft="$(jq -e -r --arg tag "$TAG" '
|
||||
map(select(.tag_name == $tag))
|
||||
| if length == 1 and (.[0].draft | type) == "boolean" then (.[0].draft | tostring) else empty end
|
||||
' <<<"$releases_json")" || {
|
||||
echo "::error::Release $TAG was not found in the draft-aware releases list, or its draft state was missing."
|
||||
exit 1
|
||||
}
|
||||
if [[ "$draft" != "true" ]]; then
|
||||
echo "::error::Release $TAG was published during the mac artifact upload."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Why post-publish for macOS: electron-builder packs and uploads in a
|
||||
# single `--publish always` invocation, so there is no cheap insertion
|
||||
# point between pack and upload without splitting those steps.
|
||||
- name: Verify telemetry constants present in app.asar
|
||||
run: node config/scripts/verify-telemetry-constants.mjs
|
||||
@@ -0,0 +1,148 @@
|
||||
name: Terminal Perf
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ref to check out (defaults to the workflow ref)
|
||||
required: false
|
||||
type: string
|
||||
grep:
|
||||
description: Optional Playwright grep for a targeted terminal perf run
|
||||
required: false
|
||||
type: string
|
||||
report_path:
|
||||
description: Path for the generated terminal perf JSON report
|
||||
required: false
|
||||
type: string
|
||||
frame_count:
|
||||
description: Number of prompt redraw frames per typing measurement
|
||||
required: false
|
||||
type: string
|
||||
frame_interval_ms:
|
||||
description: Milliseconds between synthetic OpenCode redraw frames
|
||||
required: false
|
||||
type: string
|
||||
pressure_output_chars:
|
||||
description: Bytes/chars each ACK-backpressured real PTY should emit
|
||||
required: false
|
||||
type: string
|
||||
scale_panes:
|
||||
description: Comma-separated same-workspace pane counts
|
||||
required: false
|
||||
type: string
|
||||
scale_cross_workspace_panes:
|
||||
description: Comma-separated hidden cross-workspace pane counts
|
||||
required: false
|
||||
type: string
|
||||
scale_pressure_panes:
|
||||
description: Comma-separated active real-PTY pressure pane counts
|
||||
required: false
|
||||
type: string
|
||||
scale_hidden_pressure_panes:
|
||||
description: Comma-separated hidden real-PTY pressure pane counts
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
# Why: keep the expensive scale run off PRs, but exercise it daily so
|
||||
# terminal throughput regressions are caught before users report typing lag.
|
||||
- cron: '30 8 * * *'
|
||||
|
||||
jobs:
|
||||
terminal-perf:
|
||||
name: terminal scale perf
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
ORCA_E2E_FORWARD_APP_LOGS: '1'
|
||||
ORCA_E2E_TERMINAL_PERF_REPORT_PATH: ${{ inputs.report_path || 'test-results/terminal-scale-perf-report.json' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Install native build tools and xvfb
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb zsh
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: this scheduled/manual workflow uses the same native install path as
|
||||
# PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Electron app for terminal perf
|
||||
run: npx electron-vite build --mode e2e
|
||||
|
||||
- name: Run terminal scale perf report gate
|
||||
env:
|
||||
ORCA_TERMINAL_PERF_GREP: ${{ inputs.grep }}
|
||||
ORCA_TERMINAL_PERF_FRAME_COUNT: ${{ inputs.frame_count }}
|
||||
ORCA_TERMINAL_PERF_FRAME_INTERVAL_MS: ${{ inputs.frame_interval_ms }}
|
||||
ORCA_TERMINAL_PERF_PRESSURE_OUTPUT_CHARS: ${{ inputs.pressure_output_chars }}
|
||||
ORCA_TERMINAL_PERF_SCALE_PANES: ${{ inputs.scale_panes }}
|
||||
ORCA_TERMINAL_PERF_SCALE_CROSS_WORKSPACE_PANES: ${{ inputs.scale_cross_workspace_panes }}
|
||||
ORCA_TERMINAL_PERF_SCALE_PRESSURE_PANES: ${{ inputs.scale_pressure_panes }}
|
||||
ORCA_TERMINAL_PERF_SCALE_HIDDEN_PRESSURE_PANES: ${{ inputs.scale_hidden_pressure_panes }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=()
|
||||
if [ -n "${ORCA_TERMINAL_PERF_GREP:-}" ]; then
|
||||
args+=(--grep "$ORCA_TERMINAL_PERF_GREP")
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_FRAME_COUNT:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_FRAME_COUNT="$ORCA_TERMINAL_PERF_FRAME_COUNT"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_FRAME_INTERVAL_MS:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_FRAME_INTERVAL_MS="$ORCA_TERMINAL_PERF_FRAME_INTERVAL_MS"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_PRESSURE_OUTPUT_CHARS:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_PRESSURE_OUTPUT_CHARS="$ORCA_TERMINAL_PERF_PRESSURE_OUTPUT_CHARS"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_SCALE_PANES:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_SCALE_PANES="$ORCA_TERMINAL_PERF_SCALE_PANES"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_SCALE_CROSS_WORKSPACE_PANES:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_SCALE_CROSS_WORKSPACE_PANES="$ORCA_TERMINAL_PERF_SCALE_CROSS_WORKSPACE_PANES"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_SCALE_PRESSURE_PANES:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_SCALE_PRESSURE_PANES="$ORCA_TERMINAL_PERF_SCALE_PRESSURE_PANES"
|
||||
fi
|
||||
if [ -n "${ORCA_TERMINAL_PERF_SCALE_HIDDEN_PRESSURE_PANES:-}" ]; then
|
||||
export ORCA_E2E_OPENCODE_SCALE_HIDDEN_PRESSURE_PANES="$ORCA_TERMINAL_PERF_SCALE_HIDDEN_PRESSURE_PANES"
|
||||
fi
|
||||
xvfb-run --auto-servernum env SKIP_BUILD=1 pnpm run test:e2e:terminal-perf:scale:report -- "${args[@]}"
|
||||
|
||||
- name: Upload terminal perf report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: terminal-scale-perf-report
|
||||
path: ${{ env.ORCA_E2E_TERMINAL_PERF_REPORT_PATH }}
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload Playwright traces
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: terminal-scale-perf-traces
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,119 @@
|
||||
name: Track Community PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to backfill or retry'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
track-community-pr:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Generate bufo-bot token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: 2590194
|
||||
private-key: ${{ secrets.BUFO_BOT_PRIVATE_KEY }}
|
||||
owner: stablyai
|
||||
|
||||
- name: Add PR to project
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
PROJECT_OWNER: stablyai
|
||||
PROJECT_NUMBER: '13'
|
||||
INTERNAL_TEAM_SLUG: stably-eng
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const projectOwner = process.env.PROJECT_OWNER;
|
||||
const projectNumber = Number(process.env.PROJECT_NUMBER);
|
||||
const internalTeamSlug = process.env.INTERNAL_TEAM_SLUG;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const prNumber = context.eventName === 'workflow_dispatch'
|
||||
? Number(context.payload.inputs.pr_number)
|
||||
: context.payload.pull_request.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
const author = pr.user.login;
|
||||
|
||||
const skippedAuthors = new Set([
|
||||
'github-actions[bot]',
|
||||
'dependabot[bot]',
|
||||
]);
|
||||
|
||||
if (skippedAuthors.has(author)) {
|
||||
core.info(`Skipping bot PR author ${author}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let isInternalAuthor = false;
|
||||
try {
|
||||
const membership = await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: projectOwner,
|
||||
team_slug: internalTeamSlug,
|
||||
username: author,
|
||||
});
|
||||
isInternalAuthor = membership.data.state === 'active';
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalAuthor) {
|
||||
core.info(`Skipping internal PR author ${author}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectResult = await github.graphql(
|
||||
`
|
||||
query($owner: String!, $number: Int!) {
|
||||
organization(login: $owner) {
|
||||
projectV2(number: $number) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ owner: projectOwner, number: projectNumber },
|
||||
);
|
||||
|
||||
const projectId = projectResult.organization.projectV2.id;
|
||||
|
||||
await github.graphql(
|
||||
`
|
||||
mutation($projectId: ID!, $contentId: ID!) {
|
||||
addProjectV2ItemById(input: {
|
||||
projectId: $projectId,
|
||||
contentId: $contentId
|
||||
}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
projectId,
|
||||
contentId: pr.node_id,
|
||||
},
|
||||
);
|
||||
|
||||
core.info(`Added PR #${pr.number} (${pr.html_url}) to project ${projectOwner}/${projectNumber}.`);
|
||||
@@ -0,0 +1,145 @@
|
||||
name: Windows Update-Survival E2E
|
||||
|
||||
# Why: proves what happens to terminal sessions across a real Windows app update
|
||||
# by installing one released version, updating to another, and asserting on the
|
||||
# packaged artifacts (daemon survival, terminal interactivity, zero console
|
||||
# flashes). This MUST run on a disposable CI Windows: electron-builder's oneClick
|
||||
# installer uninstalls the registry-registered copy of the app before every
|
||||
# install, so running the harness on a machine with a real Orca install would
|
||||
# delete it. A fresh runner has no Orca registered, so it is the only safe home
|
||||
# for install-based testing. Manual dispatch only.
|
||||
|
||||
# Why: workflow_dispatch only works once a workflow is on the default branch.
|
||||
# To exercise this from the unmerged feature branch WITHOUT touching main, a
|
||||
# push trigger scoped to that exact branch runs the workflow from the branch's
|
||||
# own tree. The paths filter keeps it to harness/workflow edits so ordinary
|
||||
# commits don't spend 30 min of Windows runner time. Push runs have no inputs,
|
||||
# so every parameter falls back to a default below.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- Jinwoo-H/windows-update-survival
|
||||
paths:
|
||||
- 'tools/win-update-e2e/**'
|
||||
- '.github/workflows/win-update-e2e.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
from_tag:
|
||||
description: Release tag to install first (e.g. v1.4.124-rc.8)
|
||||
required: true
|
||||
type: string
|
||||
to_tag:
|
||||
description: Release tag to update to (e.g. v1.4.124-rc.9)
|
||||
required: true
|
||||
type: string
|
||||
expect:
|
||||
description: Expected outcome profile
|
||||
required: true
|
||||
type: choice
|
||||
default: cold-restore
|
||||
options:
|
||||
- cold-restore
|
||||
- survival
|
||||
asset_pattern:
|
||||
description: Installer asset glob
|
||||
required: false
|
||||
type: string
|
||||
default: '*windows-setup.exe'
|
||||
soak_seconds:
|
||||
description: Post-relaunch console-window watch duration
|
||||
required: false
|
||||
type: string
|
||||
default: '60'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Why: cancel a superseded run when iterating — a new push to the branch makes
|
||||
# the in-flight Windows job obsolete, so free the runner immediately.
|
||||
concurrency:
|
||||
group: win-update-e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-e2e:
|
||||
name: update ${{ inputs.from_tag || 'v1.4.124-rc.8' }} -> ${{ inputs.to_tag || 'v1.4.124-rc.9' }} (${{ inputs.expect || 'cold-restore' }})
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FROM_TAG: ${{ inputs.from_tag || 'v1.4.124-rc.8' }}
|
||||
TO_TAG: ${{ inputs.to_tag || 'v1.4.124-rc.9' }}
|
||||
EXPECT: ${{ inputs.expect || 'cold-restore' }}
|
||||
ASSET_PATTERN: ${{ inputs.asset_pattern || '*windows-setup.exe' }}
|
||||
SOAK_SECONDS: ${{ inputs.soak_seconds || '60' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# This job only reads the repo and downloads a release; it never pushes.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
# Why: the harness only needs its runtime deps (the Playwright Electron
|
||||
# driver); it drives already-built installer artifacts, so no app build.
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: fail fast with a readable message if a tag has no matching Windows
|
||||
# installer, rather than deep inside the harness.
|
||||
- name: Download installers
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force artifacts/from | Out-Null
|
||||
New-Item -ItemType Directory -Force artifacts/to | Out-Null
|
||||
gh release download "$env:FROM_TAG" --repo "${{ github.repository }}" --pattern "$env:ASSET_PATTERN" --dir artifacts/from
|
||||
gh release download "$env:TO_TAG" --repo "${{ github.repository }}" --pattern "$env:ASSET_PATTERN" --dir artifacts/to
|
||||
$from = Get-ChildItem artifacts/from -Filter *.exe | Select-Object -First 1
|
||||
$to = Get-ChildItem artifacts/to -Filter *.exe | Select-Object -First 1
|
||||
if (-not $from) { throw "No installer matching '$env:ASSET_PATTERN' in $env:FROM_TAG" }
|
||||
if (-not $to) { throw "No installer matching '$env:ASSET_PATTERN' in $env:TO_TAG" }
|
||||
"FROM_EXE=$($from.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"TO_EXE=$($to.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
# Why: no --install-dir here. A CI runner has no real Orca registered, so
|
||||
# the harness installs to the default per-user location and owns it — the
|
||||
# isolated-install machinery exists only to (imperfectly) protect a real
|
||||
# developer install, which does not apply on a throwaway runner.
|
||||
- name: Run update-survival harness
|
||||
id: harness
|
||||
shell: pwsh
|
||||
env:
|
||||
# Why: on a driving failure the harness dumps a screenshot + DOM state
|
||||
# here so the actual post-onboarding UI is visible in the artifacts.
|
||||
ORCA_E2E_DIAG_DIR: artifacts/diag
|
||||
run: |
|
||||
$log = "artifacts/harness-output.log"
|
||||
node tools/win-update-e2e/run.mjs `
|
||||
--from "$env:FROM_EXE" `
|
||||
--to "$env:TO_EXE" `
|
||||
--expect "$env:EXPECT" `
|
||||
--soak-seconds "$env:SOAK_SECONDS" 2>&1 | Tee-Object -FilePath $log
|
||||
exit $LASTEXITCODE
|
||||
|
||||
- name: Upload harness output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: win-update-e2e-output
|
||||
# Why: log + diagnostics only, never the multi-hundred-MB installers.
|
||||
path: |
|
||||
artifacts/harness-output.log
|
||||
artifacts/diag/**
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,116 @@
|
||||
name: Windows Update-Survival E2E (branch build)
|
||||
|
||||
# Why: proves the Phase 1 daemon-host relocation actually makes terminal
|
||||
# sessions SURVIVE a Windows update. Builds an (unsigned) installer FROM THIS
|
||||
# BRANCH, then runs the win-update-e2e harness with --expect survival: install,
|
||||
# open a terminal, silently update over it, relaunch, and assert the daemon PID
|
||||
# is unchanged and the pre-update terminal is still interactive with no console
|
||||
# flashing. Runs from the feature branch via push (workflow_dispatch only works
|
||||
# on the default branch); main is never touched. A CI runner is the only safe
|
||||
# place to install/update — see the relocation post-mortem.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- Jinwoo-H/windows-update-survival
|
||||
paths:
|
||||
- 'src/main/daemon/**'
|
||||
- 'src/main/pty/**'
|
||||
- 'tools/win-update-e2e/**'
|
||||
- 'config/electron-builder.config.cjs'
|
||||
- '.github/workflows/win-update-survival-e2e.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expect:
|
||||
description: Expected outcome profile
|
||||
required: true
|
||||
type: choice
|
||||
default: survival
|
||||
options:
|
||||
- survival
|
||||
- cold-restore
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: win-update-survival-e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
survival:
|
||||
name: survival (branch build over itself)
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
EXPECT: ${{ inputs.expect || 'survival' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# This job only builds and runs the harness locally; it never pushes.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: cache the built installer keyed on the inputs that affect it, so a
|
||||
# harness-only edit skips the ~20 min electron-builder build. The daemon
|
||||
# relocation code lives under src/, so changing it correctly rebuilds.
|
||||
- name: Cache branch installer
|
||||
id: cache-installer
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: dist/orca-windows-setup.exe
|
||||
key: branch-installer-${{ hashFiles('src/**', 'config/**', 'native/**', 'resources/win32/**', 'package.json', 'pnpm-lock.yaml') }}
|
||||
|
||||
- name: Build Windows installer (unsigned)
|
||||
if: steps.cache-installer.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
node config/scripts/ensure-native-runtime.mjs --runtime=electron
|
||||
pnpm run build:desktop
|
||||
pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish never
|
||||
|
||||
# Why: install the branch build, open a terminal, update the SAME build
|
||||
# over it, and assert the relocated daemon survives. Same build on both
|
||||
# sides isolates the survival mechanism (NSIS kill sweep + userData daemon
|
||||
# + adoption) from cross-version staleness policy. No --install-dir: a CI
|
||||
# runner has no real Orca to protect.
|
||||
- name: Run survival harness
|
||||
id: harness
|
||||
shell: pwsh
|
||||
env:
|
||||
ORCA_E2E_DIAG_DIR: artifacts/diag
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force artifacts | Out-Null
|
||||
$exe = "dist/orca-windows-setup.exe"
|
||||
if (-not (Test-Path $exe)) { throw "Installer not found at $exe" }
|
||||
$log = "artifacts/survival-output.log"
|
||||
node tools/win-update-e2e/run.mjs `
|
||||
--from "$exe" `
|
||||
--to "$exe" `
|
||||
--expect "$env:EXPECT" `
|
||||
--soak-seconds 60 2>&1 | Tee-Object -FilePath $log
|
||||
exit $LASTEXITCODE
|
||||
|
||||
- name: Upload survival output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: win-update-survival-output
|
||||
path: |
|
||||
artifacts/survival-output.log
|
||||
artifacts/diag/**
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,360 @@
|
||||
# Windows inner-binary signing rehearsal.
|
||||
#
|
||||
# Why: SignPath cannot deep-sign inside NSIS installers, so shipping signed
|
||||
# inner binaries (Orca.exe, node-pty *.node, DLLs — see issue #7785) requires
|
||||
# a two-request flow: sign the unpacked PE files first, then build the NSIS
|
||||
# installer from the signed tree, then sign the installer. This workflow
|
||||
# rehearses that entire flow from a branch, end to end, without publishing
|
||||
# anything — so the release pipeline on main is never at risk while we verify.
|
||||
#
|
||||
# Runs only via manual dispatch. Use the test-signing policy for iteration
|
||||
# (auto-approved test certificate) and release-signing to rehearse the
|
||||
# production flow (requires manual SignPath approvals). Rehearsed successfully
|
||||
# with test-signing (run 28987534795) and release-signing (run 28988432001).
|
||||
name: Windows signing rehearsal
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
signing-policy-slug:
|
||||
description: SignPath signing policy
|
||||
type: choice
|
||||
default: test-signing
|
||||
options:
|
||||
- test-signing
|
||||
- release-signing
|
||||
inner-artifact-configuration-slug:
|
||||
description: SignPath artifact configuration for the inner-binaries zip
|
||||
type: string
|
||||
default: windows-inner-binaries-zip
|
||||
|
||||
jobs:
|
||||
rehearse:
|
||||
# Why: SignPath origin verification requires GitHub-hosted runners, and
|
||||
# windows-2022 matches the real release job's build environment.
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why: nothing here pushes; keep the token out of .git/config.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~\AppData\Local\electron\Cache
|
||||
~\AppData\Local\electron-builder\Cache
|
||||
key: electron-builder-win-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
electron-builder-win-
|
||||
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: rehearsal builds are never published, so the official-build
|
||||
# secrets (telemetry key, diagnostics URL) are intentionally omitted.
|
||||
- name: Build app
|
||||
run: pnpm build:release
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
- name: Package unpacked Windows app
|
||||
shell: pwsh
|
||||
run: |
|
||||
node config/scripts/ensure-native-runtime.mjs --runtime=electron
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
pnpm exec electron-builder --config config/electron-builder.config.cjs --win --dir --publish never
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
if (-not (Test-Path 'dist/win-unpacked/Orca.exe')) {
|
||||
throw 'electron-builder --dir did not produce dist/win-unpacked/Orca.exe'
|
||||
}
|
||||
|
||||
# Why: only unsigned PE files go to SignPath. Files that already carry a
|
||||
# valid signature (Microsoft's OpenConsole.exe, signed vendor DLLs) must
|
||||
# keep their original signer, so they are excluded from the request.
|
||||
- name: Stage unsigned PE files for inner signing
|
||||
id: stage-inner
|
||||
shell: pwsh
|
||||
run: |
|
||||
$root = Resolve-Path 'dist/win-unpacked'
|
||||
$stage = New-Item -ItemType Directory -Force -Path 'signing-stage'
|
||||
$list = New-Object System.Collections.Generic.List[string]
|
||||
$skipped = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
Get-ChildItem -Path $root -Recurse -File |
|
||||
Where-Object { $_.Extension -in '.exe', '.dll', '.node' } |
|
||||
ForEach-Object {
|
||||
$relative = [System.IO.Path]::GetRelativePath($root, $_.FullName)
|
||||
$signature = Get-AuthenticodeSignature -FilePath $_.FullName
|
||||
if ($signature.Status -eq 'Valid') {
|
||||
$skipped.Add("$relative <already signed: $($signature.SignerCertificate.Subject)>")
|
||||
return
|
||||
}
|
||||
$destination = Join-Path $stage.FullName $relative
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $destination) | Out-Null
|
||||
Copy-Item -Path $_.FullName -Destination $destination -Force
|
||||
$list.Add($relative)
|
||||
}
|
||||
|
||||
if (-not ($list -contains 'Orca.exe')) {
|
||||
throw 'Orca.exe was not staged for signing; unpacked layout changed?'
|
||||
}
|
||||
if (-not ($list | Where-Object { $_ -like '*conpty_console_list.node' })) {
|
||||
throw 'node-pty conpty_console_list.node was not staged; this is the file from issue #7785.'
|
||||
}
|
||||
|
||||
Set-Content -Path 'inner-signing-list.txt' -Value ($list -join "`n")
|
||||
Write-Host "Staged $($list.Count) unsigned PE files for signing:"
|
||||
$list | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host "Skipped $($skipped.Count) already-signed files:"
|
||||
$skipped | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
- name: Upload unsigned inner binaries for SignPath
|
||||
id: upload-unsigned-inner
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: orca-windows-inner-unsigned-${{ github.run_id }}
|
||||
path: signing-stage/**
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install SignPath PowerShell module
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
$useResourceGet = $null -ne (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue)
|
||||
if ($useResourceGet) {
|
||||
if ($null -eq (Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
|
||||
Register-PSResourceRepository -PSGallery -Trusted
|
||||
} else {
|
||||
Set-PSResourceRepository -Name PSGallery -Trusted
|
||||
}
|
||||
Install-PSResource -Name SignPath -Version '[4.0.0,5.0.0)' -Repository PSGallery -Scope CurrentUser -TrustRepository -Reinstall -ErrorAction Stop
|
||||
} else {
|
||||
Install-Module -Name SignPath -Repository PSGallery -MinimumVersion 4.0.0 -MaximumVersion 4.999.999 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
|
||||
}
|
||||
Import-Module SignPath -ErrorAction Stop
|
||||
Get-Command -Name Get-SignedArtifact -Module SignPath -ErrorAction Stop
|
||||
|
||||
- name: Submit inner binaries signing request
|
||||
id: submit-inner-signing
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: c37aa192-a27a-4377-9c90-5d6c95912dc0
|
||||
project-slug: orca
|
||||
signing-policy-slug: ${{ inputs.signing-policy-slug || 'test-signing' }}
|
||||
artifact-configuration-slug: ${{ inputs.inner-artifact-configuration-slug || 'windows-inner-binaries-zip' }}
|
||||
github-artifact-id: ${{ steps.upload-unsigned-inner.outputs.artifact-id }}
|
||||
wait-for-completion: false
|
||||
|
||||
- name: Download signed inner binaries from SignPath
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
SIGNPATH_REQUEST_ID: ${{ steps.submit-inner-signing.outputs.signing-request-id }}
|
||||
run: |
|
||||
Get-SignedArtifact `
|
||||
-ApiToken $env:SIGNPATH_API_TOKEN `
|
||||
-OrganizationId 'c37aa192-a27a-4377-9c90-5d6c95912dc0' `
|
||||
-SigningRequestId $env:SIGNPATH_REQUEST_ID `
|
||||
-OutputArtifactPath signed-inner.zip `
|
||||
-Force `
|
||||
-WaitForCompletionTimeoutInSeconds 3600
|
||||
New-Item -ItemType Directory -Path signed-inner -Force
|
||||
Expand-Archive -Path signed-inner.zip -DestinationPath signed-inner -Force
|
||||
|
||||
# Why: copy back strictly by the staged list so a layout mismatch in the
|
||||
# returned artifact fails loudly instead of silently shipping a mix of
|
||||
# signed and unsigned binaries.
|
||||
- name: Restore signed inner binaries into unpacked app
|
||||
shell: pwsh
|
||||
run: |
|
||||
$root = Resolve-Path 'dist/win-unpacked'
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($relative in Get-Content 'inner-signing-list.txt') {
|
||||
$signed = Get-ChildItem -Path signed-inner -Recurse -File |
|
||||
Where-Object { [System.IO.Path]::GetRelativePath((Resolve-Path 'signed-inner'), $_.FullName).TrimStart('\', '/') -like "*$relative" } |
|
||||
Select-Object -First 1
|
||||
if ($null -eq $signed) {
|
||||
$failures.Add("missing from signed artifact: $relative")
|
||||
continue
|
||||
}
|
||||
$signature = Get-AuthenticodeSignature -FilePath $signed.FullName
|
||||
if ($null -eq $signature.SignerCertificate) {
|
||||
$failures.Add("returned without a signature: $relative")
|
||||
continue
|
||||
}
|
||||
Copy-Item -Path $signed.FullName -Destination (Join-Path $root $relative) -Force
|
||||
Write-Host ("{0,-14} {1} <{2}>" -f $signature.Status, $relative, $signature.SignerCertificate.Subject)
|
||||
}
|
||||
if ($failures.Count -gt 0) {
|
||||
$failures | ForEach-Object { Write-Host "::error::$_" }
|
||||
throw "Signed inner artifact did not round-trip cleanly ($($failures.Count) failures)."
|
||||
}
|
||||
|
||||
- name: Build NSIS installer from signed unpacked app
|
||||
shell: pwsh
|
||||
run: |
|
||||
pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish never --prepackaged "$env:GITHUB_WORKSPACE\dist\win-unpacked"
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
if (-not (Test-Path 'dist/orca-windows-setup.exe')) {
|
||||
throw 'electron-builder --prepackaged did not produce dist/orca-windows-setup.exe'
|
||||
}
|
||||
|
||||
- name: Upload unsigned Windows installer for SignPath
|
||||
id: upload-unsigned-installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: orca-windows-installer-unsigned-${{ github.run_id }}
|
||||
path: dist/orca-windows-setup.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Submit Windows installer signing request
|
||||
id: submit-installer-signing
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: c37aa192-a27a-4377-9c90-5d6c95912dc0
|
||||
project-slug: orca
|
||||
signing-policy-slug: ${{ inputs.signing-policy-slug || 'test-signing' }}
|
||||
artifact-configuration-slug: github-actions-windows-installer
|
||||
github-artifact-id: ${{ steps.upload-unsigned-installer.outputs.artifact-id }}
|
||||
wait-for-completion: false
|
||||
|
||||
- name: Download signed Windows installer from SignPath
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
SIGNPATH_REQUEST_ID: ${{ steps.submit-installer-signing.outputs.signing-request-id }}
|
||||
run: |
|
||||
Get-SignedArtifact `
|
||||
-ApiToken $env:SIGNPATH_API_TOKEN `
|
||||
-OrganizationId 'c37aa192-a27a-4377-9c90-5d6c95912dc0' `
|
||||
-SigningRequestId $env:SIGNPATH_REQUEST_ID `
|
||||
-OutputArtifactPath signed-windows.zip `
|
||||
-Force `
|
||||
-WaitForCompletionTimeoutInSeconds 14400
|
||||
New-Item -ItemType Directory -Path signed-windows -Force
|
||||
Expand-Archive -Path signed-windows.zip -DestinationPath signed-windows -Force
|
||||
|
||||
# Why: signing changes installer bytes, so the updater metadata must be
|
||||
# regenerated exactly like the release job's staging step does.
|
||||
- name: Stage signed Windows installer and regenerate updater metadata
|
||||
shell: pwsh
|
||||
run: |
|
||||
$signedInstaller = Get-ChildItem -Path signed-windows -Recurse -File -Filter 'orca-windows-setup.exe' | Select-Object -First 1
|
||||
if ($null -eq $signedInstaller) {
|
||||
throw 'Signed Windows installer was not returned by SignPath.'
|
||||
}
|
||||
Copy-Item -Path $signedInstaller.FullName -Destination 'dist/orca-windows-setup.exe' -Force
|
||||
& 'node_modules/app-builder-bin/win/x64/app-builder.exe' blockmap --input 'dist/orca-windows-setup.exe' --output 'dist/orca-windows-setup.exe.blockmap'
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
$installer = Get-Item 'dist/orca-windows-setup.exe'
|
||||
$blockmap = Get-Item 'dist/orca-windows-setup.exe.blockmap'
|
||||
$stream = [System.IO.File]::OpenRead($installer.FullName)
|
||||
try {
|
||||
$sha512 = [System.Security.Cryptography.SHA512]::Create()
|
||||
$hash = [Convert]::ToBase64String($sha512.ComputeHash($stream))
|
||||
} finally {
|
||||
if ($null -ne $sha512) { $sha512.Dispose() }
|
||||
$stream.Dispose()
|
||||
}
|
||||
$latestYml = Get-Content -Path 'dist/latest.yml' -Raw
|
||||
$latestYml = [regex]::Replace($latestYml, '(?m)^(\s*)sha512: .+$', {
|
||||
param($match)
|
||||
"$($match.Groups[1].Value)sha512: $hash"
|
||||
})
|
||||
$latestYml = $latestYml -replace '(?m)^ size: \d+$', " size: $($installer.Length)"
|
||||
$latestYml = $latestYml -replace '(?m)^ blockMapSize: \d+$', " blockMapSize: $($blockmap.Length)"
|
||||
Set-Content -Path 'dist/latest.yml' -Value $latestYml -NoNewline
|
||||
|
||||
# Why: this is the pass/fail heart of the rehearsal — the same evidence
|
||||
# check that will later gate releases (see PR #7170). Test certificates
|
||||
# do not chain to a trusted root, so Status=Valid is only required for
|
||||
# release-signing runs; test-signing runs require a present signature.
|
||||
- name: Verify signatures end to end
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ inputs.signing-policy-slug || 'test-signing' }}
|
||||
run: |
|
||||
$report = New-Object System.Collections.Generic.List[string]
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
$requireValid = $env:SIGNING_POLICY -eq 'release-signing'
|
||||
|
||||
function Test-Signature([string]$label, [string]$path) {
|
||||
$signature = Get-AuthenticodeSignature -FilePath $path
|
||||
$subject = if ($null -eq $signature.SignerCertificate) { '<none>' } else { $signature.SignerCertificate.Subject }
|
||||
$line = "{0,-14} {1} <{2}>" -f $signature.Status, $label, $subject
|
||||
$script:report.Add($line)
|
||||
Write-Host $line
|
||||
if ($null -eq $signature.SignerCertificate -or $signature.Status -eq 'NotSigned') {
|
||||
$script:failures.Add("unsigned: $label")
|
||||
} elseif ($script:requireValid -and $signature.Status -ne 'Valid') {
|
||||
$script:failures.Add("not Valid under release-signing: $label ($($signature.Status))")
|
||||
} elseif ($script:requireValid -and $subject -notlike '*CN=SignPath Foundation*') {
|
||||
$script:failures.Add("unexpected signer: $label ($subject)")
|
||||
}
|
||||
}
|
||||
|
||||
Test-Signature 'installer: orca-windows-setup.exe' 'dist/orca-windows-setup.exe'
|
||||
|
||||
# Extract the signed installer and verify the files a user actually
|
||||
# gets on disk — including the exact file from issue #7785.
|
||||
$7za = 'node_modules/7zip-bin/win/x64/7za.exe'
|
||||
New-Item -ItemType Directory -Path extracted-app -Force | Out-Null
|
||||
& $7za x 'dist/orca-windows-setup.exe' '-oextracted-app' -y | Out-Null
|
||||
|
||||
$root = Resolve-Path 'extracted-app'
|
||||
foreach ($relative in Get-Content 'inner-signing-list.txt') {
|
||||
$path = Join-Path $root $relative
|
||||
if (-not (Test-Path $path)) {
|
||||
$failures.Add("missing from installer payload: $relative")
|
||||
continue
|
||||
}
|
||||
Test-Signature "installed: $relative" $path
|
||||
}
|
||||
|
||||
Set-Content -Path 'signing-evidence.txt' -Value ($report -join "`n")
|
||||
if ($failures.Count -gt 0) {
|
||||
$failures | ForEach-Object { Write-Host "::error::$_" }
|
||||
throw "Signing rehearsal failed with $($failures.Count) problems."
|
||||
}
|
||||
Write-Host "All $((Get-Content 'inner-signing-list.txt').Count) inner binaries plus the installer are signed."
|
||||
|
||||
- name: Upload rehearsal evidence and installer
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-signing-rehearsal-${{ inputs.signing-policy-slug || 'test-signing' }}-${{ github.run_id }}
|
||||
path: |
|
||||
signing-evidence.txt
|
||||
inner-signing-list.txt
|
||||
dist/orca-windows-setup.exe
|
||||
dist/orca-windows-setup.exe.blockmap
|
||||
dist/latest.yml
|
||||
retention-days: 7
|
||||
Reference in New Issue
Block a user