Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.patch whitespace=-blank-at-eol
36 changes: 36 additions & 0 deletions .github/RUNNERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ specific operator notes outside the public repository.
| Label | Used by | Purpose |
| --- | --- | --- |
| `openphone-build` | `release.yml` | Builds Pixel/device artifacts on a large Linux Android build host. |
| `openphone-emulator` | `emulator.yml` | Builds or reuses an OpenPhone SDK phone image, boots it in the Android emulator, and runs fast runtime smoke checks. |
| `openphone-device` | `eval.yml` | Runs evals against an authorized Android device connected over USB. |

## `openphone-build`
Expand Down Expand Up @@ -40,6 +41,39 @@ Register the runner with labels similar to:
Install it as a service according to GitHub's self-hosted runner instructions
for the host OS.

## `openphone-emulator`

The emulator runner needs:

- Linux x86_64 or macOS host with hardware acceleration suitable for Android
Emulator.
- Full Android build dependencies for the selected LineageOS branch.
- Android SDK Platform Tools and Emulator on `PATH`.
- `adb`, `emulator`, `node`, `python3`, `bash`, `git-lfs`, and `repo`.
- Repository checkout with `.worktree/android` or `OPENPHONE_ANDROID_DIR`
pointing at the synced Android tree.
- Enough disk for the Android tree, incremental build outputs, emulator system
image, and wiped userdata.

Prefer setting `OPENPHONE_ANDROID_DIR` to a persistent path outside the Actions
checkout. The workflow disables checkout cleaning to avoid deleting
`.worktree/android`, but keeping the Android tree outside the repository
workspace is easier to reason about on long-lived runners.

Register the runner with labels similar to:

```bash
./config.sh \
--url https://github.com/<org>/<repo> \
--token <registration-token> \
--labels self-hosted,openphone-emulator \
--name openphone-emulator-<host>
```

The workflow runs `scripts/run-emulator-smoke.sh`. On pull requests, it only
runs for same-repository branches because self-hosted runners must not execute
untrusted fork code with local Android build state.

## `openphone-device`

The device runner needs:
Expand All @@ -65,6 +99,8 @@ Register the runner with labels similar to:

- Android source checkouts and build output are too large for standard
GitHub-hosted runner disk limits.
- Headless emulator boot with OpenPhone system images needs a prepared Android
tree, SDK Emulator, acceleration, and enough local disk.
- Physical evals require a real phone connected over USB.

The normal CI workflow in `ci.yml` stays on `ubuntu-latest` and only runs
Expand Down
97 changes: 97 additions & 0 deletions .github/workflows/emulator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Emulator

on:
push:
branches:
- main
paths:
- ".github/workflows/emulator.yml"
- "integrations/adb/**"
- "integrations/cli/**"
- "integrations/mcp-server/**"
- "overlay/**"
- "patches/**"
- "runtime/**"
- "scripts/build.sh"
- "scripts/build-emulator.sh"
- "scripts/lab/**"
- "scripts/run-emulator*.sh"
- "scripts/run-assistant-task.sh"
workflow_dispatch:
inputs:
arch:
description: "Emulator architecture"
required: true
default: "x86_64"
type: choice
options:
- x86_64
- arm64
skip_build:
description: "Reuse the runner's existing emulator image"
required: true
default: "false"
type: choice
options:
- "false"
- "true"
assistant_smoke:
description: "Run local assistant task smoke"
required: true
default: "true"
type: choice
options:
- "true"
- "false"

permissions:
contents: read

concurrency:
group: emulator-${{ github.ref }}
cancel-in-progress: true

jobs:
emulator-smoke:
name: Boot emulator and run OpenPhone smoke
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: [self-hosted, openphone-emulator]
timeout-minutes: 180
env:
OPENPHONE_ANDROID_DIR: ${{ vars.OPENPHONE_ANDROID_DIR || '.worktree/android' }}
OPENPHONE_RELEASE: bp4a
OPENPHONE_EMULATOR_ASSISTANT_SMOKE: ${{ inputs.assistant_smoke == 'false' && '0' || '1' }}
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
clean: false

- name: Validate repository scaffold
run: |
set -euxo pipefail
./scripts/check.sh
git diff --check

