mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* refactor(mobile): take the files screens' router from the handoff seam Inside the shell's page a screen is one document standing in for one screen, so a target the page does not render has to be handed back to the app that does. `useRouteHandoff` is where that decision lives, and its web sibling is the only thing that makes it; both files screens held expo-router's own `useRouter`, so on the web the explorer's Back and the preview's Back would post nothing and a target outside the page would paint Unmatched over the page it is on. Natively this is the same object — `route-handoff.ts` is `useRouter()` — so no behaviour moves here, and `back()` stays expo-router's until the navigate-back verb lands and the seam starts wrapping it. A census rather than a behaviour test: neither screen's own tests can see the difference, because a push that is never handed off still works for a target inside the page. It walks this directory, refuses a value import of expo-router, and names the two screens that must hold a router so a walk that found nothing fails instead of passing empty. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): let the shell stand in for the two files routes Both route files take the index.tsx shape — flag, MobileWebShellScreen, native screen as fallback — and both gain the `.web.tsx` sibling that shape forces. Inert until the manifest lists these routes: the shell answers `native-route` for a route the bundle does not name, which is what `fallback` renders, and the flag is `__DEV__`-only besides. Listing them waits on C2.3 and C2.5. The sibling is not a precaution. The manifest defers every route behind `import()`, so a native-only route module is invisible until the page opens that route; the render check now opens both and, without the siblings, painted `expo-modules-core.requireNativeViewManager is not available on web` instead of the screen. That is also why the two cases render the route rather than asserting a file exists. The file path never becomes a path segment: only `hostId` and `worktreeId` are spelled into the pathname, encoded, and everything else — `relativePath`, `absolutePath`, `cwd`, `pathText` — is a param, which is how a `/`, a space or a `..` stays out of the segment vocabulary the bridge holds a route to. The preview render case proves the round trip on `docs/my notes/readme.md`. `mobileFilePreviewShellParams` drops a param the normalizer left `undefined` rather than sending it empty, because the page reads these back through useLocalSearchParams where `line: ''` and no `line` are different screens. Its test drives the normalizer rather than a hand-written literal: the literal omits the key entirely, so it held with the filter removed. The preview case also records what React Native Web says out loud — BackHandler is inert on web, so Android back inside the page skips the unsaved-draft prompt. Named in the assertion rather than filtered out, so closing it is a change to that line. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): ask about an unsaved draft in the screen, not through Alert React Native Web's `Alert` is `static alert() {}`. Inside the shell's page that made Back with an unsaved terminal-artifact draft a button that did nothing at all: no prompt, because the dialog is a no-op, and no navigation either, because the code took the branch that shows one. Silently, with nothing on the console. The prompt is now a row under the header. Not `ConfirmModal`, which every other confirm here uses: that is a `BottomDrawer`, and C1.9 has Reanimated's animated styles never reaching the DOM node on WKWebView, so on iOS in the page the drawer parks off-screen and Back would be dead a second way. This paints the same on every platform with no animation behind it. Hardware back is registered natively only. React Native Web's `BackHandler.addEventListener` logs "BackHandler is not supported on web and should not be used." and hands back an inert subscription, so the guard never armed there regardless; the render check asserted that console error on main and now asserts none. The degradation is real and stated rather than hidden: Android back inside the page pops the native stack without asking, and the page's own Back control is where the question lives. The decision moved to a hook so it is testable without a screen: the prompt also drops itself when the draft it was about is saved or reverted, which is a state `Alert` had no way to be in. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep expo-haptics' DOM shim out of the page expo-haptics has a web build, and with no `navigator.vibrate` — iOS Safari, which is the WebView the page runs in — it fakes a haptic by appending a hidden `<label><input type="checkbox" switch>` to `document.head`, clicking it, and removing it, once per call. C1.9 traced a long press that never fired on the worktree list to exactly that stray click, and the file explorer calls `triggerSelection` on every row tap, so C3 is the first domain to fire it per tap rather than per long press. `haptics.web.ts` answers the same five names with nothing. A phone holding the page is a phone whose native app is right there with the real haptics, and a missing tap feedback is worth less than a tap that does not register. The test reads the shipped bytes rather than the import, because that is the claim: with the override removed the bundle carries `ariaHidden` and `pointer: coarse`; with it, neither, nor the `setAttribute("switch"` that does the clicking. Not `navigator.vibrate` — react-native-web's own Vibration export calls that and touches no DOM until something invokes it, which cost this test one wrong red before it was narrowed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the files routes native when the page could not be given one A file path is a param, so `/`, spaces and `..` all cross safely — but `BRIDGE_MAX_ROUTE_PARAM_CHARS` is 1024 and a Windows long path is not bounded by anything the user cannot exceed. The symptom is not the blank document the design predicted, and the correction matters: `bridge-host.ts` already parses the route against the page's own schema and drops it to `null` when it fails, so `init` arrives naming no screen and the page paints "Update Orca to open this workspace" — a wrong message about a fine app, over a native screen that works. Deciding before the switch instead leaves the route native, which is where every route starts. The schema is the predicate rather than a copy of its bounds, so the rule cannot drift from the half that matters, which is the half the page reads. The same call also refuses a `worktreeId` the segment rule will not route: `..` survives `encodeURIComponent`, which is the C1.8 class. The tests assert the schema really refuses each input before asserting the guard does, so neither case can pass by being impossible. This belongs in the shell beside the schema; it is in the files domain while the contract files are the C2 lane's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin what keeps a file path out of the route vocabulary Seven shapes, one case each rather than a representative: a plain path, a space, a dot segment, an already-encoded slash, a fragment, non-ASCII, and an absolute path. Each is checked in the two directions a path travels — the href the shell writes into the page's history, and the href the page would hand back — for both the pattern accepting it and the path coming back out of the query unchanged. The counterfactual is in the file: the same paths spelled as a segment are refused. Without that, the cases above would hold for a rule that was never doing any work. Mutating `stringifyRouteHref` to join its query by hand instead of through `URLSearchParams` fails three of them. Also fixes two new test files the tests-typecheck ratchet caught: the partial `react-native` mock needs a typed `addEventListener`, `act` will not take a callback that returns a value, and `findAllByType('Pressable')` does not typecheck against `ElementType` — the neighbouring files that do it are grandfathered, so the tag comparison goes through a helper instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): derive the discard prompt instead of clearing it in an effect Both changed-code gate findings, which the lane had not run until the last commit. React Doctor is right: the effect that cleared the prompt when the draft went away adjusted state after a prop changed, so a save landing while the prompt was up painted one frame still offering to discard nothing. The prompt is now `asking && hasUnsavedDraft`, which cannot be stale by construction, and the test that covers it passes unchanged. The hoisted mock's `as` on a string literal is gone too: the literal narrows on its own and the tests reassign it, so the holder is annotated instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): add the files routes to the hybrid shell flag census The census pins every file that reads `useMobileWebShellEnabled`, because a reader nobody listed is how a dark feature stops being dark. C3's two routes are deliberate entries: each has a native screen behind it as `fallback`, and each is inert until the manifest lists the route. Found by the full mobile suite rather than by the files subset this lane had been running per commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): serve the files explorer and preview from the page The last C3 commit: both routes join MOBILE_WEB_PAGE_ROUTES, and the shell starts rendering the page for them on a phone with the dev flag on. Grants are not the same for the two, and the difference is the point. Both take `navigate` (Back pops the native stack, and the explorer's rows open the preview beside it) and `storage` (the shared components the host layout renders above them). Only the preview takes `externalLink`: a Markdown preview renders links and `MobileMarkdown` opens them through the platform seam. The explorer does not, and measuring is what says so rather than reading. Every page route reaches `external-link.web.ts` — `/h/[hostId]` and agent-history included, both granted nothing for it — because the protocol wall in the shared host layout imports it. So closure membership is not the oracle for a grant; the question is whether the route's own screens call it, and only the preview's do. `MobileMarkdown` is in the preview closure and absent from the explorer's, which the census now asserts in both directions. Neither route writes a clipboard, so neither takes `native.clipboard.write`; the census pins that as the absence of both `ExpoClipboard.web.js` and the clipboard seam, with the tasks closure as the control that the probe can see one when there is one. The seam predicate moved into a module both censuses import rather than being restated per series: two spellings of one rule drift, and this one is a regex. Red-first: both manifest assertions failed on the new entries before they were updated, and routing `MobileMarkdown` around the seam fails the preview's census while leaving the explorer's passing, which is the asymmetry the grants encode. Closure sizes as the page ships them, extensionless so the `.web.tsx` is what is measured: explorer 3439 modules / 302 local / 10 under src/files, preview 3667 / 331 / 20. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read the files route's ids as one value and key the shell on them Two round-1 findings, both reproduced before the fix. A repeated query key reaches `useLocalSearchParams` as an array, and the explorer read `hostId` and `worktreeId` bare. `String(['a','b'])` is `a,b`, so the template built `/h/host-a%2Chost-b/files/wt-1%2Cwt-2` — a single segment the bridge's rule accepts, and the shell would open a page for a host nobody has. Read through `firstParam` now, as the tasks and agent-history switches do. The preview already went through `singleParam` and is unchanged. Neither switch keyed `MobileWebShellScreen`, where `index.tsx`, `tasks.tsx` and agent-history all do. A host captures the grants its session opened with, so a screen reused across a route change keeps authorising frames under the grants of the route the page has left; only a remount drops that bridge. Both are keyed on the route pathname now, with agent-history's reason. The new route test is the agent-history one's shape. It caught both: the array case landed on no route at all, because `name` was an array too and the schema refuses a non-string param value, and the two lifecycle cases saw a prop update where a remount was owed. It also needs agent-history's `lucide-react-native` mock, since `firstParam` lives in the source-control barrel. `name` is now omitted when empty rather than sent as `name=`, matching the two switches beside it: an absent label lets the panel derive its own. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): confirm a discarded draft with the app's own modal Round-1 findings 3, 4, 5 and the minor one. **ConfirmModal, not the bespoke row.** The row existed because C1.9 had Reanimated's animated styles never reaching the DOM node on WKWebView, which left every BottomDrawer parked off-screen. C1.10 (`b7c06900e2`, an ancestor of this branch) fixed that with a dependency array on the mapper hooks, and the drawer render check now holds it on WebKit as well as Chromium. With the reason gone the row does not stand on its other merits: `Alert.alert` was modal on native before the page existed, and the row quietly changed that for phones too, so the app's own confirm is both the idiom and the closer behaviour. `MobileFilePreviewDiscardPrompt`, its test and its thirty style keys are gone; the hook's state machine and its tests are unchanged. **The encoding test claimed more than it pinned.** Hand-joining the query reds only three of the seven shapes; `docs/readme.md`, `../etc/passwd`, `docs/日本語.md` and `/logs/run.txt` are encoding-neutral in the query, whose pattern half is `[^#\s]*` and admits a slash, a dot segment and non-ASCII verbatim. Rather than narrow the claim in a comment, the split is now pinned by behaviour: each neutral shape must survive the query unencoded, each load-bearing one must not. Moving `docs/readme.md` between the lists fails it. **The manifest comment named one shared-layout opener and there are two.** The New Workspace source field, which the sidebar renders on a wide layout, opens a URL through the seam as well. Both are the shared layout's and every `/h` route reaches both, `/h/[hostId]` included with no `externalLink`, so the tablet tap is dead on all of them — recorded here as pre-existing rather than fixed, since the grants do not move. **Minor:** the dot-segment case in the guard test now asserts the schema refuses the route before asserting the guard returns null, as the length case does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop every page drawer logging a BackHandler error when it opens Round-2 findings. **The registration belongs to the drawer, and that is where the guard went.** `mounted-bottom-drawer.tsx` armed `hardwareBackPress` whenever a drawer was visible and interactive, with no platform check, so the hook's claim to have dropped that console line held only while its prompt was closed — and every page drawer since C1 has logged it on open. Platform-gated at the drawer now; the hook's comment says so rather than claiming the credit. **Nothing had ever opened a modal in a browser.** The render check next door mounts both files routes and reads what they paint but taps nothing, so `ConfirmModal` inside the page — a BottomDrawer, so Reanimated, a portal and a gesture handler — was unproved. A new render file loads an editable terminal artifact through the harness's scripted reply, edits it, taps the page's Back, and asserts the prompt's title is up and no BackHandler line is on the console. Red first on exactly that line; the prompt itself painted, which is also the first proof on a browser that C1.10's fix carries a real drawer in the page. A second case answers Stay and checks the draft survives. Its own file rather than the render check's, which is at 482 of the 600-line cap; registered in pr.yml. **The encoding rule was stated wrong.** Two rules decide it and neither is about paths: the pattern's query half refuses whitespace and `#`, and `URLSearchParams` is form-urlencoded, so it reinterprets `&`, `+` and a valid `%XX`. `a+b.ts` reads back `a b.ts` and `a&b.ts` reads back `a`, so both are load-bearing; `a=b.ts` and `a%b.ts` are not, because only the first `=` splits the pair and a lone `%` begins no escape. A newline joins the load-bearing list as the refused shape rather than the altered one. **The web sibling read its params bare** where the native one uses `firstParam`. Not reachable — the page only arrives through `init.route`, whose params are already `Record<string, string>` — but the two files are meant to be one screen. The preview keys on the pathname alone, and the comment now says why that is enough: every caller in this tree pushes. Closures after this: explorer 3441 / 304 / 10, preview 3666 / 330 / 19. The explorer grew two modules because its web sibling now reaches `firstParam`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give the explorer the grants the preview needs, and key on the route Bot findings, one of them a real gap. **Pullfrog is right, and my grant oracle was half a rule.** Grants resolve once, from the route the shell opened: `grantsForRoute` reads `session.routePathname` and `init.grants.native` carries the answer for that session. The explorer's rows push to the preview, and because the preview is a page route that push stays inside the same document — no second `init`. So a preview opened that way runs under the explorer's grants, and a Markdown link in it was refused by `notifyExternalLink` with nothing on screen to say why. "Does the route's own screen call it" was right for a route's own screens and wrong for the routes it reaches in-page, so the explorer now declares `externalLink` as a transitive grant, with the comment saying that rather than claiming it opens links. The census pins the pair as a superset; removing the grant reds it. **The seam regexes matched one quote style.** A double-quoted `react-native` specifier walked past both censuses unseen. Both styles now, with the predicate tested directly for the first time. **The discard request outlived its draft.** `asking` stayed set after a save or a revert, so the next edit re-showed the prompt with no Back request behind it. The request is now dropped when the draft it was about goes, adjusted during render rather than in an effect — the shape React Doctor named in the round-1 fold. Red first: save with the prompt up, edit again, prompt is back. **CodeRabbit's keying comment is a correctness point, not the question I answered.** The page learns its route exactly once, out of `init`, so a same-path param change — another file in the same worktree — left the shell mounted and the page still showing the file it was opened on. My comment claimed "the screen reloads the preview from the param either way", which is true only with the shell absent. Both switches key on the whole route now, params included; two tests cover the same-path case and both red on a pathname-only key. `build-mobile-web-app-bundle.test.mjs` hit 601 of its 600-line cap on the way, so the two manifest assertions now share one expected list instead of repeating it. Closures unchanged: explorer 3441 / 304 / 10, preview 3666 / 330 / 19. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the seam test import the module it is testing Round 3. **The blocker is mine and the reviewer's diagnosis is exact.** The seam predicate test imported an absolute path into this lane's worktree. On CI that module does not exist and it takes the whole `config/scripts` suite down; here it resolved to the same file by accident, so the test was green against a tree rather than against the checkout — which is why reverting the double-quote fix left it passing and the predicate untested. Relative now, and proved: reverting the fix in place reds both double-quoted cases, which is the first time this test has failed for the right reason. Every file this PR touches is grepped for `/Users/` and `orca-lanes`; none carries a path. **Three comments outlived the grant change.** The two lists became equal when the explorer took `externalLink`, so "longer than the explorer's" and "declared with different grants" were both false. Corrected to what is actually true: the lists are equal and the reasons are not — the preview has its own consumer in `MobileMarkdown`, the explorer has none and declares the grant because its rows push to the preview in-page. **The duplicated serializer is pinned rather than imported.** `shellRouteHref` lives in `page-bootstrap.ts` beside the page's RPC client and its document channel, so a native route file importing it would pull both into the app. The copy stays, and a test asserts the two agree on three routes; dropping the empty-search branch reds it. **Recorded, not fixed:** the sidebar `HostScreen` pushes to `/h/<id>/tasks` through the handoff, which is local, so on a tablet the tasks page runs without `native.clipboard.write` from any page route and its copy actions refuse silently. Pre-existing since C2.1 for the worktree list and agent history. Named in the explorer's manifest comment as the known remaining hop, with the fix being a handoff rule in its own PR. The equality pin needed `it.each<BridgeInitRoute>`: the inferred table is a union whose members carry `?: undefined`, which the ratchet caught. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
1182 lines
55 KiB
YAML
1182 lines
55 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:
|
|
# Why: a README/docs-only PR used to start the full matrix (test shards,
|
|
# two package jobs, typecheck, git compat, xterm, shell contracts). Path
|
|
# filters on `on.pull_request` would drop the `verify` check entirely; this
|
|
# detector keeps verify as the required aggregate and skips the expensive jobs.
|
|
# Per-job outputs also skip git-compat/xterm/packaging/shell when those
|
|
# inputs are unchanged; empty diffs fail closed and run everything.
|
|
code_paths:
|
|
name: detect code-relevant changes
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
should_run: ${{ steps.filter.outputs.should_run }}
|
|
native_cache_changed: ${{ steps.filter.outputs.native_cache_changed }}
|
|
mobile_dependencies: ${{ steps.filter.outputs.mobile_dependencies }}
|
|
mobile_web_app: ${{ steps.filter.outputs.mobile_web_app }}
|
|
static_analysis: ${{ steps.filter.outputs.static_analysis }}
|
|
typecheck: ${{ steps.filter.outputs.typecheck }}
|
|
git_compatibility: ${{ steps.filter.outputs.git_compatibility }}
|
|
codex_index_heal_contract: ${{ steps.filter.outputs.codex_index_heal_contract }}
|
|
xterm_patch_sync: ${{ steps.filter.outputs.xterm_patch_sync }}
|
|
shell_contracts: ${{ steps.filter.outputs.shell_contracts }}
|
|
test: ${{ steps.filter.outputs.test }}
|
|
orcad_browser: ${{ steps.filter.outputs.orcad_browser }}
|
|
cross-version-wire: ${{ steps.filter.outputs.cross-version-wire }}
|
|
managed_hook_node18: ${{ steps.filter.outputs.managed_hook_node18 }}
|
|
package: ${{ steps.filter.outputs.package }}
|
|
package_windows: ${{ steps.filter.outputs.package_windows }}
|
|
e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }}
|
|
test_files: ${{ steps.e2e_filter.outputs.test_files }}
|
|
ssh_source_changed: ${{ steps.e2e_filter.outputs.ssh_source_changed }}
|
|
native_ime_source_changed: ${{ steps.e2e_filter.outputs.native_ime_source_changed }}
|
|
wsl_source_changed: ${{ steps.e2e_filter.outputs.wsl_source_changed }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# 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: Classify changed paths
|
|
id: filter
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Why --no-renames: name-only rename detection can report only the destination.
|
|
# A code file moved under docs/ must still expose its code-side deletion.
|
|
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE_SHA" "$HEAD_SHA")"
|
|
echo "Changed paths:"
|
|
printf '%s\n' "$CHANGED"
|
|
printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs | tee -a "$GITHUB_OUTPUT"
|
|
|
|
# Reuse the path-detector checkout instead of queuing another runner.
|
|
- name: Filter changed E2E specs
|
|
id: e2e_filter
|
|
if: github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
CHANGED="$(git diff --name-only --diff-filter=AMCR --merge-base "$BASE" "$HEAD")"
|
|
# Source routes are executable contracts so a test can prove exact
|
|
# authorities, exclusions, and sentinels without evaluating workflow shell.
|
|
TEST_FILES_JSON="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs)"
|
|
echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT"
|
|
# Why a separate signal: the Docker-SSH lane must trigger on SSH source, not on a
|
|
# spec name surviving in a route's list. Same routes, so the two cannot drift.
|
|
SSH_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --ssh-source)"
|
|
echo "ssh_source_changed=$SSH_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
|
echo "SSH source changed: $SSH_SOURCE_CHANGED"
|
|
# Why its own signal: the real-IME lane is a whole ibus session, not a spec, so it must
|
|
# trigger on IME source rather than on a spec name in some route's list.
|
|
NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)"
|
|
echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
|
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE" "$HEAD")"
|
|
WSL_SOURCE_CHANGED="$(printf '%s\n' "$WSL_CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --wsl-source)"
|
|
echo "wsl_source_changed=$WSL_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
|
echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED"
|
|
SHOULD_RUN="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --reusable-workflow)"
|
|
if [ "$SHOULD_RUN" = true ]; then
|
|
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
|
echo "Changed E2E specs: $TEST_FILES_JSON"
|
|
else
|
|
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
|
echo "No specs requiring the reusable E2E workflow"
|
|
fi
|
|
|
|
static_analysis:
|
|
name: static analysis
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.static_analysis == 'true'
|
|
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
|
|
with:
|
|
native-runtime: node
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
- name: Lint
|
|
run: pnpm exec oxlint --format github
|
|
|
|
- name: Reject low-evidence patterns
|
|
run: pnpm run audit:anti-slop
|
|
|
|
- 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
|
|
|
|
# Why here: the changed-code gate lints mobile files too, and its type-aware pass
|
|
# resolves types from mobile/node_modules. Without the install every mobile type
|
|
# degrades to an `error` type — reported as phantom findings against the changed lines.
|
|
- uses: ./.github/actions/install-mobile-dependencies
|
|
if: needs.code_paths.outputs.mobile_dependencies == 'true'
|
|
|
|
- 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 dead design-system classes
|
|
run: pnpm run check:dead-classes
|
|
|
|
- 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: Enforce ts-nocheck ratchet
|
|
run: pnpm run check:ts-nocheck-ratchet
|
|
|
|
- name: Enforce runtime Electron-import ratchet
|
|
run: pnpm run check:runtime-electron-ratchet
|
|
|
|
# Why both: the ratchet proves nothing reachable from the runtime imports electron,
|
|
# which is a property of the import graph. This proves the Node artifact it enables
|
|
# actually boots, pairs, creates a worktree and round-trips a real PTY.
|
|
- name: Boot orcad and round-trip a terminal
|
|
run: pnpm run smoke:orcad-terminal
|
|
|
|
- name: Verify the generated RPC params catalog
|
|
run: pnpm run verify:rpc-params-catalog
|
|
|
|
- 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: the renderer ships only the English entries i18next cannot rebuild
|
|
# from each call site's inline default, so the generated subset has to
|
|
# track en.json and those defaults.
|
|
- name: Verify runtime-required localization catalog
|
|
run: pnpm run verify:localization-runtime-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"
|
|
|
|
# Why here: the READMEs embed media owned by docs/site and resources/onboarding,
|
|
# and the classifier skips static_analysis for docs-only diffs. This job runs
|
|
# on every PR and needs no install.
|
|
- name: Check README local links
|
|
run: node config/scripts/check-readme-local-links.mjs
|
|
|
|
typecheck:
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.typecheck == 'true'
|
|
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. Share one cache entry across commits while the
|
|
# PR base stays stable; actions/cache keeps the first successful graph and the
|
|
# compiler still invalidates stale files from its content hashes.
|
|
- 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.event.pull_request.base.sha }}
|
|
restore-keys: |
|
|
tsbuildinfo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'config/tsconfig*.json') }}-
|
|
|
|
- run: pnpm run typecheck
|
|
|
|
git_compatibility:
|
|
name: Git compatibility
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.git_compatibility == 'true'
|
|
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. Only this PR's own later pushes
|
|
# can restore it — GitHub scopes a cache written from a pull_request run to that
|
|
# ref — so a first push always takes the build path below.
|
|
- 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
|
|
|
|
# Why its own step: this is `make -j$(nproc)` on every core, and the lanes below
|
|
# spend their wall clock waiting on container starts, not on Git. Sharing a runner
|
|
# with the build stretched one ~1.5s boundary case past Vitest's 30s timeout, so
|
|
# the build has to finish before anything timed starts.
|
|
- name: Build the baseline Git binary
|
|
run: |
|
|
archive="$RUNNER_TEMP/git-2.25.5.tar.gz"
|
|
source="$HOME/.cache/orca-git-compat/git-2.25.5"
|
|
if [ -x "$source/git" ]; then
|
|
exit 0
|
|
fi
|
|
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
|
|
|
|
- name: Verify Git binary compatibility matrix
|
|
run: |
|
|
specs=(
|
|
"alpine/git:edge-2.38.1|2.38.1"
|
|
"alpine/git:v2.49.1|2.49.1"
|
|
)
|
|
# Why pull up front: a lane's first `docker run` otherwise pulls its image
|
|
# while the sibling lane is mid-test, and that stall is charged to the test.
|
|
for spec in "${specs[@]}"; do
|
|
docker pull --quiet "${spec%%|*}"
|
|
done
|
|
|
|
pids=()
|
|
(
|
|
ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \
|
|
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 "${specs[@]}"; do
|
|
(
|
|
image="${spec%%|*}"
|
|
version="${spec#*|}"
|
|
ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \
|
|
pnpm exec vitest run --config config/vitest.config.ts \
|
|
src/shared/git-binary-compatibility.test.ts
|
|
) &
|
|
pids+=("$!")
|
|
done
|
|
|
|
status=0
|
|
for pid in "${pids[@]}"; do
|
|
wait "$pid" || status=1
|
|
done
|
|
exit "$status"
|
|
|
|
# Why this job: Orca's session index-heal depends on a Codex behavior — a
|
|
# `thread/read` of an unindexed rollout performs a read-repair that inserts the
|
|
# `threads` row. Every unit test drives a stub app-server and asserts only that the
|
|
# call did not error, so if Codex dropped the repair they would all stay green while
|
|
# the subsystem went inert. This runs the pinned real binary and fails when the
|
|
# repair stops happening. Pinned because the binary is the thing expected to drift.
|
|
codex_index_heal_contract:
|
|
name: Codex index-heal contract
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.codex_index_heal_contract == 'true'
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
CODEX_CLI_VERSION: '0.150.1'
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
- name: Install pinned Codex CLI
|
|
run: |
|
|
set -euo pipefail
|
|
npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli" \
|
|
"@openai/codex@$CODEX_CLI_VERSION"
|
|
|
|
- name: Verify Codex index-heal contract
|
|
env:
|
|
# Why REQUIRED: without a binary the suite skips, and a job that skips
|
|
# reports success. This turns a failed or missing install into a red test
|
|
# instead of a green no-op.
|
|
ORCA_CODEX_CONTRACT_REQUIRED: '1'
|
|
ORCA_CODEX_CONTRACT_VERSION: ${{ env.CODEX_CLI_VERSION }}
|
|
run: |
|
|
set -euo pipefail
|
|
ORCA_CODEX_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli/node_modules/.bin/codex" \
|
|
pnpm exec vitest run --config config/vitest.config.ts \
|
|
src/main/codex/codex-index-heal-binary-contract.test.ts
|
|
|
|
xterm_patch_sync:
|
|
name: xterm patch sync
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.xterm_patch_sync == 'true'
|
|
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 every package in the manifest from a pinned upstream
|
|
# commit — @xterm/xterm and its three addons, each built twice (once unmodified to
|
|
# prove the toolchain still reproduces the published bundles, once patched). Caching
|
|
# the npm metadata and the shallow clone keeps the repeated cost to the builds
|
|
# themselves; the key is the manifest, so a commit, package 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
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.shell_contracts == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Why: this job's cost is almost entirely package download, and a stalled mirror has
|
|
# no wall-clock bound of its own. A successful run finishes in ~4.5 minutes, so this
|
|
# is generous; it exists so a wedge fails the job instead of holding the whole run
|
|
# open for the 6h GitHub default — which also blocks `gh run rerun --failed`.
|
|
timeout-minutes: 15
|
|
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.
|
|
# Why bound acquisition: measured on a *passing* run, this step spent 40s
|
|
# fetching 11.4 MB of index and then 2m17s fetching 8.9 MB of packages at
|
|
# 65 kB/s — it is dominated by download throughput, not by work. apt applies
|
|
# no wall-clock bound to a stalled mirror, so a slow Launchpad or archive
|
|
# host wedges the step for tens of minutes. This job is a required check, so
|
|
# a wedge holds the entire run open and blocks `gh run rerun --failed`.
|
|
# Bounded timeouts plus retries turn an unbounded hang into a fast, legible
|
|
# failure. Set in apt.conf.d rather than on each command line so the two
|
|
# invocations below stay exactly as pr-workflow-parallelism.test.mjs parses
|
|
# them. Retries are 1, not 3: a first attempt at these bounds already multiplied
|
|
# 30s x 3 retries across every index file into a ~15 minute stall on a dead
|
|
# mirror, which is worse than failing once and moving on.
|
|
sudo tee /etc/apt/apt.conf.d/99-orca-shell-contracts >/dev/null <<'APTCONF'
|
|
Acquire::http::Timeout "15";
|
|
Acquire::https::Timeout "15";
|
|
Acquire::Retries "1";
|
|
APTCONF
|
|
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
|
|
# Why a wall-clock bound on each command: apt's Acquire timeouts are per-connection,
|
|
# so a dead mirror costs timeout x retries x every index file. Measured: the archive
|
|
# mirror stalled with zero bytes and the step burned 14m26s before the job bound
|
|
# killed it. `timeout` is the only thing that bounds the command as a whole.
|
|
# The update is already tolerant by design (see above), so bounding it just caps
|
|
# what a dead mirror can cost before the install runs against whatever index exists.
|
|
timeout 120 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.
|
|
timeout 300 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/__tests__/shell-ready-framework-example.test.ts \
|
|
src/main/pty/codex-shell-launch-preflight.test.ts \
|
|
src/main/pty/omp-shell-wrapper-alias-safety.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-startup-hook-user-config-equivalence.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/fish-query-reply-child-stdin.node-pty.test.ts \
|
|
src/shared/pty-reply-echo-shapes.node-pty.test.ts \
|
|
src/shared/startup-shell-portability.live-shell.test.ts \
|
|
src/shared/posix-command-path-lookup.test.ts
|
|
|
|
# Cache-key input changes would otherwise make every shard compile the same
|
|
# native addon concurrently. Prime the supported Node ABI before the matrix fans out.
|
|
test_native_cache:
|
|
name: prepare test native cache node 24
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.native_cache_changed == 'true'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
native-runtime: node
|
|
node-version: '24'
|
|
|
|
test:
|
|
needs: [code_paths, test_native_cache]
|
|
if: >-
|
|
always() &&
|
|
needs.code_paths.outputs.test == 'true' &&
|
|
(needs.test_native_cache.result == 'success' || needs.test_native_cache.result == 'skipped')
|
|
uses: ./.github/workflows/unit-tests.yml
|
|
with:
|
|
node_versions: '["24"]'
|
|
|
|
# Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
|
|
# would pay for it on every shard to run one file in whichever shard it landed in.
|
|
orcad_browser:
|
|
name: orcad browser provider
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.orcad_browser == 'true'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Why no native-runtime: the provider drives the prebuilt agent-browser binary
|
|
# shipped in node_modules and never touches node-pty.
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
|
|
# Why the runner's Google Chrome and not its chromium: Ubuntu 24.04 only ships an
|
|
# AppArmor userns profile for the Chrome .deb, so chromium dies with "No usable
|
|
# sandbox" and the provider passes no --no-sandbox. Why fail instead of skip: an
|
|
# unset ORCA_BROWSER_EXECUTABLE is exactly how this test went uncovered for so long.
|
|
- name: Resolve Chrome for the browser provider
|
|
run: |
|
|
set -euo pipefail
|
|
chrome="$(command -v google-chrome || command -v google-chrome-stable || true)"
|
|
if [ -z "$chrome" ]; then
|
|
echo "::error::No Google Chrome on the runner; the browser provider test would silently skip."
|
|
exit 1
|
|
fi
|
|
"$chrome" --version
|
|
echo "ORCA_BROWSER_EXECUTABLE=$chrome" >> "$GITHUB_ENV"
|
|
|
|
- name: Test external Chromium browser provider
|
|
run: |
|
|
pnpm exec vitest run --config config/vitest.config.ts \
|
|
src/main/orcad/external-chromium-browser-process.integration.test.ts
|
|
|
|
# Why its own job: it needs mobile/node_modules and a real browser, and the sharded `test`
|
|
# matrix would pay for both on every shard to run two files. Dark through Phase C: this proves
|
|
# `build:mobile-web:app` on every PR that touches the page, and ships nothing -- packaging still
|
|
# builds the Phase A bootstrap via build:mobile-web.
|
|
mobile_web_app:
|
|
name: mobile web app bundle
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.mobile_web_app == 'true'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Why no native-runtime: the builder is esbuild and the render check is a browser. Nothing
|
|
# in this job loads node-pty.
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
native-runtime: node
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
# The entry lives in mobile/ so one React resolves; without this every RN import is nothing.
|
|
- uses: ./.github/actions/install-mobile-dependencies
|
|
|
|
# Why the runner's Google Chrome and not a downloaded chromium: same reason as the orcad
|
|
# browser job -- Ubuntu 24.04 only ships an AppArmor userns profile for the Chrome .deb.
|
|
# Why fail instead of skip: a silently skipped render check is the failure this job exists
|
|
# to prevent.
|
|
- name: Resolve Chrome for the render check
|
|
run: |
|
|
set -euo pipefail
|
|
chrome="$(command -v google-chrome || command -v google-chrome-stable || true)"
|
|
if [ -z "$chrome" ]; then
|
|
echo "::error::No Google Chrome on the runner; the render check would silently skip."
|
|
exit 1
|
|
fi
|
|
"$chrome" --version
|
|
echo "ORCA_MOBILE_WEB_RENDER_BROWSER=$chrome" >> "$GITHUB_ENV"
|
|
|
|
# The drawer check runs on WebKit as well as Chrome, because the shell's iOS WebView is
|
|
# WebKit and the Chrome above cannot stand in for it. Downloaded rather than resolved from
|
|
# the runner: Ubuntu ships no WebKit build to point at.
|
|
- name: Install WebKit for the drawer check
|
|
run: pnpm exec playwright install --with-deps webkit
|
|
|
|
- name: Build and verify the app bundle
|
|
run: pnpm run build:mobile-web:app
|
|
|
|
# The bundling tests skip themselves where mobile dependencies are absent, which is how they
|
|
# stay green in the sharded `test` job. This is the job that installs them, so here a missing
|
|
# install has to fail rather than skip everything the job exists to run.
|
|
- name: Builder, override census and render checks
|
|
env:
|
|
ORCA_MOBILE_WEB_APP_DEPS_REQUIRED: '1'
|
|
run: |
|
|
pnpm exec vitest run --config config/vitest.config.ts \
|
|
config/scripts/build-mobile-web-app-bundle.test.mjs \
|
|
config/scripts/mobile-web-app-web-overrides.test.mjs \
|
|
config/scripts/mobile-web-app-render.test.mjs \
|
|
config/scripts/mobile-web-app-drawer-render.test.mjs \
|
|
config/scripts/mobile-web-app-agent-history-render.test.mjs \
|
|
config/scripts/mobile-web-app-tasks-render.test.mjs \
|
|
config/scripts/mobile-web-app-tasks-external-links.test.mjs \
|
|
config/scripts/mobile-web-app-files-external-links.test.mjs \
|
|
config/scripts/mobile-web-app-files-render.test.mjs \
|
|
config/scripts/mobile-web-app-page-closure-families.test.mjs
|
|
|
|
cross-version-wire:
|
|
name: cross-version wire compatibility
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.cross-version-wire == 'true'
|
|
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 compatibility journeys
|
|
run: >-
|
|
pnpm exec vitest run --config config/vitest.config.ts
|
|
tests/e2e/cross-version-wire/release-checkout.unit.test.ts
|
|
tests/e2e/cross-version-wire/cross-version-browser-placement.unit.test.ts
|
|
tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts
|
|
tests/e2e/cross-version-wire/reported-lossy-initial-snapshot.unit.test.ts
|
|
tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
|
|
tests/e2e/cross-version-wire/cross-version-worktree-identity-downgrade.unit.test.ts
|
|
tests/e2e/cross-version-wire/cross-version-session-tabs-retirement-proof.unit.test.ts
|
|
|
|
managed_hook_node18:
|
|
name: managed hooks on Node 18
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.managed_hook_node18 == 'true'
|
|
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
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.package == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Let the serial Docker gates reach their own deadlines and report cleanup failures.
|
|
timeout-minutes: 90
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Cache electron-builder downloads
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.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
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
|
# build resolves React Native and Expo from mobile/node_modules.
|
|
- uses: ./.github/actions/install-mobile-dependencies
|
|
|
|
# Why --no-file-parallelism: every file here launches a full Electron stack twice, and each
|
|
# probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other
|
|
# past those deadlines; serial, every probe owns the runner.
|
|
- name: Test Linux Electron lifecycle boundary
|
|
run: >-
|
|
xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts
|
|
--no-file-parallelism
|
|
src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts
|
|
src/main/browser/browser-route-tcp-egress.electron.test.ts
|
|
src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
|
src/main/browser/browser-route-h3-egress.electron.test.ts
|
|
src/main/browser/browser-route-dns-prefetch.electron.test.ts
|
|
|
|
- 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
|
|
|
|
# Why here and not inside "Build package inputs": this job assembles packaging inputs step by
|
|
# step instead of calling build:release, and electron-builder's beforePack guard hard-fails
|
|
# without out/mobile-web.
|
|
- name: Build mobile web bundle
|
|
run: pnpm run build:mobile-web
|
|
|
|
- name: Build native components
|
|
run: pnpm run build:native
|
|
|
|
- name: Install Linux package tooling
|
|
run: sudo apt-get update && sudo apt-get install -y cpio rpm
|
|
|
|
- name: Package unpacked app
|
|
env:
|
|
ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1'
|
|
# PR artifacts are only inspected locally; gzip avoids release-size xz compression.
|
|
run: >-
|
|
pnpm exec electron-builder --config config/electron-builder.config.cjs
|
|
--linux AppImage deb rpm --x64 --publish never
|
|
--config.deb.compression=gz --config.rpm.compression=gzip
|
|
|
|
- name: Verify root-package marker payloads
|
|
run: |
|
|
set -euo pipefail
|
|
version="$(node -p "require('./package.json').version")"
|
|
deb="dist/orca-ide_${version}_amd64.deb"
|
|
rpm="dist/orca-ide-${version}.x86_64.rpm"
|
|
test -s "$deb"
|
|
test -s "$rpm"
|
|
deb_marker="$(dpkg-deb --fsys-tarfile "$deb" | tar -xOf - ./opt/Orca/resources/package-type)"
|
|
rpm_marker="$(rpm2cpio "$rpm" | cpio --quiet --extract --to-stdout ./opt/Orca/resources/package-type)"
|
|
[[ "$deb_marker" == deb ]] || { echo "Expected deb marker, got: $deb_marker"; exit 1; }
|
|
[[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; }
|
|
|
|
- name: Verify headless serve signal shutdown
|
|
run: >-
|
|
node config/scripts/run-headless-serve-shutdown-docker.mjs
|
|
--appimage dist/orca-linux.AppImage --all-entrypoints
|
|
|
|
# A default container reproduces the hostile AppImage launch environment.
|
|
- name: Verify Linux CLI launch contract
|
|
run: node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage
|
|
|
|
- name: Smoke packaged CLI
|
|
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked
|
|
|
|
- name: Smoke packaged hang watchdog worker
|
|
run: xvfb-run --auto-servernum node config/scripts/smoke-packaged-hang-watchdog-worker.mjs --app-dir=dist/linux-unpacked
|
|
|
|
package_windows:
|
|
name: package (windows)
|
|
needs: [code_paths]
|
|
if: needs.code_paths.outputs.package_windows == 'true'
|
|
runs-on: windows-2022
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- 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-
|
|
|
|
# Why persist-native-cache false: this job later rebuilds the same path for
|
|
# Electron. A post-job save would store the Electron ABI under the Node key.
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
id: deps
|
|
with:
|
|
native-runtime: node
|
|
persist-native-cache: 'false'
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
|
# build resolves React Native and Expo from mobile/node_modules.
|
|
- uses: ./.github/actions/install-mobile-dependencies
|
|
|
|
- name: Save compiled Node native modules
|
|
if: steps.deps.outputs.native-cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: |
|
|
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
|
native/windows-registry/build
|
|
node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build
|
|
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
|
|
|
# vitest runs here directly rather than through `pnpm test`, so the addon
|
|
# assertions only hold once install-node-dependencies has rebuilt natives.
|
|
- name: Test Windows-specific boundaries
|
|
run: >-
|
|
pnpm exec vitest run --config config/vitest.config.ts
|
|
config/scripts/rebuild-native-deps.test.mjs
|
|
config/scripts/rebuild-native-deps-windows-process-tree.test.mjs
|
|
config/scripts/rebuild-native-deps-node-pty.test.mjs
|
|
config/scripts/ensure-native-runtime-job-ownership.test.mjs
|
|
config/scripts/verify-packaged-node-pty-job-ownership.test.mjs
|
|
config/scripts/windows-pe-machine.test.mjs
|
|
config/scripts/script-module-dependencies.test.mjs
|
|
src/main/windows-registry-addon.test.ts
|
|
config/scripts/windows-process-tree-gyp-path.test.mjs
|
|
config/scripts/windows-process-tree-gyp-rebuild.test.mjs
|
|
config/scripts/package-electron-runtime-contract.test.mjs
|
|
config/scripts/electron-builder-runtime-resources.test.mjs
|
|
src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts
|
|
src/main/browser/browser-route-tcp-egress.electron.test.ts
|
|
src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
|
src/main/browser/browser-route-h3-egress.electron.test.ts
|
|
src/main/browser/browser-route-dns-prefetch.electron.test.ts
|
|
src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts
|
|
src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts
|
|
src/shared/child-process/windows-command-line.win32.test.ts
|
|
src/shared/child-process/windows-cmd-shim-resolution.test.ts
|
|
src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts
|
|
src/main/agent-hooks/windows-hook-payload-delivery.test.ts
|
|
src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts
|
|
src/main/codex/windows-hook-command.test.ts
|
|
src/main/codex/windows-hook-upgrade.test.ts
|
|
src/main/windows/windows-pty-job.win32.test.ts
|
|
src/main/windows/windows-msys-job.win32.test.ts
|
|
src/main/windows/windows-host-job.win32.test.ts
|
|
src/main/windows/windows-process-tree-command-line-patch.test.ts
|
|
src/main/windows/windows-process-table-native-addon.win32.test.ts
|
|
src/main/windows-live-tree-kill.win32.test.ts
|
|
src/main/wsl/wsl-runner.test.ts
|
|
src/main/wsl/wsl-guest-environment.test.ts
|
|
src/main/wsl/wsl-invocation-boundary.test.ts
|
|
src/main/wsl/wsl-executable-path.win32.test.ts
|
|
src/main/wsl/wsl-w1-w3-contract.test.ts
|
|
src/shared/source-scan/source-tree-scan.test.ts
|
|
src/main/cli/wsl-cli-powershell-boundary.test.ts
|
|
src/main/computer/desktop-script-runtime-host.win32.test.ts
|
|
src/main/cursor/hook-service.test.ts
|
|
src/main/orca-profiles/profile-index-store.test.ts
|
|
src/main/startup/windows-install-dir-acl-repair.win32.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
|
|
src/shared/secure-path-windows-acl.win32.test.ts
|
|
src/main/runtime/unreadable-secret-store-preservation.win32.test.ts
|
|
src/main/ipc/pty-codex-account-attribution.test.ts
|
|
src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts
|
|
src/relay/windows-port-scan.win32.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: Cache Windows CLI launcher
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: native/windows-cli-launcher/.build
|
|
key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'config/scripts/build-windows-cli-launcher.mjs') }}
|
|
|
|
- name: Build package inputs
|
|
env:
|
|
ORCA_REUSE_WINDOWS_CLI_LAUNCHER: '1'
|
|
run: pnpm run build:release:parallel
|
|
|
|
- name: Restore compiled Electron native modules
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
|
|
native/windows-registry/build
|
|
node_modules/.pnpm/@vscode+windows-process-tre*/node_modules/@vscode/windows-process-tree/build
|
|
key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', '.github/actions/install-node-dependencies/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json') }}
|
|
|
|
- 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 Windows PTY native capability
|
|
run: pnpm run smoke:windows-pty-native-capability -- --exe=dist/win-unpacked/Orca.exe
|
|
|
|
- name: Smoke packaged CLI
|
|
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/win-unpacked
|
|
|
|
e2e:
|
|
name: e2e
|
|
needs: code_paths
|
|
if: needs.code_paths.outputs.e2e_should_run == 'true'
|
|
# Why: reusable e2e.yml only checkouts, builds, and uploads artifacts.
|
|
permissions:
|
|
contents: read
|
|
uses: ./.github/workflows/e2e.yml
|
|
with:
|
|
# The synthetic pull-request merge ref can disappear while this reusable
|
|
# workflow is queued. The head SHA is immutable and works for every PR.
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
test_files: ${{ needs.code_paths.outputs.test_files }}
|
|
ssh_source_changed: ${{ needs.code_paths.outputs.ssh_source_changed }}
|
|
|
|
# Why this is not in verify's needs: it is the first PR-gate run of a harness whose reliability
|
|
# is only known from nightly main runs (20/20 green, 2026-08-09..2026-08-29, p50 3m25s). It
|
|
# reports a red X on the PR without blocking, exactly like `e2e` above. Deliberately no
|
|
# continue-on-error: that renders the check green and hides the signal it exists to give. To
|
|
# make it blocking, add it to verify.needs, add TERMINAL_IME_NATIVE to the env below, and
|
|
# require `success || skipped` outside the strict loop — see the note on `e2e`.
|
|
terminal_ime_native:
|
|
name: real IME
|
|
needs: code_paths
|
|
if: needs.code_paths.outputs.native_ime_source_changed == 'true'
|
|
# Why: the reusable workflow only checks out, builds, and uploads artifacts.
|
|
permissions:
|
|
contents: read
|
|
uses: ./.github/workflows/terminal-ime-e2e.yml
|
|
|
|
windows_wsl:
|
|
name: real WSL terminal
|
|
needs: code_paths
|
|
if: needs.code_paths.outputs.wsl_source_changed == 'true'
|
|
permissions:
|
|
contents: read
|
|
uses: ./.github/workflows/windows-wsl-e2e.yml
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
|
|
verify:
|
|
if: always()
|
|
needs:
|
|
- code_paths
|
|
- static_analysis
|
|
- root_directory_guard
|
|
- typecheck
|
|
- git_compatibility
|
|
- codex_index_heal_contract
|
|
- xterm_patch_sync
|
|
- shell_contracts
|
|
- test
|
|
- orcad_browser
|
|
- mobile_web_app
|
|
- cross-version-wire
|
|
- 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:
|
|
CODE_PATHS: ${{ needs.code_paths.result }}
|
|
SHOULD_RUN: ${{ needs.code_paths.outputs.should_run }}
|
|
STATIC_ANALYSIS: ${{ needs.static_analysis.result }}
|
|
STATIC_ANALYSIS_SHOULD_RUN: ${{ needs.code_paths.outputs.static_analysis }}
|
|
ROOT_DIRECTORY_GUARD: ${{ needs.root_directory_guard.result }}
|
|
TYPECHECK: ${{ needs.typecheck.result }}
|
|
TYPECHECK_SHOULD_RUN: ${{ needs.code_paths.outputs.typecheck }}
|
|
GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }}
|
|
GIT_COMPATIBILITY_SHOULD_RUN: ${{ needs.code_paths.outputs.git_compatibility }}
|
|
CODEX_INDEX_HEAL_CONTRACT: ${{ needs.codex_index_heal_contract.result }}
|
|
CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN: ${{ needs.code_paths.outputs.codex_index_heal_contract }}
|
|
XTERM_PATCH_SYNC: ${{ needs.xterm_patch_sync.result }}
|
|
XTERM_PATCH_SYNC_SHOULD_RUN: ${{ needs.code_paths.outputs.xterm_patch_sync }}
|
|
SHELL_CONTRACTS: ${{ needs.shell_contracts.result }}
|
|
SHELL_CONTRACTS_SHOULD_RUN: ${{ needs.code_paths.outputs.shell_contracts }}
|
|
TEST: ${{ needs.test.result }}
|
|
TEST_SHOULD_RUN: ${{ needs.code_paths.outputs.test }}
|
|
ORCAD_BROWSER: ${{ needs.orcad_browser.result }}
|
|
ORCAD_BROWSER_SHOULD_RUN: ${{ needs.code_paths.outputs.orcad_browser }}
|
|
MOBILE_WEB_APP: ${{ needs.mobile_web_app.result }}
|
|
MOBILE_WEB_APP_SHOULD_RUN: ${{ needs.code_paths.outputs.mobile_web_app }}
|
|
CROSS_VERSION_WIRE: ${{ needs.cross-version-wire.result }}
|
|
CROSS_VERSION_WIRE_SHOULD_RUN: ${{ needs.code_paths.outputs.cross-version-wire }}
|
|
MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }}
|
|
MANAGED_HOOK_NODE18_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node18 }}
|
|
PACKAGE: ${{ needs.package.result }}
|
|
PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }}
|
|
PACKAGE_WINDOWS: ${{ needs.package_windows.result }}
|
|
PACKAGE_WINDOWS_SHOULD_RUN: ${{ needs.code_paths.outputs.package_windows }}
|
|
run: |
|
|
if [ "$CODE_PATHS" != "success" ]; then
|
|
exit 1
|
|
fi
|
|
if [ "$ROOT_DIRECTORY_GUARD" != "success" ]; then
|
|
exit 1
|
|
fi
|
|
if [ "$SHOULD_RUN" != "true" ]; then
|
|
echo "Docs-only change; expensive PR checks skipped."
|
|
fi
|
|
failed=0
|
|
check_job() {
|
|
local name="$1" result="$2" should="$3"
|
|
if [ "$should" = "true" ]; then
|
|
if [ "$result" != "success" ]; then
|
|
echo "$name: expected success, got $result"
|
|
failed=1
|
|
fi
|
|
else
|
|
if [ "$result" != "skipped" ]; then
|
|
echo "$name: expected skipped, got $result"
|
|
failed=1
|
|
fi
|
|
fi
|
|
}
|
|
# Require success when the PR has code-relevant changes
|
|
check_job static_analysis "$STATIC_ANALYSIS" "$STATIC_ANALYSIS_SHOULD_RUN"
|
|
check_job typecheck "$TYPECHECK" "$TYPECHECK_SHOULD_RUN"
|
|
check_job git_compatibility "$GIT_COMPATIBILITY" "$GIT_COMPATIBILITY_SHOULD_RUN"
|
|
check_job codex_index_heal_contract "$CODEX_INDEX_HEAL_CONTRACT" "$CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN"
|
|
check_job xterm_patch_sync "$XTERM_PATCH_SYNC" "$XTERM_PATCH_SYNC_SHOULD_RUN"
|
|
check_job shell_contracts "$SHELL_CONTRACTS" "$SHELL_CONTRACTS_SHOULD_RUN"
|
|
check_job test "$TEST" "$TEST_SHOULD_RUN"
|
|
check_job orcad_browser "$ORCAD_BROWSER" "$ORCAD_BROWSER_SHOULD_RUN"
|
|
check_job mobile_web_app "$MOBILE_WEB_APP" "$MOBILE_WEB_APP_SHOULD_RUN"
|
|
check_job cross-version-wire "$CROSS_VERSION_WIRE" "$CROSS_VERSION_WIRE_SHOULD_RUN"
|
|
check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN"
|
|
check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN"
|
|
check_job package_windows "$PACKAGE_WINDOWS" "$PACKAGE_WINDOWS_SHOULD_RUN"
|
|
exit "$failed"
|