mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* fix(mobile): name a create's launch so a lost reply cannot build two workspaces `agent.launch` admits a caller-supplied `operationId` through a durable ledger, so exactly one execution happens and every replay returns the recorded answer. No client sent one, so the machinery was inert and the original defect was still live: mobile retries a lost create by design, and a retried launch built a second agent in a second workspace. Mobile now mints an operation id per create candidate and sends it whenever the host advertises `agent.launch.replay.v1`. The invariant is one operation per candidate. `computeAgentLaunchFingerprint` folds `target` whole, so the workspace name is inside the fingerprint; carrying one id across a name-collision bump would meet its own row under a differing fingerprint and refuse `agent_session_operation_conflict`, failing the create outright on the second candidate. The id is therefore minted beside `clientMutationId` at the top of each loop iteration and reused verbatim by every retry arm inside that candidate — never re-minted, since a new id is a new operation. Admission runs ahead of every effect, so `_invalid` / `_expired` / `_capacity` prove nothing launched: those re-send the same candidate unnamed rather than let bookkeeping fail a create the host would have performed. `_unknown` is the one refusal that is not safe to re-send, and it surfaces. Also corrects a false comment: the legacy path caches the whole launch under `clientMutationId`, so inside its 60s window a replay adds neither a workspace nor a surface, and outside it adds both — not "a second surface, never a second workspace". * fix(mobile): preserve launch identity on refusals * fix(mobile): use launch receipts to authorize replay * test: move mobile launch replay coverage outside node project * fix(mobile): enforce replay-safe launch delivery at the host * test: run mobile launch contracts in mobile checks * test: cover mobile launch contract workflow dependencies
177 lines
6.9 KiB
YAML
177 lines
6.9 KiB
YAML
name: Mobile Checks
|
|
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- opened
|
|
- synchronize
|
|
- reopened
|
|
- ready_for_review
|
|
paths:
|
|
- 'mobile/**'
|
|
# Mobile launch contracts exercise the real host dispatcher and durable receipt store.
|
|
- 'src/main/agent-launch/**'
|
|
- 'src/main/runtime/rpc/**'
|
|
- 'src/main/runtime/runtime-rpc/**'
|
|
- 'src/main/runtime/runtime-rpc.ts'
|
|
- 'src/main/runtime/device-registry.ts'
|
|
- 'src/main/runtime/orca-runtime.ts'
|
|
- 'src/main/runtime/agent-session-*.ts'
|
|
- 'src/main/native-chat/agent-session-wire/**'
|
|
- 'src/shared/agent-launch-*.ts'
|
|
- 'src/shared/agent-session-*.ts'
|
|
- 'src/shared/new-workspace/worktree-create-collision.ts'
|
|
# Why: the mobile terminal link parsers are conformance-tested against
|
|
# these shared fixtures; desktop-side fixture edits must re-run this suite.
|
|
- 'src/shared/terminal-file-link-conformance.ts'
|
|
# Why: mobile imports the negotiated capability names directly and records
|
|
# the whole capability read verbatim in its goldens, so a capability added
|
|
# desktop-side rewrites a mobile fixture and must re-run this suite.
|
|
- 'src/shared/protocol-version.ts'
|
|
# Why: mobile's rpc-params-contract.ts is a type-only re-export of the
|
|
# generated params catalog, and mobile/tsconfig.json includes **/*.ts. A
|
|
# schema edit anywhere under here changes mobile's types, so a desktop-only
|
|
# change can break mobile's typecheck with no other mobile signal.
|
|
- 'src/shared/rpc-contract/**'
|
|
# Why: this job holds the only checks that load the Fastfile, so edits to
|
|
# it or to the release workflow it guards must re-run them.
|
|
- '.github/workflows/mobile.yml'
|
|
- '.github/actions/install-node-dependencies/**'
|
|
- '.github/workflows/mobile-ios-release.yml'
|
|
# Why main too: a behaviour-change branch legitimately pins its own last fenced commit, and that
|
|
# commit only stops being reachable when the branch squash-merges. The pull_request run cannot
|
|
# see that; this one is where the pin guard finds it.
|
|
push:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- 'mobile/**'
|
|
- '.github/workflows/mobile.yml'
|
|
|
|
concurrency:
|
|
# Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one
|
|
# queues, whatever `cancel-in-progress` says, so one shared main group drops the middle merge of
|
|
# three -- and a pin that breaks there is exactly what this workflow now checks for.
|
|
group: mobile-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
verify:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
|
|
env:
|
|
# Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts
|
|
# from the Gemfile, which is how the release jobs could land on different
|
|
# fastlane versions in the first place. Fail here instead.
|
|
BUNDLE_FROZEN: 'true'
|
|
|
|
defaults:
|
|
run:
|
|
working-directory: mobile
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
# bundler-cache installs mobile/Gemfile.lock, so this job is also what
|
|
# proves the pinned fastlane the release workflow depends on still
|
|
# resolves — before a release run finds out.
|
|
- name: Setup Ruby and fastlane
|
|
uses: ruby/setup-ruby@v1
|
|
with:
|
|
ruby-version: '3.3'
|
|
bundler-cache: true
|
|
working-directory: mobile
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Typecheck
|
|
run: pnpm typecheck
|
|
|
|
- name: Test
|
|
run: pnpm test
|
|
|
|
- name: Test iOS release version resolution
|
|
run: ruby fastlane/ios_release_version_test.rb
|
|
|
|
- name: Test TestFlight lane arguments
|
|
run: ruby fastlane/fastfile_testflight_arguments_test.rb
|
|
|
|
# Why: nothing else in CI loads the Fastfile, so a syntax error, a broken
|
|
# require, or an undefined constant only surfaces mid-release — the
|
|
# ios-distribute job failed every run for six days that way. `lanes` just
|
|
# loads and lists, so it needs no App Store Connect credentials and makes
|
|
# no network calls to Apple.
|
|
- name: Smoke-check the Fastfile
|
|
env:
|
|
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
|
FASTLANE_OPT_OUT_USAGE: '1'
|
|
run: bundle exec fastlane lanes
|
|
|
|
- name: Lint
|
|
run: pnpm lint
|
|
|
|
- name: Check formatting
|
|
run: pnpm format:check
|
|
|
|
recording-pin:
|
|
name: RPC recording pin
|
|
runs-on: ubuntu-latest
|
|
|
|
defaults:
|
|
run:
|
|
working-directory: mobile
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# The ancestry verdict is read straight off history. On a shallow checkout
|
|
# `git merge-base --is-ancestor` answers from grafted parents, so the guard refuses to
|
|
# answer at all rather than reporting a pass it has no evidence for -- and the pinned tree
|
|
# below has to be checkable out.
|
|
fetch-depth: 0
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# Seconds. No `--ref`, so the pin is judged against the same tree it was read out of. On a
|
|
# pull request that is the merge preview, which already carries main's repins; judging the
|
|
# branch head instead fails every branch cut before the day's repin, and its instruction would
|
|
# tell the author to pin their own head -- creating the break this guard exists to catch. A
|
|
# branch that pins its own commit passes here and fails on the push after the squash, which is
|
|
# where the pin actually leaves the history.
|
|
- name: Check the recording pin is reachable
|
|
shell: bash
|
|
run: pnpm exec tsx scripts/rpc-recording-pin-guard.mts ancestry
|
|
|
|
# ~2 min locally for the record itself, so it is gated rather than run twice over. A pull
|
|
# request that moves none of the corpus, the manifest or the recorder cannot move this
|
|
# verdict away from the one the base commit already published, and `verify` replays the
|
|
# corpus against the branch tree in the meantime. A push to main has no `verify` job and is
|
|
# where a squash lands a spliced corpus, so there it always runs.
|
|
- name: Reproduce the corpus from the pinned tree
|
|
shell: bash
|
|
env:
|
|
PIN_GUARD_BASE: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
if [ -n "$PIN_GUARD_BASE" ]; then
|
|
pnpm exec tsx scripts/rpc-recording-pin-guard.mts reproduce --if-changed-since "$PIN_GUARD_BASE"
|
|
else
|
|
pnpm exec tsx scripts/rpc-recording-pin-guard.mts reproduce
|
|
fi
|