From cb424caa9d08507f593f1954da62410844069904 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 15:10:47 +0000 Subject: [PATCH 1/2] ci: build APK once, lint in parallel, iOS/E2E manual only - build.yml: setup-gradle + --build-cache instead of actions/cache (no task ever hit the cache); lintVital moved to own parallel lint job - E2E: manual only (build.yml dispatch, "e2e" input); reuses build's APK (arm64-v8a + x86_64 on that run) instead of its own 4-ABI build (~16 min) - ios-ci.yml: macOS build only on workflow_dispatch; typecheck/test job still on PRs in case branch protection requires it - cancel superseded PR builds; build on PRs to any base (stacked PRs) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Sde9uW6eic8ezJagyJc29R --- .github/workflows/activation-e2e.yml | 69 ++++++----------------- .github/workflows/build.yml | 84 +++++++++++++++++++++++----- .github/workflows/ios-ci.yml | 11 ++-- 3 files changed, 94 insertions(+), 70 deletions(-) diff --git a/.github/workflows/activation-e2e.yml b/.github/workflows/activation-e2e.yml index 03170f5e..dc396018 100644 --- a/.github/workflows/activation-e2e.yml +++ b/.github/workflows/activation-e2e.yml @@ -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: @@ -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: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca70e7ba..97ae36ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -50,16 +62,9 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 - - 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- + # Replaces actions/cache: saves a fresh cache from main (PRs only read it) and keeps the Gradle + # build cache, so unchanged library modules are not recompiled. + - uses: gradle/actions/setup-gradle@v4 - name: Install dependencies run: npm install --legacy-peer-deps @@ -100,12 +105,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 @@ -114,6 +125,49 @@ 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 + + - 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') diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 4657cee8..b4deb567 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -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 @@ -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: @@ -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 From 8c6f47207fb392ada5254eb6396e12530a4385f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 15:24:30 +0000 Subject: [PATCH 2/2] ci: save Gradle cache from PRs too PR build ran cold (no main cache yet; setup-gradle read-only on PRs): +2.5 min dep resolution. Later pushes on a PR now start warm. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Sde9uW6eic8ezJagyJc29R --- .github/workflows/build.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97ae36ae..e421d35f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,9 +62,12 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 - # Replaces actions/cache: saves a fresh cache from main (PRs only read it) and keeps the Gradle - # build cache, so unchanged library modules are not recompiled. + # 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: + cache-read-only: false - name: Install dependencies run: npm install --legacy-peer-deps @@ -145,6 +148,8 @@ jobs: 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