Add legacy/modern targetSdk flavors, extract guest-launch seam, fix c… #45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build a SIGNED debug APK and upload it as an artifact, so teammates can install a | |
| # team-signed build (Google Drive works) WITHOUT having the shared keystore locally. | |
| # | |
| # Why a separate workflow from android-assets: the heavy part (the ~1 GB aarch64 rootfs, | |
| # built under QEMU, ~15 min) rarely changes — only when Ubuntu/node/claude/codex change. | |
| # A UI/server/Kotlin update does NOT need a new rootfs. So this workflow REUSES the latest | |
| # android-assets artifact's rootfs + jniLibs and only rebuilds the lightweight server | |
| # bundle (webview + localhost dist) before signing. No QEMU here -> fast (~5 min). | |
| # | |
| # Run android-assets when the rootfs deps change; run THIS for every UI/app change. | |
| # | |
| # Requires one repo secret: ANDROID_DEBUG_KEYSTORE_B64 = base64 of the shared debug | |
| # keystore (`base64 -i surfaces/android/agentnet-debug.keystore`). The signed APK never | |
| # contains the private key (only the public cert + signature), so the artifact is safe to | |
| # share. Manual / tag trigger only — never fork PRs, so the secret is never exposed to | |
| # untrusted code. | |
| name: android-apk | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| abi: | |
| description: "Target ABI" | |
| type: choice | |
| options: ["arm64", "x86_64"] | |
| default: "arm64" | |
| assets_run_id: | |
| description: "android-assets run id to reuse (blank = latest successful)" | |
| required: false | |
| default: "" | |
| # Auto-build a fresh signed APK whenever main gets an app/server/UI change, so the latest | |
| # team-signed APK is always waiting as an artifact. Paths-filtered so doc-only pushes don't | |
| # build. (Public repo -> GitHub-hosted minutes are free.) | |
| push: | |
| branches: [main] | |
| paths: | |
| - "surfaces/**" | |
| - "packages/**" | |
| - ".github/workflows/android-apk.yml" | |
| # One build per ref: a newer push to main cancels an in-progress build so only the latest wins. | |
| concurrency: | |
| group: android-apk-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| actions: read | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| env: | |
| ABI: ${{ github.event.inputs.abi || 'arm64' }} | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Fail fast if the signing secret is missing — otherwise gradle would sign with an | |
| # auto-generated key whose SHA-1 isn't registered for Drive, producing a silently | |
| # wrong "team" APK. (Passed via env, not interpolated into the command.) | |
| - name: Require signing secret | |
| env: | |
| KS_B64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_B64 }} | |
| run: | | |
| if [ -z "$KS_B64" ]; then | |
| echo "::error::ANDROID_DEBUG_KEYSTORE_B64 is not set. Add it as a repo secret: base64 -i surfaces/android/agentnet-debug.keystore" | |
| exit 1 | |
| fi | |
| # Lightweight, arch-independent JS — rebuilt every run so the APK carries the latest UI. | |
| - uses: pnpm/action-setup@v4 | |
| with: { version: 9 } | |
| - uses: actions/setup-node@v4 | |
| with: { node-version: 22, cache: pnpm } | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm --filter agentnet-localhost build | |
| - run: pnpm --filter agentnet-webview build | |
| # Reuse the rootfs + jniLibs from the latest android-assets run (no QEMU rebuild). | |
| - name: Resolve android-assets run | |
| id: assets | |
| run: | | |
| ID="${{ github.event.inputs.assets_run_id }}" | |
| if [ -z "$ID" ]; then | |
| # Pick the newest successful android-assets run whose artifact is STILL LIVE. | |
| # A run can be "success" yet have an expired artifact (retention window passed); | |
| # blindly taking the latest success then fails the download with a cryptic | |
| # "no valid artifacts found". Walk recent successes and take the first whose | |
| # android-assets-$ABI artifact hasn't expired. | |
| for cand in $(gh run list --workflow=android-assets.yml --status success -L 15 \ | |
| --json databaseId -q '.[].databaseId' || true); do | |
| live=$(gh api "repos/${{ github.repository }}/actions/runs/$cand/artifacts" \ | |
| -q ".artifacts[] | select(.name==\"android-assets-${ABI}\" and .expired==false) | .id" \ | |
| 2>/dev/null | head -1 || true) | |
| if [ -n "$live" ]; then ID="$cand"; break; fi | |
| done | |
| fi | |
| if [ -z "$ID" ]; then | |
| echo "::error::No android-assets run with a live android-assets-${ABI} artifact (they expire). Run the android-assets workflow to rebuild the rootfs, then re-run this." | |
| exit 1 | |
| fi | |
| echo "Using android-assets run $ID" | |
| echo "id=$ID" >> "$GITHUB_OUTPUT" | |
| - name: Download rootfs + jniLibs | |
| run: gh run download "${{ steps.assets.outputs.id }}" -n "android-assets-${ABI}" -D ci-assets | |
| - name: Stage assets into the source tree | |
| run: | | |
| set -e | |
| # assets/ is gitignored (its tars never get committed), so it doesn't exist on a | |
| # fresh checkout — create it before copying. | |
| mkdir -p surfaces/android/app/src/main/assets | |
| rm -rf surfaces/android/app/src/main/jniLibs | |
| cp -R ci-assets/jniLibs surfaces/android/app/src/main/jniLibs | |
| cp ci-assets/assets/rootfs-*.tar surfaces/android/app/src/main/assets/ | |
| # Repack the server bundle FRESH from this run's dist (don't reuse the artifact's | |
| # stale agentnet-server.tar) so the APK carries the current UI. | |
| STAGE="$(mktemp -d)/server-bundle" | |
| mkdir -p "$STAGE/webview" | |
| cp -R surfaces/localhost/dist/. "$STAGE/" | |
| cp -R surfaces/webview/dist/. "$STAGE/webview/" | |
| tar -cf surfaces/android/app/src/main/assets/agentnet-server.tar -C "$STAGE" . | |
| ls -lh surfaces/android/app/src/main/assets/ | |
| - uses: actions/setup-java@v4 | |
| with: { distribution: temurin, java-version: 17 } | |
| # Decode the shared keystore into the path build.gradle.kts expects. Removed at job end; | |
| # GitHub-hosted runners are ephemeral and isolated. | |
| - name: Install shared debug keystore | |
| env: | |
| KS_B64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_B64 }} | |
| run: printf '%s' "$KS_B64" | base64 -d > surfaces/android/agentnet-debug.keystore | |
| # Ship the `legacy` flavor (targetSdk 28): it keeps the pre-29 W^X exemption that runs | |
| # node + the proot guest's binaries from app storage, and is the proven, on-device-tested | |
| # variant. The `modern` flavor (targetSdk 35) is validated on hardware but not yet the | |
| # public download — build it explicitly here only once it is. Do NOT use bare | |
| # `assembleDebug`: with flavors it builds BOTH variants and writes to per-flavor dirs, | |
| # so the output path below would no longer resolve. | |
| - name: Build signed debug APK | |
| working-directory: surfaces/android | |
| run: ./gradlew --no-daemon assembleLegacyDebug | |
| # SHA-1 is a public fingerprint (not the key), safe to log — proves the team key signed it. | |
| - name: Print signing SHA-1 | |
| working-directory: surfaces/android | |
| run: ./gradlew --no-daemon :app:signingReport | grep -E "Variant|SHA1" | head -20 | |
| - name: Remove keystore | |
| if: always() | |
| run: rm -f surfaces/android/agentnet-debug.keystore | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: agentnet-apk-${{ env.ABI }} | |
| path: surfaces/android/app/build/outputs/apk/legacy/debug/app-legacy-debug.apk | |
| retention-days: 7 | |
| if-no-files-found: error | |
| # Publish to the rolling `android-latest` release so the public download link | |
| # (.../releases/download/android-latest/app-debug.apk) always serves the newest build. | |
| # arm64 only, so a manual x86_64 build doesn't clobber the shareable arm64 APK. | |
| # The flavor split names the file app-legacy-debug.apk, but the public link people already | |
| # have points at app-debug.apk — upload it under that display name (gh's `file#name` syntax) | |
| # so the shared URL keeps working. | |
| - name: Update android-latest release | |
| if: env.ABI == 'arm64' | |
| run: | | |
| APK=surfaces/android/app/build/outputs/apk/legacy/debug/app-legacy-debug.apk | |
| gh release view android-latest >/dev/null 2>&1 \ | |
| || gh release create android-latest --title "AgentNet Android (latest)" --latest \ | |
| --notes "Auto-built from main. Download app-debug.apk and install (allow unknown sources)." | |
| gh release upload android-latest "$APK#app-debug.apk" --clobber |