Merge pull request #121 from IQCoreTeam/feat/issue-118-progressive-un… #57
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. Scoped to exactly what the build below | |
| # bundles into the APK: the android app, plus the localhost (server) and webview (UI) dists | |
| # it repacks each run, plus the shared packages those two import. The cli and vscode surfaces | |
| # never ship in the APK, so their changes must NOT trigger a (wasteful, re-publishing) build. | |
| push: | |
| branches: [main] | |
| paths: | |
| - "surfaces/android/**" | |
| - "surfaces/localhost/**" | |
| - "surfaces/webview/**" | |
| - "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 | |
| # Single variant now: the app targets SDK 35 (our standard). proot loads the guest binaries | |
| # via its own loader mmap, which survives the targetSdk 35 SELinux W^X policy (verified on | |
| # device), so there is no separate legacy target to build. | |
| - name: Build signed debug APK | |
| working-directory: surfaces/android | |
| run: ./gradlew --no-daemon assembleDebug | |
| # 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/debug/app-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 build now | |
| # outputs app-debug.apk directly (single variant), so the asset NAME the public URL is keyed | |
| # by is already correct — upload it as-is and --clobber replaces the asset in place. | |
| - name: Update android-latest release | |
| if: env.ABI == 'arm64' | |
| run: | | |
| 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 surfaces/android/app/build/outputs/apk/debug/app-debug.apk --clobber |