mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/i18n-catalog-decoupling
# Conflicts: # .github/workflows/pr.yml # package.json # pnpm-lock.yaml
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
name: Install Node dependencies
|
||||
description: Installs the Node toolchain and repository dependencies for Linux CI jobs.
|
||||
|
||||
inputs:
|
||||
native-runtime:
|
||||
description: Native runtime to prepare after the script-free install (none, node, or electron).
|
||||
required: false
|
||||
default: none
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# setup-node needs pnpm on PATH to locate and restore its store.
|
||||
- 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: Validate native runtime
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
|
||||
run: |
|
||||
case "$NATIVE_RUNTIME" in
|
||||
none|node|electron) ;;
|
||||
*)
|
||||
echo "::error::native-runtime must be none, node, or electron"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# pnpm's bundled gyp_main.py is not executable on fresh Linux runners.
|
||||
- name: Use external node-gyp
|
||||
if: inputs.native-runtime != 'none'
|
||||
shell: bash
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -e node_modules ]; then
|
||||
ls -ld node_modules
|
||||
rm -rf node_modules
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install \
|
||||
--no-frozen-lockfile \
|
||||
--prefer-frozen-lockfile=false \
|
||||
--ignore-scripts
|
||||
git diff --exit-code package.json pnpm-lock.yaml
|
||||
|
||||
- name: Prepare native runtime
|
||||
if: inputs.native-runtime != 'none'
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
|
||||
run: node config/scripts/ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
- '.github/workflows/computer-e2e.yml'
|
||||
- 'config/electron-builder.config.cjs'
|
||||
- 'config/scripts/build-computer-macos.mjs'
|
||||
- 'config/scripts/build-windows-cli-launcher.mjs'
|
||||
- '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.mjs'
|
||||
@@ -22,12 +24,15 @@ on:
|
||||
- 'native/computer-use-macos/**'
|
||||
- 'native/computer-use-linux/**'
|
||||
- 'native/computer-use-windows/**'
|
||||
- 'native/windows-cli-launcher/**'
|
||||
- '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/main/ssh/ssh-remote-cli-launcher.ts'
|
||||
- 'src/main/ssh/ssh-remote-cli-launcher.test.ts'
|
||||
- 'src/shared/computer-use-*.ts'
|
||||
- 'tests/e2e/computer-linux.e2e.ts'
|
||||
- 'tests/e2e/computer-mac.e2e.ts'
|
||||
@@ -70,9 +75,14 @@ jobs:
|
||||
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
|
||||
# Why: without --config, bare vitest ignores config/vitest.config.ts (there
|
||||
# is no root config) and falls back to the 5s default timeout with no
|
||||
# Windows worker cap, so the real csc.exe launcher-compile tests time out
|
||||
# on hosted Windows. Use the shared config so this job matches pnpm test.
|
||||
- run: >-
|
||||
pnpm vitest run
|
||||
pnpm vitest run --config config/vitest.config.ts
|
||||
config/scripts/build-windows-cli-launcher.test.mjs
|
||||
src/main/ssh/ssh-remote-cli-launcher.test.ts
|
||||
config/scripts/computer-e2e-workflow.test.mjs
|
||||
config/scripts/computer-use-skill-guidance.test.mjs
|
||||
config/scripts/computer-use-smoke.test.mjs
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
name: E2E
|
||||
|
||||
run-name: E2E ${{ inputs.ref || github.ref }}
|
||||
|
||||
# Why: checkout + artifact upload only; callers can only further restrict.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -76,7 +82,7 @@ jobs:
|
||||
name: e2e ${{ matrix.shard_name }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -178,7 +184,9 @@ jobs:
|
||||
name: ssh docker watcher isolation
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
# Why 35: the terminal parking + retention specs below add two more
|
||||
# docker-rig tests capped at 240s each on top of the watcher isolation pair.
|
||||
timeout-minutes: 35
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -221,6 +229,13 @@ jobs:
|
||||
- name: Run Docker SSH watcher isolation E2E
|
||||
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-watcher-isolation
|
||||
|
||||
# Why always(): these specs gate the C1 terminal parking/retention budget
|
||||
# over a real SSH host and run in no other lane, so a watcher-isolation
|
||||
# failure above must not silently skip them.
|
||||
- name: Run Docker SSH terminal parking + retention E2E
|
||||
if: always()
|
||||
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-terminal-parking
|
||||
|
||||
- name: Upload watcher isolation traces
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
- 'tests/e2e/fixtures/terminal-emoji-table.md'
|
||||
- 'tests/e2e/helpers/**'
|
||||
- 'tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts'
|
||||
- 'tests/e2e/terminal-webgl-atlas-budget.spec.ts'
|
||||
- 'config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch'
|
||||
- 'src/renderer/src/components/sidebar/SidebarHeader.tsx'
|
||||
- 'src/renderer/src/lib/pane-manager/**'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
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).'
|
||||
description: 'Use the first open iOS patch version after the checked-in version, skipping versions already closed on the App Store.'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
@@ -30,6 +30,11 @@ jobs:
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
@@ -51,11 +56,14 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: npx tsc --noEmit
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Test iOS release version resolution
|
||||
run: ruby fastlane/ios_release_version_test.rb
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
|
||||
+293
-91
@@ -8,79 +8,67 @@ on:
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
static_analysis:
|
||||
name: static analysis
|
||||
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
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- 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
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
|
||||
- name: Enforce focused code-quality plugins
|
||||
run: pnpm run audit:code-quality:native
|
||||
|
||||
- name: Enforce type-aware code-quality baseline
|
||||
run: pnpm run audit:code-quality:type-aware
|
||||
|
||||
- name: Enforce changed-code quality
|
||||
run: pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}"
|
||||
|
||||
- name: Enforce React Doctor on changed lines
|
||||
run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}"
|
||||
|
||||
- name: Check Zustand selector fan-out budget
|
||||
run: pnpm run check:zustand-selector-fanout
|
||||
|
||||
- name: Check reliability gate manifest
|
||||
run: pnpm run check:reliability-gates
|
||||
|
||||
- name: Enforce max-lines ratchet
|
||||
run: pnpm run check:max-lines-ratchet
|
||||
|
||||
- name: Verify bundled skill guides
|
||||
run: pnpm run verify:bundled-skill-guides
|
||||
|
||||
- name: Verify skill freshness manifest
|
||||
run: pnpm run verify:skill-bundle-manifest
|
||||
|
||||
- name: Verify localization catalog
|
||||
run: pnpm run verify:localization-catalog
|
||||
|
||||
# Why: extraction writes sorted evidence to an isolated temporary path,
|
||||
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
|
||||
- name: Verify localization extraction
|
||||
run: pnpm run verify:localization-extraction
|
||||
|
||||
- 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
|
||||
- name: Verify localization coverage
|
||||
run: pnpm run verify:localization-coverage
|
||||
|
||||
# Why: project-owned type declarations must live in .ts so tsc
|
||||
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
||||
@@ -105,55 +93,269 @@ jobs:
|
||||
- name: Verify macOS entitlements
|
||||
run: pnpm verify:macos-entitlements
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- run: pnpm typecheck
|
||||
|
||||
git_compatibility:
|
||||
name: Git compatibility
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
# 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
|
||||
pids=()
|
||||
(
|
||||
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" -j"$(nproc)" \
|
||||
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
|
||||
) &
|
||||
pids+=("$!")
|
||||
|
||||
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
|
||||
(
|
||||
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
|
||||
) &
|
||||
pids+=("$!")
|
||||
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
|
||||
status=0
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || status=1
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
shell_contracts:
|
||||
name: shell contracts
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install zsh
|
||||
run: sudo apt-get update && sudo apt-get install -y zsh
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
|
||||
- name: Test real shell contracts
|
||||
run: |
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
src/main/daemon/shell-ready.test.ts \
|
||||
src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
src/main/providers/local-pty-shell-ready.test.ts \
|
||||
src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
src/shared/posix-command-path-lookup.test.ts
|
||||
|
||||
test:
|
||||
name: tests ${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
|
||||
# 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: Test shard
|
||||
run: |
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
--exclude=src/main/daemon/shell-ready.test.ts \
|
||||
--exclude=src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready.test.ts \
|
||||
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
--exclude=src/shared/posix-command-path-lookup.test.ts \
|
||||
--shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
|
||||
- name: Build unpacked app
|
||||
run: pnpm build:unpack
|
||||
package:
|
||||
name: package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/electron
|
||||
~/.cache/electron-builder
|
||||
key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
electron-builder-linux-
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: electron
|
||||
|
||||
- name: Build package inputs
|
||||
run: |
|
||||
status=0
|
||||
pnpm run build:cli || status=1
|
||||
|
||||
scripts=(build:relay build:electron-vite:parallel)
|
||||
pids=()
|
||||
for script in "${scripts[@]}"; do
|
||||
pnpm run "$script" &
|
||||
pids+=("$!")
|
||||
done
|
||||
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || status=1
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
- name: Project web client from renderer build
|
||||
run: pnpm run build:web-from-renderer
|
||||
|
||||
- name: Build native components
|
||||
run: pnpm run build:native
|
||||
|
||||
- name: Package unpacked app
|
||||
env:
|
||||
ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1'
|
||||
run: pnpm exec electron-builder --config config/electron-builder.config.cjs --dir
|
||||
|
||||
# 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
|
||||
|
||||
# Why: regression specs under tests/e2e/** used to merge green without ever
|
||||
# running — e2e.yml only fired on schedule/release (#10518). Path-filter so
|
||||
# ordinary PRs stay light; any E2E suite change still gets a full shard run.
|
||||
e2e-paths:
|
||||
name: detect e2e path changes
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft != true
|
||||
# Why: detector only needs to read the checkout; do not inherit repo defaults.
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
should_run: ${{ steps.filter.outputs.should_run }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Filter E2E-relevant paths
|
||||
id: filter
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
# Why: capture first so a failed git diff does not look like "no matches"
|
||||
# (pipeline status in `if` is not aborted by set -e). Merge-base limits the
|
||||
# list to files this PR introduced, not base-branch drift.
|
||||
CHANGED="$(git diff --name-only --merge-base "$BASE" "$HEAD")"
|
||||
# Why: tests/playwright.config.ts sits beside tests/e2e/, not inside it, so
|
||||
# it needs its own pattern — a bare `playwright.` prefix matches no tracked
|
||||
# file and would silently skip E2E when the runner config changes.
|
||||
if printf '%s\n' "$CHANGED" | grep -E '^(tests/e2e/|tests/playwright\.|\.github/workflows/e2e\.yml$)' >/dev/null; then
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
echo "E2E path changes detected"
|
||||
else
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No E2E path changes"
|
||||
fi
|
||||
|
||||
e2e:
|
||||
name: e2e
|
||||
needs: e2e-paths
|
||||
if: needs.e2e-paths.outputs.should_run == 'true'
|
||||
# Why: reusable e2e.yml only checkouts, builds, and uploads artifacts.
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
|
||||
verify:
|
||||
if: always()
|
||||
needs:
|
||||
- static_analysis
|
||||
- typecheck
|
||||
- git_compatibility
|
||||
- shell_contracts
|
||||
- test
|
||||
- package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Why: e2e is deliberately absent from needs. The suite is currently red on
|
||||
# main (every scheduled run), so gating merges on it would block any PR that
|
||||
# touches tests/e2e/** — including the ones fixing the suite. Until it is
|
||||
# green the job runs and reports for E2E-path PRs without blocking. To flip
|
||||
# it on: add `e2e` to needs, add E2E to the env below, and require
|
||||
# `"$E2E" = success || skipped` after the loop — skipped is the normal
|
||||
# result for a path-filtered job and must keep passing, so it has to be
|
||||
# checked outside the loop or it would excuse the jobs above.
|
||||
- name: Require successful checks
|
||||
env:
|
||||
STATIC_ANALYSIS: ${{ needs.static_analysis.result }}
|
||||
TYPECHECK: ${{ needs.typecheck.result }}
|
||||
GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }}
|
||||
SHELL_CONTRACTS: ${{ needs.shell_contracts.result }}
|
||||
TEST: ${{ needs.test.result }}
|
||||
PACKAGE: ${{ needs.package.result }}
|
||||
run: |
|
||||
for result in \
|
||||
"$STATIC_ANALYSIS" \
|
||||
"$TYPECHECK" \
|
||||
"$GIT_COMPATIBILITY" \
|
||||
"$SHELL_CONTRACTS" \
|
||||
"$TEST" \
|
||||
"$PACKAGE"; do
|
||||
if [ "$result" != "success" ]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -42,7 +42,12 @@ on:
|
||||
default: false
|
||||
type: boolean
|
||||
version_suffix:
|
||||
description: Extra prerelease identifier appended to an rc version (e.g. "perf" -> 1.2.3-rc.4.perf). rc kind only.
|
||||
description: Extra prerelease identifier appended to an rc version (e.g. "perf" -> 1.2.3-rc.4.perf). Applies to kind=rc, or to an explicit version that is a bare X.Y.Z-rc.N.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
version:
|
||||
description: Exact version to cut (e.g. 1.4.155 or 1.4.155-rc.4), bypassing kind-based computation. Use to leapfrog a deleted/rolled-back stable that regressed the release list. Must be greater than the latest published stable, and an -rc.N must be above the highest RC already cut for its own base.
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
@@ -68,6 +73,28 @@ jobs:
|
||||
should_release: ${{ steps.tag.outputs.tag != '' || steps.version.outputs.recovered_tag != '' }}
|
||||
latest_published_rc_tag: ${{ steps.publish_drafts.outputs.latest_published_tag }}
|
||||
steps:
|
||||
# Why inlined (not m-s-abeer/update-gha-summary-with-workflow-inputs):
|
||||
# this job runs with contents:write and secret scope, so avoid executing
|
||||
# any external (mutable @v1) action here. Surfaces every
|
||||
# workflow_dispatch input as a table for audit; the resolved commit /
|
||||
# branch / tag enrichment is written later in "Resolve ref SHA".
|
||||
# Inputs are passed as JSON via env and parsed by jq as data — never
|
||||
# interpolated into the shell — to avoid injection from dispatch values.
|
||||
- name: Summarize workflow inputs
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
INPUTS_JSON: ${{ toJSON(inputs) }}
|
||||
run: |
|
||||
{
|
||||
echo "## Workflow inputs"
|
||||
echo ""
|
||||
echo "| Input | Value |"
|
||||
echo "| --- | --- |"
|
||||
# Values are data from env JSON; wrap in backticks for readability.
|
||||
# Newlines collapsed so a multi-line input cannot break the table.
|
||||
jq -r '(. // {}) | to_entries[] | "| `\(.key)` | `\(.value | tostring | gsub("\n"; " "))` |"' <<<"$INPUTS_JSON"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Checkout ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -86,8 +113,17 @@ jobs:
|
||||
|
||||
- name: Resolve ref SHA
|
||||
id: resolve
|
||||
env:
|
||||
# Why: keep the caller's ref as data (env) so we can label it in the
|
||||
# summary without shell-interpolating a dispatch-controlled string
|
||||
# into the script body.
|
||||
INPUT_REF: ${{ github.event_name == 'schedule' && 'main' || inputs.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha="$(git rev-parse HEAD)"
|
||||
short_sha="$(git rev-parse --short=12 HEAD)"
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
|
||||
# Why: only push the version-bump commit back to main when the
|
||||
@@ -101,6 +137,97 @@ jobs:
|
||||
echo "push_main=false" >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Always surface the resolved commit in the job summary, plus any
|
||||
# branches/tags that currently point at it (clickable). The raw
|
||||
# `ref` input alone is ambiguous (branch vs tag vs SHA); for SHA
|
||||
# inputs it also hides the human-readable names operators need
|
||||
# when auditing RC cuts.
|
||||
input_ref="${INPUT_REF:-main}"
|
||||
repo_url="${SERVER_URL}/${REPO}"
|
||||
|
||||
branches="$(
|
||||
git for-each-ref --format='%(refname:short)' --points-at="$sha" 'refs/remotes/origin/*' \
|
||||
| sed 's|^origin/||' \
|
||||
| grep -vx 'HEAD' \
|
||||
| sort -u \
|
||||
|| true
|
||||
)"
|
||||
tags="$(
|
||||
git for-each-ref --format='%(refname:short)' --points-at="$sha" 'refs/tags/*' \
|
||||
| sort -u \
|
||||
|| true
|
||||
)"
|
||||
|
||||
# Build comma-separated markdown links. Branch/tag names go in the
|
||||
# URL path as-is (slashes must stay literal for GitHub tree URLs).
|
||||
linkify_names() {
|
||||
local url_kind="$1"
|
||||
local names="$2"
|
||||
if [[ -z "${names//[$'\t\r\n']/}" ]]; then
|
||||
printf '_none_'
|
||||
return
|
||||
fi
|
||||
local first=1
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
local path_name url
|
||||
path_name="${name// /%20}"
|
||||
case "$url_kind" in
|
||||
branch) url="${repo_url}/tree/${path_name}" ;;
|
||||
tag) url="${repo_url}/releases/tag/${path_name}" ;;
|
||||
*) url="${repo_url}" ;;
|
||||
esac
|
||||
if [[ "$first" -eq 1 ]]; then
|
||||
first=0
|
||||
else
|
||||
printf ', '
|
||||
fi
|
||||
printf '[`%s`](%s)' "$name" "$url"
|
||||
done <<<"$names"
|
||||
}
|
||||
|
||||
branch_md="$(linkify_names branch "$branches")"
|
||||
tag_md="$(linkify_names tag "$tags")"
|
||||
|
||||
# When no branch tip matches (historical SHA cuts), fall back to
|
||||
# name-rev so the summary still shows something like `main~3`.
|
||||
contains_md="_none_"
|
||||
if [[ "$branch_md" == "_none_" ]]; then
|
||||
approx="$(git name-rev --name-only --no-undefined --refs='refs/remotes/origin/*' "$sha" 2>/dev/null || true)"
|
||||
if [[ -n "$approx" ]]; then
|
||||
# name-rev prints remotes/origin/<branch>[~N]; strip to branch[~N].
|
||||
approx="${approx#remotes/origin/}"
|
||||
approx="${approx#origin/}"
|
||||
contains_md="\`${approx}\`"
|
||||
fi
|
||||
fi
|
||||
|
||||
input_kind="ref"
|
||||
if git rev-parse -q --verify "refs/remotes/origin/${input_ref}" >/dev/null 2>&1; then
|
||||
input_kind="branch"
|
||||
elif git rev-parse -q --verify "refs/tags/${input_ref}" >/dev/null 2>&1; then
|
||||
input_kind="tag"
|
||||
elif [[ "$input_ref" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
||||
input_kind="sha"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "## Resolved source"
|
||||
echo ""
|
||||
echo "Every cut resolves to a commit. Branch/tag rows list refs whose tip is that commit."
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "| --- | --- |"
|
||||
echo "| Input ref | \`${input_ref}\` (${input_kind}) |"
|
||||
echo "| Commit | [\`${short_sha}\`](${repo_url}/commit/${sha}) |"
|
||||
echo "| Branches at commit | ${branch_md} |"
|
||||
echo "| Tags at commit | ${tag_md} |"
|
||||
if [[ "$branch_md" == "_none_" ]]; then
|
||||
echo "| Also on | ${contains_md} |"
|
||||
fi
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Compute RC slot
|
||||
id: slot
|
||||
run: |
|
||||
@@ -198,6 +325,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KIND: ${{ github.event_name == 'schedule' && 'rc' || inputs.kind }}
|
||||
VERSION_SUFFIX: ${{ github.event_name == 'schedule' && '' || inputs.version_suffix }}
|
||||
EXPLICIT_VERSION: ${{ github.event_name == 'schedule' && '' || inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -260,6 +388,23 @@ jobs:
|
||||
node config/scripts/release-rc-history.mjs "$1"
|
||||
}
|
||||
|
||||
require_valid_version_suffix() {
|
||||
# Why a dot-appended identifier (rc.N.perf): it sorts just
|
||||
# above its own base rc.N but BELOW rc.N+1, so suffixed side-
|
||||
# branch builds never outrank the main RC series and cannot
|
||||
# hijack the update channel; clients find them by matching the
|
||||
# identifier ("perf") in the prerelease components.
|
||||
# Why the numeric alternation rather than plain [0-9A-Za-z]+:
|
||||
# semver forbids a leading zero on an all-digit identifier, and
|
||||
# `npm version` silently renormalizes rc.4.01 to rc.4.1 while the
|
||||
# tag step keeps the literal input — so the shipped package.json
|
||||
# version and its own release tag would name different releases.
|
||||
if [[ ! "$1" =~ ^(0|[1-9][0-9]*|[0-9A-Za-z]*[A-Za-z][0-9A-Za-z]*)$ ]]; then
|
||||
echo "::error::version_suffix (or the trailing .identifier in version) must be alphanumeric with no leading zero on an all-digit identifier, got: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
current_package_stable() {
|
||||
node -e '
|
||||
const { version } = require("./package.json");
|
||||
@@ -336,7 +481,14 @@ jobs:
|
||||
# floor for the current ref so the next cut cannot reuse an older
|
||||
# stable number just because the public release was nuked.
|
||||
if semver_gt "$package_stable" "$latest_stable"; then
|
||||
if [[ "$KIND" != "rc" ]]; then
|
||||
# Skip floor-tag recovery when an explicit version is requested:
|
||||
# recover_unpublished_tag can exit 0, which would recover the
|
||||
# package-floor tag instead of cutting the requested version —
|
||||
# defeating the very rollback scenario the override exists for.
|
||||
# We still raise latest_stable to the floor below so the explicit
|
||||
# version is gated against it; the collision recovery for the
|
||||
# requested tag runs later.
|
||||
if [[ "$KIND" != "rc" && -z "${EXPLICIT_VERSION:-}" ]]; then
|
||||
package_tag="v$package_stable"
|
||||
if git rev-parse "$package_tag" >/dev/null 2>&1; then
|
||||
recover_unpublished_tag "$package_tag" "current ref stable tag is newer than latest published stable" || true
|
||||
@@ -348,6 +500,98 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Explicit version override (manual dispatch only).
|
||||
#
|
||||
# Why: kind-based math derives the next number from the latest
|
||||
# *published* stable. When a shipped stable is deleted (e.g. a
|
||||
# rolled-back 1.4.154), the release list regresses to the prior
|
||||
# stable, so a kind cut recomputes a number at or below the nuked one
|
||||
# and strands every client that already installed the deleted build.
|
||||
# The package.json floor above only recovers this when the deleted
|
||||
# version's bump commit is on the ref being cut, which a hotfix cut
|
||||
# from an older RC ref does not carry. An explicit version lets a
|
||||
# human assert the exact target (e.g. leapfrog to 1.4.155); the
|
||||
# updater-safety gate and tag-collision recovery below still apply.
|
||||
new=""
|
||||
if [[ -n "${EXPLICIT_VERSION:-}" ]]; then
|
||||
explicit="${EXPLICIT_VERSION#v}"
|
||||
# Why the optional trailing identifier: it lets an operator name a
|
||||
# suffixed side-branch RC (X.Y.Z-rc.N.perf) directly, the same shape
|
||||
# the rc path cuts. Note this only ever admits one *above* the
|
||||
# series head — the gate below refuses a suffixed rc at or below it
|
||||
# just like a bare one, so this is a second spelling of
|
||||
# `version=X.Y.Z-rc.N` + `version_suffix`, not a way back into a
|
||||
# series that already shipped.
|
||||
# Why rc.(0|[1-9][0-9]{0,8}): the `-le` below compares with bash's
|
||||
# machine-width integers, so both ends of that range fall *open* on
|
||||
# exactly the RCs this gate must catch. A leading zero (rc.08) is an
|
||||
# invalid octal literal, and the failed test makes the `if` false.
|
||||
# Past INTMAX the literal wraps two's-complement, so whether it
|
||||
# reads as above or below the published rc depends on the value:
|
||||
# rc.99999999999999999999 wraps to 7766279631452241919 and sails
|
||||
# through. The cut then lands a tag that pins highest_rc_for_base
|
||||
# at 1e20 forever, and every later cut wraps to a *lower* rc that
|
||||
# sorts below it, so the fleet never updates again. Nine digits is
|
||||
# far above any real series and exact in bash math either way.
|
||||
if [[ ! "$explicit" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.(0|[1-9][0-9]{0,8})(\.[0-9A-Za-z]+)?)?$ ]]; then
|
||||
echo "::error::version must be X.Y.Z, X.Y.Z-rc.N, or X.Y.Z-rc.N.suffix, got: $EXPLICIT_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Why route the embedded identifier through the same validator the
|
||||
# kind path uses: the regex above only checks shape, and rc.4.01
|
||||
# is a shape-valid identifier that is not valid semver.
|
||||
if [[ "$explicit" == *-rc.*.* ]]; then
|
||||
require_valid_version_suffix "${explicit##*.}"
|
||||
fi
|
||||
# Same updater-safety gate the kind path enforces: stable line must
|
||||
# strictly increase over the latest published stable (prerelease
|
||||
# identifiers ignored for the comparison).
|
||||
if ! semver_gt "$explicit" "$latest_stable"; then
|
||||
echo "::error::Refusing explicit version $explicit: not greater than latest stable $latest_stable." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Why a second gate for prereleases: semver_gt compares through
|
||||
# strip_pre(), so the stable-line check reads 1.4.156-rc.0 as
|
||||
# 1.4.156 and waves it past a 1.4.155 stable even when rc.0..rc.3
|
||||
# already shipped — republishing an RC *below* what clients run,
|
||||
# the same regression class as the rc.4 cut that orphaned live
|
||||
# daemons. Anchor on the same rc history the kind path uses so the
|
||||
# override can only ever advance the series it targets.
|
||||
if [[ "$explicit" == *-rc.* ]]; then
|
||||
explicit_base="${explicit%%-*}"
|
||||
explicit_rc="${explicit#*-rc.}"
|
||||
explicit_rc="${explicit_rc%%.*}"
|
||||
highest_explicit_rc="$(highest_rc_for_base "$explicit_base")"
|
||||
if [[ -n "$highest_explicit_rc" && "$explicit_rc" -le "$highest_explicit_rc" ]]; then
|
||||
# Why the remedy is spelled this narrowly: kind=rc derives its
|
||||
# base from bump(latest_stable, patch), so it can only resume a
|
||||
# series on that base. A minor/major series (1.5.0-rc.N) exists
|
||||
# only because this override created it, and pointing an
|
||||
# operator at kind=rc there would cut an unrelated release.
|
||||
echo "::error::Refusing explicit version $explicit: rc.$explicit_rc is not above rc.$highest_explicit_rc, the highest already cut for $explicit_base. Request rc.$((highest_explicit_rc + 1)) or higher. If you are resuming an unpublished tag and $explicit_base is the next patch after latest stable $latest_stable, dispatch kind=rc instead, which recovers that tag when it was cut from the ref you dispatch; otherwise cut rc.$((highest_explicit_rc + 1)) and leave the unpublished tag alone." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
new="$explicit"
|
||||
# Why here too: the suffix append below lives in the kind path the
|
||||
# override skips, so an operator passing both inputs used to get
|
||||
# their suffix silently dropped. Only a bare rc can take one — a
|
||||
# stable X.Y.Z.perf is not valid semver, and re-suffixing an
|
||||
# already-suffixed rc would produce rc.N.perf.perf.
|
||||
if [[ -n "${VERSION_SUFFIX:-}" ]]; then
|
||||
# Same bounded rc pattern as the shape check above, so the two
|
||||
# cannot drift apart under a later edit.
|
||||
if [[ ! "$explicit" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.(0|[1-9][0-9]{0,8})$ ]]; then
|
||||
echo "::error::version_suffix applies only to a bare X.Y.Z-rc.N version, got: $explicit" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_valid_version_suffix "$VERSION_SUFFIX"
|
||||
new="${new}.${VERSION_SUFFIX}"
|
||||
fi
|
||||
echo "Explicit version override: $new"
|
||||
fi
|
||||
|
||||
if [[ -z "$new" ]]; then
|
||||
case "$KIND" in
|
||||
rc)
|
||||
# Why: RCs always stabilize the *next* patch after whatever
|
||||
@@ -373,15 +617,7 @@ jobs:
|
||||
new="${base}-rc.$((highest_rc + 1))"
|
||||
fi
|
||||
if [[ -n "${VERSION_SUFFIX:-}" ]]; then
|
||||
# Why a dot-appended identifier (rc.N.perf): it sorts just
|
||||
# above its own base rc.N but BELOW rc.N+1, so suffixed side-
|
||||
# branch builds never outrank the main RC series and cannot
|
||||
# hijack the update channel; clients find them by matching the
|
||||
# identifier ("perf") in the prerelease components.
|
||||
if [[ ! "$VERSION_SUFFIX" =~ ^[0-9A-Za-z]+$ ]]; then
|
||||
echo "::error::version_suffix must be alphanumeric, got: $VERSION_SUFFIX" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_valid_version_suffix "$VERSION_SUFFIX"
|
||||
new="${new}.${VERSION_SUFFIX}"
|
||||
fi
|
||||
;;
|
||||
@@ -425,6 +661,7 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Orphan-tag recovery.
|
||||
#
|
||||
@@ -472,7 +709,19 @@ jobs:
|
||||
# message and tag name explicitly (avoids npm's `v1.2.3` prefix
|
||||
# assumptions and any lifecycle scripts that would run on bump).
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
git add package.json
|
||||
# Why: the cut is the only point where committed skill bytes become a
|
||||
# released revision. Without this row the ledger never advances, so the
|
||||
# next skill change rebuilds the revision this tag ships over different
|
||||
# bytes and every install of it stops matching a known snapshot.
|
||||
# --release is provenance-only: it fails if the content-addressed
|
||||
# artifacts do not already match this ref and writes just the mapping
|
||||
# row, so the version commit stays skill-independent. Node built-ins
|
||||
# only, so this needs no install.
|
||||
if ! node config/scripts/generate-skill-bundle-manifest.mjs --release "$VERSION"; then
|
||||
echo "::error::Refusing to record release provenance for v$VERSION: the committed skill artifacts do not match this ref. Land a regeneration on main, then re-run the cut." >&2
|
||||
exit 1
|
||||
fi
|
||||
git add package.json resources/skills/release-mapping.json
|
||||
commit_message="release: v$VERSION"
|
||||
if [[ "$EVENT_NAME" == "schedule" ]]; then
|
||||
commit_message="$commit_message [rc-slot:$SLOT]"
|
||||
@@ -485,6 +734,22 @@ jobs:
|
||||
else
|
||||
git commit -m "$commit_message"
|
||||
fi
|
||||
# Why: a lint that greps this file cannot see a path built from an env
|
||||
# var, a composite action, or concatenation, and `git commit` has forms
|
||||
# (-a, -i, --only, a pathspec) that commit the working tree rather than
|
||||
# the index. Assert what the commit actually carries, so the tag can
|
||||
# only ever ship the version bump and the provenance row, no matter
|
||||
# which step staged what or how the commit was spelled.
|
||||
# -F because the allowlist is literal: unanchored, `.` would match any
|
||||
# character and quietly admit a path like `packageXjson`.
|
||||
# -m --first-parent: plain diff-tree prints NOTHING for a merge commit,
|
||||
# which would make this guard pass silently rather than fail closed.
|
||||
committed="$(git diff-tree --no-commit-id --name-only -r -m --first-parent HEAD |
|
||||
grep -vxF -e 'package.json' -e 'resources/skills/release-mapping.json' || true)"
|
||||
if [[ -n "$committed" ]]; then
|
||||
echo "::error::Release commit carries unexpected paths: $(echo "$committed" | tr '\n' ' ')Only package.json and the skill release-mapping row may ship in a version commit." >&2
|
||||
exit 1
|
||||
fi
|
||||
git tag -a "v$VERSION" -m "v$VERSION"
|
||||
echo "tag=v$VERSION" >>"$GITHUB_OUTPUT"
|
||||
echo "sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
|
||||
@@ -512,7 +777,7 @@ jobs:
|
||||
echo "## Release E2E Signal"
|
||||
echo ""
|
||||
echo "- Terminal rendering golden is release-blocking."
|
||||
echo "- Full E2E is diagnostic/non-blocking release evidence."
|
||||
echo "- Full E2E runs separately after publication and cannot change the release result."
|
||||
echo "- Terminal rendering release evidence is diagnostic/non-blocking."
|
||||
echo ""
|
||||
echo "Publishing behavior is controlled by the existing job dependencies; this summary does not change release gating."
|
||||
@@ -542,15 +807,6 @@ jobs:
|
||||
|
||||
node config/scripts/create-draft-release.mjs "$TAG"
|
||||
|
||||
# Why: tag-scoped E2E gives release visibility, but the suite is flaky enough
|
||||
# that publish-release must not depend on it.
|
||||
e2e:
|
||||
needs: cut
|
||||
if: needs.cut.outputs.should_release == 'true'
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
with:
|
||||
ref: refs/tags/${{ needs.cut.outputs.tag }}
|
||||
|
||||
terminal-rendering-golden:
|
||||
needs: cut
|
||||
if: needs.cut.outputs.should_release == 'true'
|
||||
@@ -1234,7 +1490,6 @@ jobs:
|
||||
Write-Warning 'Restored pre-rebuild installer; this release ships with unsigned inner binaries.'
|
||||
}
|
||||
# ── End Windows inner-binary signing ───────────────────────────────
|
||||
|
||||
- name: Upload unsigned Windows installer for SignPath
|
||||
if: matrix.platform == 'win'
|
||||
id: upload-unsigned-windows-installer
|
||||
@@ -1326,7 +1581,8 @@ jobs:
|
||||
}
|
||||
|
||||
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'
|
||||
node config/scripts/generate-windows-blockmap.mjs 'dist/orca-windows-setup.exe' 'dist/orca-windows-setup.exe.blockmap'
|
||||
if ($LASTEXITCODE -ne 0) { throw "blockmap generation failed with exit code $LASTEXITCODE" }
|
||||
|
||||
$installer = Get-Item 'dist/orca-windows-setup.exe'
|
||||
$blockmap = Get-Item 'dist/orca-windows-setup.exe.blockmap'
|
||||
@@ -1378,8 +1634,41 @@ jobs:
|
||||
INNER_SIGNING_COMPLETED: ${{ steps.rebuild-nsis-signed.outcome == 'success' }}
|
||||
run: |
|
||||
$required = $env:ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED -eq 'true'
|
||||
|
||||
# Why: a fail-open gate that writes nothing is indistinguishable from a
|
||||
# gate that passed. Always leave a verdict in the evidence artifact and
|
||||
# the job summary so a silent degradation is visible (#6487).
|
||||
# Why best-effort: while warn-only, a disk-full or permission error
|
||||
# writing the verdict must not become the thing that fails the release.
|
||||
function Add-GateEvidence([string]$line) {
|
||||
try {
|
||||
Add-Content -Path 'inner-signing-evidence.txt' -Value "`n$line" -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "::warning::Could not append to the inner-signing evidence file: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Add-GateSummary([string]$verdict) {
|
||||
if (-not $env:GITHUB_STEP_SUMMARY) { return }
|
||||
try {
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "**Windows inner-binary signing:** $verdict" -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "::warning::Could not write inner-signing verdict to the job summary: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-GateVerdict([string]$verdict) {
|
||||
try {
|
||||
Set-Content -Path 'inner-signing-evidence.txt' -Value $verdict -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "::warning::Could not persist inner-signing verdict: $_"
|
||||
}
|
||||
Add-GateSummary $verdict
|
||||
}
|
||||
|
||||
if ($env:INNER_SIGNING_COMPLETED -ne 'true') {
|
||||
$message = 'Windows inner-binary signing did not complete; this release ships unsigned inner binaries (fail-open, issue #7785).'
|
||||
Write-GateVerdict "NOT VERIFIED — $message"
|
||||
if ($required) { throw $message }
|
||||
Write-Host "::warning::$message"
|
||||
exit 0
|
||||
@@ -1388,13 +1677,28 @@ jobs:
|
||||
# Why try/catch: while the gate is warn-only, even an unexpected
|
||||
# script error (extraction hiccup, missing file) must not block
|
||||
# the release — only the flip to required makes failures fatal.
|
||||
# Why tracked separately: a required-mode signature failure must not be
|
||||
# rewritten as ERRORED by the catch below, which would replace the
|
||||
# per-file report with an exception string and lose the diagnostics.
|
||||
$policyFailure = $null
|
||||
|
||||
try {
|
||||
$report = New-Object System.Collections.Generic.List[string]
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
# Why: verify the files a user actually gets on disk, not the build
|
||||
# tree — 7z parses the NSIS exe directly as its embedded payload.
|
||||
$7za = 'node_modules/7zip-bin/win/x64/7za.exe'
|
||||
# Resolve 7za via app-builder-lib; electron-builder 26.9+ dropped the
|
||||
# bundled 7zip-bin package the old hardcoded path relied on (#6487).
|
||||
$7zaOutput = node config/scripts/resolve-7za-path.mjs
|
||||
$7zaExitCode = $LASTEXITCODE
|
||||
if ($7zaExitCode -ne 0) {
|
||||
throw "The 7za resolver exited with code $7zaExitCode for the inner-binary evidence gate."
|
||||
}
|
||||
$7za = ($7zaOutput | Out-String).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($7za) -or -not (Test-Path -LiteralPath $7za -PathType Leaf)) {
|
||||
throw "The 7za resolver returned an invalid path for the inner-binary evidence gate: $7za"
|
||||
}
|
||||
New-Item -ItemType Directory -Path inner-evidence-extract -Force | Out-Null
|
||||
& $7za x 'dist/orca-windows-setup.exe' '-oinner-evidence-extract' -y | Out-Null
|
||||
|
||||
@@ -1428,16 +1732,32 @@ jobs:
|
||||
if ($failures.Count -gt 0) {
|
||||
$failures | ForEach-Object { Write-Host "::warning::$_" }
|
||||
$message = "Windows inner-binary evidence gate found $($failures.Count) problems."
|
||||
if ($required) { throw $message }
|
||||
Write-Host "::warning::$message Fail-open until ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED is 'true'."
|
||||
# Why assigned before any I/O: a write that throws here would reach
|
||||
# the catch with $policyFailure still null, so a required-mode
|
||||
# signature failure would be re-reported as ERRORED and the per-file
|
||||
# report overwritten — the exact masking the hoist exists to prevent.
|
||||
if ($required) {
|
||||
$policyFailure = $message
|
||||
} else {
|
||||
Write-Host "::warning::$message Fail-open until ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED is 'true'."
|
||||
}
|
||||
Add-GateEvidence "VERDICT: FAILED — $message"
|
||||
Add-GateSummary "FAILED — $message"
|
||||
} else {
|
||||
Write-Host "All $($targets.Count) inner binaries in the shipped installer are signed by SignPath Foundation."
|
||||
$ok = "All $($targets.Count) inner binaries in the shipped installer are signed by SignPath Foundation."
|
||||
Add-GateEvidence "VERDICT: PASSED — $ok"
|
||||
Add-GateSummary "PASSED — $ok"
|
||||
Write-Host $ok
|
||||
}
|
||||
} catch {
|
||||
Write-GateVerdict "ERRORED — $_"
|
||||
if ($required) { throw }
|
||||
Write-Host "::warning::Windows inner-binary evidence gate errored: $_ (fail-open, issue #7785)."
|
||||
}
|
||||
|
||||
# Outside the catch so the FAILED evidence report survives intact.
|
||||
if ($policyFailure) { throw $policyFailure }
|
||||
|
||||
- name: Upload Windows inner signing evidence
|
||||
if: always() && matrix.platform == 'win'
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1601,6 +1921,32 @@ jobs:
|
||||
--prerelease="$prerelease" \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
|
||||
post-release-e2e:
|
||||
needs:
|
||||
- cut
|
||||
- publish-release
|
||||
if: ${{ needs.cut.outputs.tag != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Dispatch tag-scoped E2E
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.cut.outputs.tag }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
if gh workflow run e2e.yml \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--ref "$TAG" \
|
||||
--raw-field "ref=refs/tags/$TAG"; then
|
||||
echo "Dispatched post-release E2E for $TAG."
|
||||
exit 0
|
||||
fi
|
||||
[[ "$attempt" -eq 3 ]] || sleep "$((attempt * 5))"
|
||||
done
|
||||
echo "::warning::Failed to dispatch post-release E2E for $TAG after 3 attempts."
|
||||
|
||||
homebrew-bump-published-rc-draft:
|
||||
needs:
|
||||
- cut
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Skill update round trip
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'skills/**'
|
||||
- 'resources/skills/**'
|
||||
- 'config/scripts/verify-skill-update-roundtrip.mjs'
|
||||
- 'src/main/skills/skill-freshness-eligibility.ts'
|
||||
- 'src/shared/skill-freshness.ts'
|
||||
- '.github/workflows/skill-update-roundtrip.yml'
|
||||
merge_group:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
roundtrip:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
shape: [symlink, copy]
|
||||
autocrlf: ['false', 'true']
|
||||
skills-cli: ['1.5.17']
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
shape: symlink
|
||||
autocrlf: 'false'
|
||||
skills-cli: latest
|
||||
continue-on-error: ${{ matrix.skills-cli == 'latest' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
- name: Verify targeted update convergence and copy behavior
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
SKILL_UPDATE_SOURCE: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
SKILL_UPDATE_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: >-
|
||||
node config/scripts/verify-skill-update-roundtrip.mjs
|
||||
--cli=${{ matrix.skills-cli }}
|
||||
--autocrlf=${{ matrix.autocrlf }}
|
||||
--shape=${{ matrix.shape }}
|
||||
@@ -0,0 +1,199 @@
|
||||
name: Windows Crash-Survival E2E
|
||||
|
||||
# Why: proves the detached daemon-host relocation makes terminal sessions SURVIVE
|
||||
# a CRASH of Orca's main process (GitHub #7742), the companion guarantee to the
|
||||
# update-survival harness. Builds an (unsigned) installer FROM THIS BRANCH,
|
||||
# silent-installs it, opens a terminal, force-kills ONLY the app main (no
|
||||
# tree-kill), and asserts the daemon + shell stay alive, a relaunch adopts the
|
||||
# same daemon and shell, and post-crash input causes no pwsh FailFast. Targeted
|
||||
# pull requests keep it as a durable regression gate. A CI runner is the only
|
||||
# safe place to install — see the relocation post-mortem.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths:
|
||||
- '.github/workflows/win-crash-survival-e2e.yml'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'electron.vite.config.ts'
|
||||
- 'build-plugins/**'
|
||||
- 'config/electron-builder.config.cjs'
|
||||
- 'config/patches/**'
|
||||
- 'config/scripts/ensure-native-runtime.mjs'
|
||||
- 'config/scripts/rebuild-native-deps.mjs'
|
||||
- 'config/scripts/verify-packaged-plugin-resources.cjs'
|
||||
- 'native/**'
|
||||
# Byte-hashed at package time, and CRLF-sensitive on Windows.
|
||||
- '.gitattributes'
|
||||
- 'resources/plugins/**'
|
||||
- 'resources/win32/**'
|
||||
- 'src/main/daemon/**'
|
||||
- 'src/main/index.ts'
|
||||
- 'src/main/ipc/pty*.ts'
|
||||
- 'src/main/persistence.ts'
|
||||
- 'src/main/providers/**'
|
||||
- 'src/main/pty/**'
|
||||
- 'src/main/startup/first-window-startup-services.ts'
|
||||
- 'src/main/window/attach-main-window-services.ts'
|
||||
- 'src/preload/**'
|
||||
- 'src/renderer/src/App.tsx'
|
||||
- 'src/renderer/src/components/terminal-pane/**'
|
||||
- 'src/renderer/src/hooks/useIpcEvents.ts'
|
||||
- 'src/renderer/src/lib/pane-manager/**'
|
||||
- 'src/renderer/src/lib/session-write-subscriber.ts'
|
||||
- 'src/renderer/src/lib/workspace-session-host-persistence.ts'
|
||||
- 'src/renderer/src/store/slices/terminals.ts'
|
||||
- 'src/shared/pty-session-id-format.ts'
|
||||
- 'tools/win-crash-survival-e2e/**'
|
||||
- 'tools/win-update-e2e/**'
|
||||
# Why: source tests and benchmarks do not change the packaged artifact;
|
||||
# their own verify jobs cover them without spending a Windows build slot.
|
||||
- '!src/**/*.test.*'
|
||||
- '!src/**/*.bench.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expect:
|
||||
description: Expected outcome profile
|
||||
required: true
|
||||
type: choice
|
||||
default: survival
|
||||
options:
|
||||
- survival
|
||||
- orphaned
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: win-crash-survival-e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
crash-survival:
|
||||
name: crash-survival (packaged build)
|
||||
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
|
||||
|
||||
# Why: setup-node can restore pnpm's content-addressed store only after
|
||||
# the pnpm binary exists, avoiding repeat dependency downloads per run.
|
||||
- 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: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why: cache the built installer by production build inputs, excluding
|
||||
# tests/reliability metadata so harness-only edits skip electron-builder.
|
||||
# The daemon relocation lives under src/, so product changes still rebuild.
|
||||
- name: Cache branch installer
|
||||
id: cache-installer
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: dist/orca-windows-setup.exe
|
||||
key: >-
|
||||
crash-survival-installer-${{ hashFiles(
|
||||
'src/**',
|
||||
'!src/**/*.test.*',
|
||||
'!src/**/*.bench.*',
|
||||
'config/**',
|
||||
'!config/**/*.test.*',
|
||||
'!config/reliability-gates.jsonc',
|
||||
'!config/max-lines-baseline.txt',
|
||||
'!config/vitest.config.ts',
|
||||
'build-plugins/**',
|
||||
'native/**',
|
||||
'resources/**',
|
||||
'electron.vite.config.ts',
|
||||
'tsconfig.json',
|
||||
'vite.web.config.ts',
|
||||
'.npmrc',
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'pnpm-workspace.yaml'
|
||||
) }}
|
||||
|
||||
# Why: production edits miss the installer cache by design, but Electron
|
||||
# and NSIS downloads are lockfile-owned and need not be fetched again.
|
||||
- name: Cache electron-builder downloads
|
||||
if: steps.cache-installer.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~\AppData\Local\electron\Cache
|
||||
~\AppData\Local\electron-builder\Cache
|
||||
key: crash-survival-electron-builder-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
crash-survival-electron-builder-
|
||||
|
||||
- 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: silent-install the branch build so the crash harness has a packaged
|
||||
# Orca.exe to drive. A clean CI runner installs to the default per-user
|
||||
# location (%LOCALAPPDATA%\Programs\Orca), which the harness auto-locates.
|
||||
- name: Silent-install branch build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$exe = "dist/orca-windows-setup.exe"
|
||||
if (-not (Test-Path $exe)) { throw "Installer not found at $exe" }
|
||||
Start-Process -FilePath $exe -ArgumentList '/S' -Wait
|
||||
$installed = Join-Path $env:LOCALAPPDATA 'Programs/Orca/Orca.exe'
|
||||
if (-not (Test-Path $installed)) {
|
||||
$installed = Join-Path $env:LOCALAPPDATA 'Programs/orca/Orca.exe'
|
||||
}
|
||||
if (-not (Test-Path $installed)) { throw "Installed Orca.exe not found after silent install" }
|
||||
"ORCA_EXE=$installed" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
# Why: crash ONLY the app main and assert the relocated daemon + its shell
|
||||
# survive, then that relaunch adopts them and input causes no pwsh FailFast.
|
||||
- name: Run crash-survival harness
|
||||
id: harness
|
||||
shell: pwsh
|
||||
env:
|
||||
ORCA_E2E_DIAG_DIR: artifacts/diag
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force artifacts | Out-Null
|
||||
$log = "artifacts/crash-survival-output.log"
|
||||
node tools/win-crash-survival-e2e/run.mjs `
|
||||
--expect "$env:EXPECT" `
|
||||
--exe-path "$env:ORCA_EXE" `
|
||||
--soak-seconds 8 2>&1 | Tee-Object -FilePath $log
|
||||
exit $LASTEXITCODE
|
||||
|
||||
- name: Upload crash-survival output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: win-crash-survival-output
|
||||
path: |
|
||||
artifacts/crash-survival-output.log
|
||||
artifacts/diag/**
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -128,6 +128,7 @@ jobs:
|
||||
node tools/win-update-e2e/run.mjs `
|
||||
--from "$env:FROM_EXE" `
|
||||
--to "$env:TO_EXE" `
|
||||
--require-distinct-artifacts `
|
||||
--expect "$env:EXPECT" `
|
||||
--soak-seconds "$env:SOAK_SECONDS" 2>&1 | Tee-Object -FilePath $log
|
||||
exit $LASTEXITCODE
|
||||
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
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'
|
||||
node config/scripts/generate-windows-blockmap.mjs 'dist/orca-windows-setup.exe' 'dist/orca-windows-setup.exe.blockmap'
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
$installer = Get-Item 'dist/orca-windows-setup.exe'
|
||||
@@ -325,7 +325,16 @@ jobs:
|
||||
|
||||
# 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'
|
||||
# electron-builder 26.9+ removed the bundled 7zip-bin package (#6487).
|
||||
$7zaOutput = node config/scripts/resolve-7za-path.mjs
|
||||
$7zaExitCode = $LASTEXITCODE
|
||||
if ($7zaExitCode -ne 0) {
|
||||
throw "The 7za resolver exited with code $7zaExitCode for the signing rehearsal."
|
||||
}
|
||||
$7za = ($7zaOutput | Out-String).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($7za) -or -not (Test-Path -LiteralPath $7za -PathType Leaf)) {
|
||||
throw "The 7za resolver returned an invalid path for the signing rehearsal: $7za"
|
||||
}
|
||||
New-Item -ItemType Directory -Path extracted-app -Force | Out-Null
|
||||
& $7za x 'dist/orca-windows-setup.exe' '-oextracted-app' -y | Out-Null
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Windows terminal restart E2E
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths:
|
||||
- '.github/workflows/windows-terminal-restart-e2e.yml'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'electron.vite.config.ts'
|
||||
- 'config/patches/**'
|
||||
- 'config/scripts/ensure-native-runtime.mjs'
|
||||
- 'config/scripts/rebuild-native-deps.mjs'
|
||||
- 'tests/playwright.config.ts'
|
||||
- 'tests/e2e/global-setup.ts'
|
||||
- 'tests/e2e/global-teardown.ts'
|
||||
- 'tests/e2e/helpers/**'
|
||||
- 'tests/e2e/restart-restore-terminal-input.spec.ts'
|
||||
- 'tests/e2e/restored-terminal-input-readiness.unit.test.ts'
|
||||
- 'tests/e2e/terminal-probe-input-sequence.ts'
|
||||
- 'tests/e2e/terminal-probe-input-sequence.unit.test.ts'
|
||||
- 'tests/e2e/terminal-restart-persistence.spec.ts'
|
||||
- 'src/main/daemon/**'
|
||||
- 'src/main/ipc/pty*.ts'
|
||||
- 'src/main/providers/**'
|
||||
- 'src/main/pty/**'
|
||||
- 'src/main/pty-descendant-termination.ts'
|
||||
- 'src/main/windows-process-tree-kill.ts'
|
||||
- 'src/main/windows-pty-root-identity.ts'
|
||||
- 'src/preload/**'
|
||||
- 'src/renderer/src/components/terminal-pane/**'
|
||||
- 'src/renderer/src/lib/pane-manager/**'
|
||||
- 'src/shared/process-table-snapshot.ts'
|
||||
- 'src/shared/pty-session-id-format.ts'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ref to check out
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: windows-terminal-restart-e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
windows-terminal-restart:
|
||||
name: Windows terminal restart regressions
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
# Why: pnpm must exist before setup-node resolves its dependency cache.
|
||||
- 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: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Electron app for Windows E2E
|
||||
run: pnpm exec electron-vite build --mode e2e
|
||||
|
||||
- name: Run Windows terminal restart regressions
|
||||
env:
|
||||
SKIP_BUILD: '1'
|
||||
ORCA_E2E_FORWARD_APP_LOGS: '1'
|
||||
ORCA_REQUIRE_WINDOWS_TERMINAL_RESTART_E2E: '1'
|
||||
# Why: pnpm forwards a literal `--` to Playwright, which makes the
|
||||
# grep and worker flags positional filters instead of CLI options.
|
||||
run: >-
|
||||
pnpm run test:e2e
|
||||
tests/e2e/restart-restore-terminal-input.spec.ts
|
||||
tests/e2e/terminal-restart-persistence.spec.ts
|
||||
--grep "clean restart with a live daemon session|cold-restored pane accepts typing|daemon snapshot relaunch preserves"
|
||||
--workers=1
|
||||
|
||||
- name: Upload Playwright traces
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-terminal-restart-playwright-traces
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
Reference in New Issue
Block a user