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
69 changes: 18 additions & 51 deletions .github/workflows/activation-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,29 @@ name: Activation E2E (Maestro)
# devices against a real server, so asserting on it here would be asserting
# on the harness rather than the app.

# Manual only: run build.yml from the Actions tab (workflow_dispatch, "Run the Maestro E2E flows").
# Its build job makes an arm64-v8a + x86_64 APK and this workflow installs that artifact instead
# of running a second Gradle build.
on:
push:
branches: [main]
paths:
- "app/**"
- "src/**"
- "tests/fixtures/**"
- ".maestro/**"
- ".github/workflows/activation-e2e.yml"
pull_request:
branches: [main]
paths:
- "app/**"
- "src/**"
- "tests/fixtures/**"
- ".maestro/**"
- ".github/workflows/activation-e2e.yml"
workflow_dispatch: {}
workflow_call:
inputs:
apk-artifact:
description: Name of the APK artifact uploaded earlier in the calling run
type: string
default: app-release

jobs:
activation-e2e:
runs-on: ubuntu-latest
# Same npm install + expo prebuild + assembleRelease + emulator pipeline as
# cua-smoke.yml, which budgets 60 min (emulator-boot-timeout alone is 10 min).
# Typical runs finish well under this; the ceiling just avoids flaky kills
# on cold Gradle caches.
timeout-minutes: 60
# emulator-boot-timeout alone is 10 min; no Gradle build here any more.
timeout-minutes: 30
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
# Runs the dependency-free mock server (TypeScript, natively); no npm install.
node-version: 24
cache: npm

- uses: actions/setup-java@v5
with:
Expand All @@ -85,39 +74,17 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Purge stale generated sources
# Same mitigation as cua-smoke.yml (whose Gradle cache entries this job
# shares — the cache key/restore-keys are identical): the restore-keys
# prefix fallback can restore a generated autolinking tree from a
# previous package id, making compileReleaseJavaWithJavac fail against
# the old package (ai.opencode.mobile vs cc.agentlabs.opencode). Delete
# generated sources so prebuild + Gradle regenerate them.
run: rm -rf android/app/build/generated android/build/generated android/app/build/intermediates

- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Install dependencies & build APK
env:
SENTRY_DISABLE_AUTO_UPLOAD: "true"
run: |
npm install --legacy-peer-deps
npx expo prebuild --platform android --no-install
keytool -genkey -v -keystore android/app/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
cd android && ./gradlew assembleRelease
- name: Download APK
uses: actions/download-artifact@v8
with:
name: ${{ inputs.apk-artifact }}
# Where scripts/run-e2e-flows.sh looks for it.
path: android/app/build/outputs/apk/release

- name: Start mock opencode servers
run: |
Expand Down
87 changes: 73 additions & 14 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ on:
push:
branches: [main]
tags: ["v*"]
# Any base branch, so PRs stacked on another PR get a build too.
pull_request:
branches: [main]
# Manual run: also runs the Maestro E2E flows (activation-e2e.yml) on this build's APK.
workflow_dispatch:
inputs:
e2e:
description: Run the Maestro E2E flows
type: boolean
default: true

# A new push to a PR cancels its running build.
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
Expand Down Expand Up @@ -50,16 +62,12 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v4

- name: Cache Gradle
uses: actions/cache@v5
# Replaces actions/cache and keeps the Gradle build cache, so unchanged library modules are not
# recompiled. Saved from PRs too (default is main only): a PR's next push starts warm instead of
# re-resolving every dependency (~2.5 min) until main has a cache.
- uses: gradle/actions/setup-gradle@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
cache-read-only: false

- name: Install dependencies
run: npm install --legacy-peer-deps
Expand Down Expand Up @@ -100,12 +108,18 @@ jobs:

- name: Build APK
working-directory: android
# arm64-v8a only: every current phone; the default list also compiles all native code for
# armeabi-v7a, x86 and x86_64. More heap/metaspace than gradle.properties (2 GiB / 512 MiB),
# where the daemon ran out of metaspace and restarted mid-build.
# arm64-v8a: every current phone. x86_64 too on manual E2E runs, so the E2E job can install
# this same APK on its x86_64 emulator instead of building its own (the default list also
# compiles armeabi-v7a and x86). More heap/metaspace than gradle.properties
# (2 GiB / 512 MiB), where the daemon ran out of metaspace and restarted mid-build.
# lintVital runs in the lint job, in parallel; skipping it here saves 1-3 min.
env:
ABIS: ${{ inputs.e2e && 'arm64-v8a,x86_64' || 'arm64-v8a' }}
run: >-
./gradlew assembleRelease
-PreactNativeArchitectures=arm64-v8a
--build-cache
-x lintVitalRelease
-PreactNativeArchitectures=$ABIS
"-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g"

- name: Upload APK artifact
Expand All @@ -114,6 +128,51 @@ jobs:
name: app-release
path: android/app/build/outputs/apk/release/app-release.apk

# Release lint (lintVital: fatal issues only), split off the build job so it runs alongside it.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm

- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17

- name: Setup Android SDK
uses: android-actions/setup-android@v4

- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false

- name: Install dependencies
run: npm install --legacy-peer-deps

- name: Expo prebuild
run: npx expo prebuild --platform android --no-install

- name: Lint
working-directory: android
run: >-
./gradlew lintVitalRelease
--build-cache
-PreactNativeArchitectures=arm64-v8a
"-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g"

# Manual runs only (emulator run is slow). Runs on the APK built above; no second Gradle build.
e2e:
needs: [build]
if: inputs.e2e
uses: ./.github/workflows/activation-e2e.yml
with:
apk-artifact: app-release

release:
needs: [build]
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/ios-ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# iOS build gate for pull requests and main.
# iOS build gate. The macOS build runs only by hand (see ios-build below).
#
# Requires NO Apple/EAS secrets: it validates the Expo config, exports the iOS JS
# bundle, generates the native project, installs CocoaPods, and compiles an
Expand All @@ -10,16 +10,16 @@

name: iOS CI

# NOTE: no paths-ignore here. "Typecheck and unit tests" is a REQUIRED status
# NOTE: no paths-ignore here. "Typecheck and unit tests" may be a REQUIRED status
# check in branch protection; a path-filtered required check never runs on
# docs-only PRs, leaving them permanently BLOCKED. Running the cheap Linux
# typecheck/test job (and the gated macOS build) on every PR keeps the required
# check satisfiable. See #143 (docs PR stuck) for why.
# typecheck/test job on every PR keeps the required check satisfiable. See #143.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch: {}

# Cancel superseded runs for the same ref (e.g. new push to an open PR).
concurrency:
Expand Down Expand Up @@ -55,6 +55,9 @@ jobs:
ios-build:
name: Unsigned iOS Simulator build
needs: test
# Manual only: the fork ships Android only, and this added ~7 min of macOS runner time to every
# PR and push. A skipped job still satisfies a required check. Run it from the Actions tab.
if: github.event_name == 'workflow_dispatch'
# macos-15 ships Xcode 16.x, which React Native 0.81 / Expo SDK 54 require.
runs-on: macos-15
timeout-minutes: 45
Expand Down
Loading