- name: Run emulator smoke
env:
OPENPHONE_EMULATOR_BUILD: ${{ inputs.skip_build == 'true' && '0' || '1' }}
run: |
set -euxo pipefail
arch="${{ inputs.arch || vars.OPENPHONE_EMULATOR_ARCH || 'x86_64' }}"
slot="ci-${{ github.run_id }}-${{ github.run_attempt }}"
./scripts/lab/allocate-slot.sh --slot "$slot"
# shellcheck disable=SC1090
source ".worktree/lab/$slot/env"
./scripts/lab/smoke.sh \
--slot "$OPENPHONE_LAB_SLOT" \
--arch "$arch" \
--ci

- name: Upload emulator smoke artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: openphone-emulator-smoke
path: .worktree/lab/*/artifacts/emulator-smoke/
if-no-files-found: warn
236 changes: 236 additions & 0 deletions .github/workflows/gcp-lab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
name: GCP Lab

on:
workflow_dispatch:
inputs:
ref:
description: "Git ref/SHA to test. Defaults to this workflow SHA."
required: false
default: ""
repo_url:
description: "Git repo URL to clone on the lab VM"
required: true
default: "https://github.com/secondly-com/OpenPhone.git"
pr_number:
description: "Optional PR number for labels/naming"
required: false
default: ""
runtime:
description: "Comma-separated runtime intents"
required: true
default: "local"
arch:
description: "Emulator architecture"
required: true
default: "x86_64"
type: choice
options:
- x86_64
- arm64
machine_type:
description: "GCP machine type"
required: true
default: "c3-standard-22"
boot_disk_size:
description: "GCP boot disk size"
required: true
default: "1000GB"
boot_disk_type:
description: "GCP boot disk type"
required: true
default: "pd-ssd"
cache_mode:
description: "Android tree/cache strategy"
required: true
default: "scratch"
type: choice
options:
- scratch
- attach-disk
- snapshot
cache_disk:
description: "Existing cache disk name for attach-disk, or per-run disk name for snapshot"
required: false
default: ""
cache_source_snapshot:
description: "Warm cache snapshot name when cache_mode=snapshot"
required: false
default: ""
keep_vm:
description: "Leave the lab VM running for debugging"
required: true
default: "false"
type: choice
options:
- "false"
- "true"
pull_request_review:
types:
- submitted

permissions:
contents: read
id-token: write
pull-requests: read

concurrency:
group: gcp-lab-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: false

jobs:
gcp-lab-smoke:
name: GCP emulator lab smoke
if: >-
${{
github.event_name == 'workflow_dispatch' ||
(
github.event.review.state == 'approved' &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
)
}}
runs-on: ubuntu-latest
timeout-minutes: 480
env:
OPENPHONE_GCP_PROJECT: ${{ vars.OPENPHONE_GCP_PROJECT || 'openphone-lab' }}
OPENPHONE_GCP_REGION: ${{ vars.OPENPHONE_GCP_REGION || 'us-central1' }}
OPENPHONE_GCP_ZONE: ${{ vars.OPENPHONE_GCP_ZONE || 'us-central1-a' }}
OPENPHONE_GCP_MACHINE_TYPE: ${{ vars.OPENPHONE_GCP_MACHINE_TYPE || 'c3-standard-22' }}
OPENPHONE_GCP_BOOT_DISK_SIZE: ${{ vars.OPENPHONE_GCP_BOOT_DISK_SIZE || '1000GB' }}
OPENPHONE_GCP_BOOT_DISK_TYPE: ${{ vars.OPENPHONE_GCP_BOOT_DISK_TYPE || 'pd-ssd' }}
OPENPHONE_GCP_CACHE_SOURCE_SNAPSHOT: ${{ vars.OPENPHONE_GCP_CACHE_SOURCE_SNAPSHOT || '' }}
steps:
- name: Check out trusted workflow repository
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request_review' && github.event.pull_request.base.sha || github.sha }}

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Set up Android SDK
uses: android-actions/setup-android@v3
with:
packages: "platforms;android-35"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "24"

- name: Validate repository scaffold
run: |
set -euxo pipefail
./scripts/check.sh
git diff --check

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.OPENPHONE_GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.OPENPHONE_GCP_SERVICE_ACCOUNT }}

- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v2

- name: Select target
id: target
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "pull_request_review" ]]; then
pr_number="${{ github.event.pull_request.number }}"
ref="${{ github.event.pull_request.head.sha }}"
repo_url="${{ github.event.pull_request.head.repo.clone_url }}"
name="openphone-lab-pr-${pr_number}-${{ github.run_id }}-${{ github.run_attempt }}"
runtime="local"
arch="${{ vars.OPENPHONE_EMULATOR_ARCH || 'x86_64' }}"
machine_type="${OPENPHONE_GCP_MACHINE_TYPE}"
boot_disk_size="${OPENPHONE_GCP_BOOT_DISK_SIZE}"
boot_disk_type="${OPENPHONE_GCP_BOOT_DISK_TYPE}"
cache_source_snapshot="${OPENPHONE_GCP_CACHE_SOURCE_SNAPSHOT}"
if [[ -n "$cache_source_snapshot" ]]; then
cache_mode="snapshot"
else
cache_mode="scratch"
fi
cache_disk=""
keep_vm="false"
else
pr_number="${{ inputs.pr_number }}"
ref="${{ inputs.ref }}"
if [[ -z "$ref" ]]; then
ref="${GITHUB_SHA}"
fi
repo_url="${{ inputs.repo_url }}"
if [[ -z "$repo_url" ]]; then
repo_url="https://github.com/secondly-com/OpenPhone.git"
fi
name="openphone-lab-manual-${pr_number:-${{ github.run_id }}}-${{ github.run_attempt }}"
runtime="${{ inputs.runtime }}"
arch="${{ inputs.arch }}"
machine_type="${{ inputs.machine_type }}"
boot_disk_size="${{ inputs.boot_disk_size }}"
boot_disk_type="${{ inputs.boot_disk_type }}"
cache_mode="${{ inputs.cache_mode }}"
cache_disk="${{ inputs.cache_disk }}"
cache_source_snapshot="${{ inputs.cache_source_snapshot }}"
keep_vm="${{ inputs.keep_vm }}"
fi

{
echo "name=$name"
echo "ref=$ref"
echo "repo_url=$repo_url"
echo "runtime=$runtime"
echo "arch=$arch"
echo "machine_type=$machine_type"
echo "boot_disk_size=$boot_disk_size"
echo "boot_disk_type=$boot_disk_type"
echo "cache_mode=$cache_mode"
echo "cache_disk=$cache_disk"
echo "cache_source_snapshot=$cache_source_snapshot"
echo "keep_vm=$keep_vm"
} >> "$GITHUB_OUTPUT"

- name: Run GCP lab smoke
shell: bash
run: |
set -euxo pipefail
args=(
--name "${{ steps.target.outputs.name }}"
--repo-url "${{ steps.target.outputs.repo_url }}"
--ref "${{ steps.target.outputs.ref }}"
--project "${OPENPHONE_GCP_PROJECT}"
--zone "${OPENPHONE_GCP_ZONE}"
--machine-type "${{ steps.target.outputs.machine_type }}"
--boot-disk-size "${{ steps.target.outputs.boot_disk_size }}"
--boot-disk-type "${{ steps.target.outputs.boot_disk_type }}"
--cache-mode "${{ steps.target.outputs.cache_mode }}"
--arch "${{ steps.target.outputs.arch }}"
)
if [[ -n "${{ steps.target.outputs.cache_disk }}" ]]; then
args+=(--cache-disk "${{ steps.target.outputs.cache_disk }}")
fi
if [[ -n "${{ steps.target.outputs.cache_source_snapshot }}" ]]; then
args+=(--cache-source-snapshot "${{ steps.target.outputs.cache_source_snapshot }}")
fi
if [[ "${{ steps.target.outputs.keep_vm }}" == "true" ]]; then
args+=(--keep-vm)
fi
IFS=',' read -r -a runtimes <<< "${{ steps.target.outputs.runtime }}"
for runtime in "${runtimes[@]}"; do
[[ -n "$runtime" ]] || continue
args+=(--runtime "$runtime")
done
./scripts/lab/gcp/run-smoke.sh "${args[@]}"

- name: Upload GCP lab artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: openphone-gcp-lab
path: .worktree/gcp-lab/
if-no-files-found: warn
Loading
Loading