name: Android Build & Publish on: workflow_call: inputs: track: description: "Play Store track" required: false type: string default: internal version_name: description: "Version name (e.g. 2.0.0-beta.0)" required: true type: string secrets: # ANDROID_KEYSTORE_* are downgraded to `required: false` so the # reusable-workflow secret contract does not block startup when # called from release-all.yml / release-orchestrator.yml with # `secrets: inherit`. The job body's signing step (gated on # ANDROID_KEYSTORE_BASE64 being non-empty) skips with a clear # warning when the secrets are absent, instead of aborting the # whole release pipeline at workflow startup with no logs. Same # pattern as publish-apt-repo.yml (PR #7976) and update-homebrew.yml. ANDROID_KEYSTORE_BASE64: required: false ANDROID_KEYSTORE_PASSWORD: required: false ANDROID_KEY_ALIAS: required: false ANDROID_KEY_PASSWORD: required: false PLAY_STORE_SERVICE_ACCOUNT_JSON: required: false workflow_dispatch: inputs: track: description: "Play Store track" type: choice options: - internal - beta - production default: internal version_name: description: "Version name (e.g. 2.0.0-beta.0)" required: false type: string concurrency: group: android-release-${{ github.ref }} cancel-in-progress: false permissions: contents: write env: JAVA_VERSION: "21" NODE_VERSION: "24.15.0" jobs: build-aab: name: Build Signed AAB runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} outputs: version_name: ${{ steps.version.outputs.name }} version_code: ${{ steps.version.outputs.code }} play_store_ready: ${{ steps.play_store_credentials.outputs.ready }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Setup Java uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 with: distribution: "temurin" java-version: ${{ env.JAVA_VERSION }} - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: "canary" - name: Determine version id: version run: | if [[ -n "${{ inputs.version_name }}" ]]; then VERSION_NAME="${{ inputs.version_name }}" else VERSION_NAME=$(node -p "require('./package.json').version") fi # Generate a monotonic Android version code from semver. # 2.0.0-alpha.82 -> 200001082, 2.0.0-beta.0 -> 200003000, # 2.0.0 -> 200009000, 2.1.3 -> 201039000. VERSION_CODE=$(node - "$VERSION_NAME" <<'NODE' const version = process.argv[2]; const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+)(?:\.(\d+))?)?$/); if (!match) { console.error(`Invalid semver version: ${version}`); process.exit(1); } const [, majorRaw, minorRaw, patchRaw, prerelease, prereleaseRaw = "0"] = match; const major = Number(majorRaw); const minor = Number(minorRaw); const patch = Number(patchRaw); const prereleaseNumber = Number(prereleaseRaw); if ( !Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch) || !Number.isInteger(prereleaseNumber) || prereleaseNumber < 0 || prereleaseNumber > 999 ) { console.error(`Invalid Android version component in ${version}`); process.exit(1); } const channelOffsets = { alpha: 1000, beta: 3000, rc: 5000, nightly: 7000, }; const channelOffset = prerelease ? (channelOffsets[prerelease] ?? 8000) : 9000; const code = major * 100000000 + minor * 1000000 + patch * 10000 + channelOffset + prereleaseNumber; if (code > 2100000000) { console.error(`Android versionCode ${code} exceeds Play Store limit`); process.exit(1); } console.log(code); NODE ) echo "name=$VERSION_NAME" >> "$GITHUB_OUTPUT" echo "code=$VERSION_CODE" >> "$GITHUB_OUTPUT" echo "Version: $VERSION_NAME (code: $VERSION_CODE)" - name: Check Play Store credentials id: play_store_credentials env: PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} run: | if [[ -n "$PLAY_STORE_SERVICE_ACCOUNT_JSON" ]]; then echo "ready=true" >> "$GITHUB_OUTPUT" else echo "ready=false" >> "$GITHUB_OUTPUT" echo "PLAY_STORE_SERVICE_ACCOUNT_JSON is not configured; signed APK/AAB artifacts will still attach to GitHub Release, but Play Store upload will be skipped." fi - name: Install dependencies run: bun install - name: Build workspace web runtime packages run: | bun run --cwd packages/core build bun run --cwd packages/shared build bun run --cwd packages/vault build bun run --cwd packages/cloud/sdk build bun run --cwd packages/app-core build:dist - name: Prepare Android platform working-directory: packages/app run: bun run build:android:cloud - name: Decode keystore run: | echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > /tmp/elizaos-upload.jks env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - name: Run Android signing preflight working-directory: packages/app env: ELIZAOS_KEYSTORE_PATH: /tmp/elizaos-upload.jks ELIZAOS_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ELIZAOS_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ELIZAOS_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | bun run preflight:android:sideload missing=() [[ ! -s "$ELIZAOS_KEYSTORE_PATH" ]] && missing+=("ANDROID_KEYSTORE_BASE64") [[ -z "$ELIZAOS_KEYSTORE_PASSWORD" ]] && missing+=("ANDROID_KEYSTORE_PASSWORD") [[ -z "$ELIZAOS_KEY_ALIAS" ]] && missing+=("ANDROID_KEY_ALIAS") [[ -z "$ELIZAOS_KEY_PASSWORD" ]] && missing+=("ANDROID_KEY_PASSWORD") if [[ ${#missing[@]} -gt 0 ]]; then echo "::error::Missing Android release signing inputs: ${missing[*]}" exit 1 fi - name: Run Play Store preflight if: steps.play_store_credentials.outputs.ready == 'true' working-directory: packages/app env: ELIZAOS_KEYSTORE_PATH: /tmp/elizaos-upload.jks ELIZAOS_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ELIZAOS_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ELIZAOS_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} run: bun run preflight:android:store - name: Build signed AAB working-directory: packages/app-core/platforms/android env: ELIZAOS_VERSION_NAME: ${{ steps.version.outputs.name }} ELIZAOS_VERSION_CODE: ${{ steps.version.outputs.code }} ELIZAOS_KEYSTORE_PATH: /tmp/elizaos-upload.jks ELIZAOS_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ELIZAOS_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ELIZAOS_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | chmod +x gradlew ./gradlew -PelizaCloudBuild=true -PelizaStripAgentAssets=true bundleRelease - name: Build signed APK working-directory: packages/app-core/platforms/android env: ELIZAOS_VERSION_NAME: ${{ steps.version.outputs.name }} ELIZAOS_VERSION_CODE: ${{ steps.version.outputs.code }} ELIZAOS_KEYSTORE_PATH: /tmp/elizaos-upload.jks ELIZAOS_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ELIZAOS_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ELIZAOS_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | chmod +x gradlew ./gradlew -PelizaCloudBuild=true -PelizaStripAgentAssets=true assembleRelease - name: Verify Android release artifacts run: | AAB_PATH="packages/app-core/platforms/android/app/build/outputs/bundle/release/app-release.aab" APK_PATH="packages/app-core/platforms/android/app/build/outputs/apk/release/app-release.apk" if [[ ! -f "$AAB_PATH" ]]; then echo "ERROR: AAB not found at $AAB_PATH" ls -la packages/app-core/platforms/android/app/build/outputs/bundle/release/ 2>/dev/null || echo "Release dir not found" exit 1 fi if [[ ! -f "$APK_PATH" ]]; then echo "ERROR: APK not found at $APK_PATH" ls -la packages/app-core/platforms/android/app/build/outputs/apk/release/ 2>/dev/null || echo "Release dir not found" exit 1 fi echo "AAB size: $(du -h "$AAB_PATH" | cut -f1)" echo "APK size: $(du -h "$APK_PATH" | cut -f1)" file "$AAB_PATH" file "$APK_PATH" sha256sum "$AAB_PATH" "$APK_PATH" > packages/app-core/platforms/android/app/build/outputs/release-SHA256SUMS.txt cat packages/app-core/platforms/android/app/build/outputs/release-SHA256SUMS.txt - name: Stage sideload APK with canonical name run: | VERSION="${{ steps.version.outputs.name }}" APK_SRC="packages/app-core/platforms/android/app/build/outputs/apk/release/app-release.apk" SIDELOAD_APK="elizaos-android-${VERSION}-release.apk" cp "$APK_SRC" "$SIDELOAD_APK" sha256sum "$SIDELOAD_APK" > "${SIDELOAD_APK}.sha256" echo "Sideload APK: $SIDELOAD_APK" echo "SHA256: $(cat "${SIDELOAD_APK}.sha256")" - name: Upload Android release artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: eliza-android-release path: | packages/app-core/platforms/android/app/build/outputs/bundle/release/app-release.aab packages/app-core/platforms/android/app/build/outputs/apk/release/app-release.apk packages/app-core/platforms/android/app/build/outputs/release-SHA256SUMS.txt elizaos-android-${{ steps.version.outputs.name }}-release.apk elizaos-android-${{ steps.version.outputs.name }}-release.apk.sha256 retention-days: 90 - name: Attach Android artifacts to GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.version.outputs.name }}" TAG="v${VERSION}" SIDELOAD_APK="elizaos-android-${VERSION}-release.apk" if ! gh release view "$TAG" >/dev/null 2>&1; then echo "::error::No GitHub release found for $TAG; cannot attach AAB." exit 1 fi cp packages/app-core/platforms/android/app/build/outputs/bundle/release/app-release.aab \ "Eliza-${VERSION}.aab" cp packages/app-core/platforms/android/app/build/outputs/apk/release/app-release.apk \ "Eliza-${VERSION}.apk" cp packages/app-core/platforms/android/app/build/outputs/release-SHA256SUMS.txt \ "Eliza-${VERSION}.android.SHA256SUMS.txt" gh release upload "$TAG" \ "Eliza-${VERSION}.aab" \ "Eliza-${VERSION}.apk" \ "Eliza-${VERSION}.android.SHA256SUMS.txt" \ "${SIDELOAD_APK}" \ "${SIDELOAD_APK}.sha256" \ --clobber - name: Clean up keystore if: always() run: rm -f /tmp/elizaos-upload.jks publish-play-store: name: Publish to Play Store needs: build-aab if: needs.build-aab.outputs.play_store_ready == 'true' runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Download AAB uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: eliza-android-release path: aab/ - name: Setup Ruby uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 with: ruby-version: "4.0.3" bundler-cache: true working-directory: packages/app-core/platforms/android - name: Decode Play Store service account key run: | echo "${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}" | base64 -d > /tmp/play-store-key.json - name: Determine track id: track run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "track=${{ inputs.track }}" >> "$GITHUB_OUTPUT" elif echo "${{ needs.build-aab.outputs.version_name }}" | grep -qE '(beta|rc)'; then echo "track=internal" >> "$GITHUB_OUTPUT" else echo "track=production" >> "$GITHUB_OUTPUT" fi - name: Upload to Play Store working-directory: packages/app-core/platforms/android env: PLAY_STORE_JSON_KEY: /tmp/play-store-key.json run: | bundle exec fastlane supply \ --aab "$GITHUB_WORKSPACE/aab/app-release.aab" \ --track "${{ steps.track.outputs.track }}" \ --json_key "$PLAY_STORE_JSON_KEY" \ --package_name "app.eliza" - name: Clean up credentials if: always() run: rm -f /tmp/play-store-key.json summary: name: Release Summary needs: [build-aab, publish-play-store] if: always() runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} steps: - name: Summary run: | { echo "## Android Release Summary" echo "" echo "**Version:** ${{ needs.build-aab.outputs.version_name }} (code: ${{ needs.build-aab.outputs.version_code }})" echo "" echo "| Step | Status |" echo "|------|--------|" echo "| Build AAB | ${{ needs.build-aab.result }} |" echo "| Publish to Play Store | ${{ needs.publish-play-store.result }} |" } >> "$GITHUB_STEP_SUMMARY" - name: Require Android release succeeded run: | failed=0 if [[ "${{ needs.build-aab.result }}" != "success" ]]; then echo "::error::Signed AAB build result was '${{ needs.build-aab.result }}', expected success" failed=1 fi if [[ "${{ needs.build-aab.outputs.play_store_ready }}" == "true" && "${{ needs.publish-play-store.result }}" != "success" ]]; then echo "::error::Play Store publish result was '${{ needs.publish-play-store.result }}', expected success" failed=1 fi exit "$failed"