mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(shell): repair worktree HISTFILE in plain zsh panes via one positive feature channel
A plain zsh pane — no startup command, no agent overlay — was never wrapped, so
Orca's HISTFILE repair never ran in it. macOS `/etc/zshrc` assigns
`HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history` with no check-before-set and runs
before any file Orca controls, so per-worktree history was a silent no-op for
every ordinary pane on the primary platform.
Wrapping those panes needs a way to say which wrapper features a shell should
turn on. That channel is one exported variable, ORCA_SHELL_FEATURES, carrying a
comma-separated positive allowlist from a closed set (history, markers, ready,
identity, overlay). The wrapper .zshenv reads it into a plain, non-exported
array and unsets it in its first executable lines, before the user's own
.zshenv — so the selection survives .zshenv -> .zprofile -> .zshrc -> .zlogin in
this process but physically cannot reach a child. There is no negative or
suppression variable anywhere; an absent or inherited value can only ever mean
fewer features. ORCA_HISTFILE is consumed and destroyed the same way, which
removes the root cause of #11146 instead of patching it.
All order-sensitive wrapper work now lives in one `__orca_shell_epilogue`
defined in .zshenv and invoked exactly once, from .zshrc for a non-login shell
and .zlogin for a login shell, with each feature an independent guard.
Selection is a pure function of spawn env and launch intent, so a pane wrapped
only for history gets no OSC 133 and is observably identical to the unwrapped
pane it used to be.
Generation is now fail-closed: wrapper files are written to a temp name and
renamed, every required path is verified non-empty, and ZDOTDIR is only set when
that holds. Previously a failed write still pointed ZDOTDIR at an empty dir and
the user silently lost their entire zsh config.
Orca also recognised only its own `*/shell-ready/zsh` dir shape when deciding
what the user's ZDOTDIR was, so being launched from any other terminal that had
hijacked ZDOTDIR captured that as the user's config dir. Ownership is now
established positively — a stamped marker file, or Orca's own dir shape for
wrappers written by older builds — and an inherited ZDOTDIR holding no zsh
startup file is ignored. No vendor is detected by name.
* fix(shell): make the zsh epilogue option-proof and stop history widening relay wrapping
Review follow-ups on the feature-channel PR.
- `emulate -L zsh` as the epilogue's first statement. It runs after the user's
own config, so `setopt no_unset` made the precmd_functions append a fatal
error that returned from the whole function (no ready widget, ZDOTDIR left at
Orca's wrapper dir), and `setopt ksh_arrays` made the 1-based feature
subscript drop whichever feature is listed first.
- The `/etc/zshrc` HISTFILE repair is no longer behind the `history` guard: it
undoes damage Orca's own ZDOTDIR caused, so it must also run for a shell that
re-enters the wrapper after the allowlist was consumed.
- The relay keeps its own wrapping gate. Its .zshenv resolves the user's config
dir from a ZDOTDIR Orca has already overwritten, so wrapping a remote pane
just for `history` cost a relocated-ZDOTDIR user their whole shell config.
- A failed primary spawn no longer leaks the primary shell's launch env
(wrapper ZDOTDIR + feature channel) into an unwrapped fallback pane.
* fix(shell): drop the relay wrapping gate and stop HISTFILE inheriting across Orca instances
The relay-specific gate added last round rested on a false premise:
main's hasOverlayRestoreEnv already included ORCA_REMOTE_CLI_BIN_DIR, and
ssh-pty-spawn-env sets that on every SSH pane whose session has a CLI
bridge — so ordinary remote zsh panes were already wrapped. The gate only
bit where remoteCliBridgeEnv is null (a host too old to report its
platform), where it silently dropped that pane's worktree history. All
three transports now share the features.length rule.
HISTFILE stays exported, so a newly wrapped pane handed the worktree
history path to every child, including a nested Orca whose panes then all
hit injectHistoryEnv's check-before-set and appended into the launching
worktree's file. Same class as the fish_history fix in #15195: recognise a
path Orca minted and drop it before the check, on the desktop, daemon and
relay injection paths and both history-disabled branches.
Also: run the epilogue from the wrapper .zshrc when zsh is in sh/ksh
emulation, since sourcehome() then reads $HOME/.zlogin and the wrapper's
.zlogin never runs; track fallback launch-env keys per attempt rather than
once from the primary; and guard the cross-file epilogue call so a wrapper
dir shared by two builds degrades quietly.
* fix(shell): make every wrapper file self-sufficient and retire the deleted marker vars from tests
- .zprofile/.zshrc/.zlogin each define __orca_resolve_user_config_dir. They
called it on line 2 while only .zshenv defined it, so a wrapper dir written by
two concurrently installed builds printed three "command not found" and
skipped the user's entire zsh config. New live-shell test covers it.
- Retarget every remaining ORCA_SHELL_READY_MARKER/ORCA_SHELL_STARTUP_IDENTITY
reference onto ORCA_SHELL_FEATURES, or delete it where the key is now dead.
- isOrcaMintedHistFile requires a leading '/', so a relative path of the same
shape stays the user's.
- Drop an unused no-control-regex disable, and register the two real-zsh suites
in the dedicated shell-contracts lane.
* fix(shell): stop the zsh wrapper colliding on REPLY and degrade under sh emulation
Widening wrapping from overlay/startup panes to every zsh pane turned three
latent wrapper defects into user-visible ones.
- The config-dir resolver used `REPLY`, zsh's shared scratch global, as its
out-parameter. `typeset -r REPLY` in a user config made the wrapper's first
executable assignment fatal, `typeset -i REPLY` silently resolved every path
to 0; both left HISTFILE inside Orca's wrapper dir. It now writes an
Orca-private `_orca_resolved_config_dir`, declared `typeset -g` so the
rename introduces no `warn_create_global` noise. A new rule test fails on any
generated wrapper file that writes a global outside Orca's namespace.
- The daemon dropped an inherited HISTFILE but never an inherited
ORCA_HISTFILE, which now both wraps a pane the client scoped nothing for and
re-exports another worktree's history path. The relay had the same gap on its
isolation-off and revive paths. Both now mirror the desktop.
- A user .zshenv or .zprofile ending in `emulate sh` makes zsh ignore ZDOTDIR,
so no later wrapper file is read and the epilogue never runs. Nothing can
repair HISTFILE from there, so the wrapper now detects the emulation and
hands the pane back unwrapped instead of leaving history somewhere invisible.
Also `typeset -g __orca_in_command` so the OSC 133 preexec hook prints no
warning under `setopt warn_create_global`.
699 lines
29 KiB
YAML
699 lines
29 KiB
YAML
name: PR Checks
|
|
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- opened
|
|
- synchronize
|
|
- reopened
|
|
- ready_for_review
|
|
|
|
concurrency:
|
|
group: pr-checks-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
static_analysis:
|
|
name: static analysis
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Why blob:none: full history is needed for the merge-base diff, but historical
|
|
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
|
# few this job actually reads on demand.
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
- 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: Check VM runtime rollback compatibility
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if git diff --quiet --merge-base "$BASE_SHA" "$HEAD_SHA" -- \
|
|
src/shared/ephemeral-vm-runtime-store.ts \
|
|
src/shared/ephemeral-vm-runtime-feature-store.ts \
|
|
src/shared/ephemeral-vm-runtime-rollback-projection.ts \
|
|
src/shared/ephemeral-vm-runtimes.ts \
|
|
src/shared/ephemeral-vm-recipes.ts \
|
|
src/shared/orca-yaml-hook-types.ts \
|
|
src/main/ephemeral-vm-runtime-service.ts \
|
|
src/main/ephemeral-vm-runtime-provisioning-persistence.ts \
|
|
src/main/ephemeral-vm-failed-start-cleanup.ts; then
|
|
echo "VM runtime persistence is unchanged."
|
|
exit 0
|
|
fi
|
|
node config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \
|
|
config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts
|
|
|
|
- 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: 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
|
|
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
|
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
|
|
# past typecheck. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts.
|
|
- name: Guard against project-owned .d.ts in preload/shared
|
|
run: |
|
|
matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true)
|
|
if [ -n "$matches" ]; then
|
|
echo "::error::Project-owned .d.ts files are not allowed under src/preload or src/shared."
|
|
echo "Move type declarations into a .ts file so skipLibCheck does not hide errors."
|
|
echo "See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts."
|
|
echo "Found:"
|
|
echo "$matches"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Check feature wall asset budget
|
|
run: pnpm check:feature-wall-assets
|
|
|
|
- name: Verify macOS entitlements
|
|
run: pnpm verify:macos-entitlements
|
|
|
|
root_directory_guard:
|
|
name: root directory guard
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Why blob:none: full history is needed for the merge-base diff, but historical
|
|
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
|
# few this job actually reads on demand.
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
persist-credentials: false
|
|
|
|
- name: Reject new root-level files and folders
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: node .github/scripts/check-root-directory-entries.mjs "$BASE_SHA" "$HEAD_SHA"
|
|
|
|
typecheck:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
# Why: every project is `composite`, so tsc already writes a .tsbuildinfo that lets
|
|
# the next run skip unchanged files. CI threw it away each time. The key is per-SHA
|
|
# so each run saves its own; restore-keys inherit the newest prior graph to diff against.
|
|
- name: Cache TypeScript incremental state
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: config/*.tsbuildinfo
|
|
key: tsbuildinfo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'config/tsconfig*.json') }}-${{ github.sha }}
|
|
restore-keys: |
|
|
tsbuildinfo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'config/tsconfig*.json') }}-
|
|
tsbuildinfo-${{ runner.os }}-
|
|
|
|
- run: pnpm run 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: the 2.25.5 lane is a source build of a pinned tarball, so it produced the
|
|
# same binary on every PR for minutes of runner time. The key carries the version
|
|
# because that is the only input; the sha256 assertion below still guards the
|
|
# tarball on the miss path that actually builds.
|
|
- name: Cache baseline Git build
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.cache/orca-git-compat/git-2.25.5
|
|
key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5
|
|
|
|
- name: Verify Git binary compatibility matrix
|
|
run: |
|
|
pids=()
|
|
(
|
|
archive="$RUNNER_TEMP/git-2.25.5.tar.gz"
|
|
source="$HOME/.cache/orca-git-compat/git-2.25.5"
|
|
if [ ! -x "$source/git" ]; then
|
|
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
|
|
# Why: the linked binaries are what the next run needs; the objects that
|
|
# produced them are most of the tree and would bloat the cache entry.
|
|
find "$source" -name '*.o' -delete
|
|
fi
|
|
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
|
|
) &
|
|
pids+=("$!")
|
|
done
|
|
|
|
status=0
|
|
for pid in "${pids[@]}"; do
|
|
wait "$pid" || status=1
|
|
done
|
|
exit "$status"
|
|
|
|
xterm_patch_sync:
|
|
name: xterm patch sync
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
# Why: the check rebuilds xterm.js from a pinned upstream commit. Caching the
|
|
# npm metadata and the shallow clone turns a ~4 min cold run into well under a
|
|
# minute; the key is the manifest, so a commit or toolchain bump invalidates it.
|
|
- name: Restore upstream xterm build inputs
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
~/.npm
|
|
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
|
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
|
|
|
|
- name: Verify xterm patches match the pinned upstream build
|
|
env:
|
|
WORK_DIR: ${{ runner.temp }}/xterm-patch-build
|
|
run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR"
|
|
|
|
shell_contracts:
|
|
name: shell contracts
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# Why: the suites below gate their live fish tests on the binary, which is
|
|
# right on a developer machine and wrong here — this job is a required check
|
|
# and its fish lane is the only end-to-end guard for #9993, so a skip would
|
|
# report green with nothing exercised. Turns those skips into failures.
|
|
ORCA_REQUIRE_FISH: '1'
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Why fish: shell-ready.test.ts gates its live fish test on the binary being
|
|
# present, so without this the fish barrier is only covered by config-shape
|
|
# assertions and never actually exercised.
|
|
# Why release-4: DECSET 2031 arming lives in the fish 4.0 Rust tty_handoff, and
|
|
# fish-color-scheme-child-stdin.node-pty.test.ts (#9993) needs it. Noble ships
|
|
# 3.7, so the PPA is what makes that lane real.
|
|
- name: Install zsh and fish
|
|
run: |
|
|
# Why the update/PPA/fish steps are tolerant: a repo the runner image already
|
|
# ships can lack a Release file for this suite, and a failed add-apt-repository
|
|
# still leaves its list entry behind — either makes `apt-get update` exit
|
|
# non-zero and would red this required check over something unrelated to the
|
|
# PR. Every fish outcome is judged by the version gate below instead, so only
|
|
# the zsh install (which has no such gate) stays fatal here.
|
|
# Why retry only here: adding the PPA is the network-flaky step, and the
|
|
# version gate below is fatal, so a transient Launchpad blip would
|
|
# otherwise red a required check on PRs unrelated to shells.
|
|
# Why -n: add-apt-repository refreshes every configured repo on its own. With
|
|
# an update on each side of it this step refreshed them three times over, and
|
|
# the Azure archive mirror alone costs ~15-30s a pass. The PPA index is the
|
|
# only thing the repo list gains here, and the single update below fetches it.
|
|
for attempt in 1 2 3; do
|
|
sudo add-apt-repository -y -n ppa:fish-shell/release-4 && break
|
|
echo "add-apt-repository attempt ${attempt} failed; retrying" >&2
|
|
sudo add-apt-repository -y -n -r ppa:fish-shell/release-4 || true
|
|
sleep 5
|
|
done
|
|
sudo apt-get update || true
|
|
# Why both shells on one line: pr-workflow-parallelism.test.mjs parses only the
|
|
# first install command in this step to prove the lane really installs them.
|
|
sudo apt-get install -y zsh fish
|
|
|
|
# Separate from the install so the failure names the contract, not an apt error.
|
|
# ORCA_REQUIRE_FISH re-checks this at test time; this step just fails in seconds
|
|
# instead of after a full dependency install.
|
|
- name: Require fish 4+
|
|
run: |
|
|
version="$(fish --version 2>/dev/null || true)"
|
|
major="${version##*version }"
|
|
major="${major%%.*}"
|
|
case "$major" in '' | *[!0-9]*) major=0 ;; esac
|
|
echo "${version:-<fish not installed>}"
|
|
if [ "$major" -lt 4 ]; then
|
|
echo "::error::shell contracts needs fish 4+ (DECSET 2031 arming, #9993) but got '${version:-none}'. Fix the ppa:fish-shell/release-4 install rather than letting the fish lane skip." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- 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 --maxWorkers=1 \
|
|
src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
|
|
src/main/daemon/shell-ready.test.ts \
|
|
src/main/daemon/node-pty-fd-leak.test.ts \
|
|
src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
|
|
src/main/providers/local-pty-shell-ready-zsh-startup-file-behavior.test.ts \
|
|
src/main/providers/local-pty-shell-ready-zsh-zdotdir-discovery.test.ts \
|
|
src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts \
|
|
src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
|
src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
|
src/main/shell-startup-feature-channel.test.ts \
|
|
src/main/terminal-history-fish-session.node-pty.test.ts \
|
|
src/main/zsh-scoped-histfile.live-shell.test.ts \
|
|
src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
|
|
src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
|
src/shared/startup-shell-portability.live-shell.test.ts \
|
|
src/shared/posix-command-path-lookup.test.ts
|
|
|
|
test:
|
|
name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }}
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
node: ['24', '26']
|
|
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
|
shard_total: [16]
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
native-runtime: node
|
|
node-version: ${{ matrix.node }}
|
|
|
|
- name: Install Electron package binary for tests
|
|
run: node config/scripts/install-electron-package-binary.mjs
|
|
|
|
- name: Test shard
|
|
run: |
|
|
pnpm exec vitest run --config config/vitest.config.ts \
|
|
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.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-zsh-launch-environment.test.ts \
|
|
--exclude=src/main/providers/local-pty-shell-ready-zsh-startup-file-behavior.test.ts \
|
|
--exclude=src/main/providers/local-pty-shell-ready-zsh-zdotdir-discovery.test.ts \
|
|
--exclude=src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.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/main/shell-startup-feature-channel.test.ts \
|
|
--exclude=src/main/terminal-history-fish-session.node-pty.test.ts \
|
|
--exclude=src/main/zsh-scoped-histfile.live-shell.test.ts \
|
|
--exclude=src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
|
|
--exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
|
--exclude=src/shared/startup-shell-portability.live-shell.test.ts \
|
|
--exclude=src/shared/posix-command-path-lookup.test.ts \
|
|
--exclude=tests/e2e/cross-version-wire/** \
|
|
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
|
|
|
|
cross-version-wire:
|
|
name: cross-version wire compatibility
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
# Why fetch-depth 0: the harness extracts the newest release tag to skew
|
|
# current code against it. The default shallow clone has no tags, which is
|
|
# why this cannot ride along in the sharded `test` job.
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Why blob:none: full history is needed for the merge-base diff, but historical
|
|
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
|
# few this job actually reads on demand.
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
native-runtime: node
|
|
|
|
# A path filter that matches nothing exits 1 ("No test files found"), so this
|
|
# lane cannot report success while running zero tests.
|
|
- name: Old/new client and server terminal journey
|
|
run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts
|
|
|
|
managed_hook_node18:
|
|
name: managed hooks on Node 18
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
- name: Build relay companions
|
|
run: pnpm run build:relay
|
|
|
|
- name: Setup Node 18 runtime
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Smoke managed-hook companions
|
|
run: node config/scripts/smoke-managed-hook-runtime-node18.mjs
|
|
|
|
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 --linux AppImage --x64 --publish never
|
|
|
|
- name: Verify headless serve signal shutdown
|
|
run: node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage
|
|
|
|
- name: Smoke packaged CLI
|
|
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked
|
|
|
|
- name: Smoke packaged hang watchdog worker
|
|
run: xvfb-run --auto-servernum node config/scripts/smoke-packaged-hang-watchdog-worker.mjs --app-dir=dist/linux-unpacked
|
|
|
|
package_windows:
|
|
name: package (windows)
|
|
runs-on: windows-2022
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
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-windows-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
electron-builder-windows-
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Test Windows-specific boundaries
|
|
run: >-
|
|
pnpm exec vitest run --config config/vitest.config.ts
|
|
src/main/cli/wsl-cli-powershell-boundary.test.ts
|
|
src/main/orca-profiles/profile-index-store.test.ts
|
|
src/main/runtime/repo-worktree-admin-fingerprint.test.ts
|
|
src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts
|
|
src/shared/secure-file-fsync-flags.test.ts
|
|
|
|
# Why the :parallel variant: identical to build:release except the three
|
|
# electron-vite targets overlap instead of running back to back. The Linux package
|
|
# job already packages and smoke-tests an AppImage built that way.
|
|
- name: Build package inputs
|
|
run: pnpm run build:release:parallel
|
|
|
|
- name: Prepare Electron native runtime
|
|
run: node config/scripts/ensure-native-runtime.mjs --runtime=electron
|
|
|
|
- name: Package unpacked app
|
|
env:
|
|
ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1'
|
|
run: pnpm exec electron-builder --config config/electron-builder.config.cjs --dir
|
|
|
|
- name: Smoke packaged CLI
|
|
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/win-unpacked
|
|
|
|
# Why: PR E2E is advisory and only validates changed specs; scheduled and
|
|
# release runs retain full-suite coverage.
|
|
e2e-paths:
|
|
name: detect changed e2e specs
|
|
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 }}
|
|
test_files: ${{ steps.filter.outputs.test_files }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# Why blob:none: full history is needed for the merge-base diff, but historical
|
|
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
|
# few this job actually reads on demand.
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
persist-credentials: false
|
|
|
|
- name: Filter changed E2E specs
|
|
id: filter
|
|
run: |
|
|
set -euo pipefail
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
CHANGED="$(git diff --name-only --diff-filter=AMCR --merge-base "$BASE" "$HEAD")"
|
|
TEST_FILES="$(printf '%s\n' "$CHANGED" | grep -E '^tests/e2e/.*\.spec\.ts$' || true)"
|
|
if printf '%s\n' "$CHANGED" | grep -Eq '^src/(main/ephemeral-vm-(runtime-(service|provisioning-persistence)|failed-start-cleanup)|shared/(ephemeral-vm-runtime-(store|feature-store|rollback-projection|runtimes)|ephemeral-vm-recipes|orca-yaml-hook-types))\.ts$'; then
|
|
TEST_FILES="$(printf '%s\n%s\n' "$TEST_FILES" 'tests/e2e/ephemeral-vm-provisioned-root.spec.ts' | sort -u)"
|
|
fi
|
|
# Why: the Docker-SSH specs only ever ran when someone edited a spec, so four
|
|
# pane-restore regressions shipped from SSH source edits that touched no test.
|
|
# Unit tests are excluded because they prove the same source without Docker.
|
|
# ssh-startup-exec-readiness.spec.ts is listed to route e2e.yml's dedicated
|
|
# ssh-docker-watcher-isolation lane, which filters it out of changed-e2e.
|
|
if printf '%s\n' "$CHANGED" | grep -Ev '\.test\.tsx?$' | grep -Eq '^(src/main/ssh/|src/main/providers/ssh-|src/main/ipc/(ssh-|pty)|src/relay/|src/renderer/src/components/terminal-pane/(pty-|ssh-|remote-runtime-|terminal-parked-pty))'; then
|
|
TEST_FILES="$(printf '%s\n' "$TEST_FILES" \
|
|
'tests/e2e/pty-input-write-queue-ssh.spec.ts' \
|
|
'tests/e2e/ssh-cold-activation-restore.spec.ts' \
|
|
'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts' \
|
|
'tests/e2e/ssh-startup-exec-readiness.spec.ts' \
|
|
'tests/e2e/ssh-terminal-window-wake-stale-grid-repro.spec.ts' | sort -u)"
|
|
fi
|
|
# Why: same lesson as the SSH specs above. The IME e2e suite asserts real pty bytes and
|
|
# real overlay geometry, but only ran when someone edited a spec, so every IME fix in the
|
|
# 1.4.18x window shipped without it - including one whose diagnosis hardware later refuted.
|
|
# config/patches is listed because the terminal's input handling lives in the xterm patch.
|
|
if printf '%s\n' "$CHANGED" | grep -Ev '\.test\.tsx?$' | grep -Eq '^(config/patches/|src/renderer/src/components/terminal-pane/(terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy))'; then
|
|
TEST_FILES="$(printf '%s\n' "$TEST_FILES" \
|
|
'tests/e2e/terminal-cjk-ime-committed-text.spec.ts' \
|
|
'tests/e2e/terminal-hangul-wrap-boundary-bytes.spec.ts' \
|
|
'tests/e2e/terminal-ime-exact-byte.spec.ts' \
|
|
'tests/e2e/terminal-korean-composing-chord-order.spec.ts' \
|
|
'tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts' \
|
|
'tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts' \
|
|
'tests/e2e/terminal-korean-preedit-visibility.spec.ts' | sort -u)"
|
|
fi
|
|
TEST_FILES_JSON="$(printf '%s\n' "$TEST_FILES" | jq --raw-input --slurp --compact-output 'split("\n") | map(select(length > 0))')"
|
|
echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT"
|
|
if [ "$TEST_FILES_JSON" != '[]' ]; then
|
|
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
|
echo "Changed E2E specs: $TEST_FILES_JSON"
|
|
else
|
|
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
|
echo "No changed E2E specs"
|
|
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
|
|
with:
|
|
test_files: ${{ needs.e2e-paths.outputs.test_files }}
|
|
|
|
verify:
|
|
if: always()
|
|
needs:
|
|
- static_analysis
|
|
- root_directory_guard
|
|
- typecheck
|
|
- git_compatibility
|
|
- xterm_patch_sync
|
|
- shell_contracts
|
|
- test
|
|
- managed_hook_node18
|
|
- package
|
|
- package_windows
|
|
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 }}
|
|
ROOT_DIRECTORY_GUARD: ${{ needs.root_directory_guard.result }}
|
|
TYPECHECK: ${{ needs.typecheck.result }}
|
|
GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }}
|
|
XTERM_PATCH_SYNC: ${{ needs.xterm_patch_sync.result }}
|
|
SHELL_CONTRACTS: ${{ needs.shell_contracts.result }}
|
|
TEST: ${{ needs.test.result }}
|
|
MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }}
|
|
PACKAGE: ${{ needs.package.result }}
|
|
PACKAGE_WINDOWS: ${{ needs.package_windows.result }}
|
|
run: |
|
|
for result in \
|
|
"$STATIC_ANALYSIS" \
|
|
"$ROOT_DIRECTORY_GUARD" \
|
|
"$TYPECHECK" \
|
|
"$GIT_COMPATIBILITY" \
|
|
"$XTERM_PATCH_SYNC" \
|
|
"$SHELL_CONTRACTS" \
|
|
"$TEST" \
|
|
"$MANAGED_HOOK_NODE18" \
|
|
"$PACKAGE" \
|
|
"$PACKAGE_WINDOWS"; do
|
|
if [ "$result" != "success" ]; then
|
|
exit 1
|
|
fi
|
|
done
|