Android Build & Publish #1
Workflow file for this run
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
| 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_BASE64: | |
| required: true | |
| ANDROID_KEYSTORE_PASSWORD: | |
| required: true | |
| ANDROID_KEY_ALIAS: | |
| required: true | |
| ANDROID_KEY_PASSWORD: | |
| required: true | |
| 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: ubuntu-24.04 | |
| 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@v6 | |
| - name: Setup Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: "temurin" | |
| java-version: ${{ env.JAVA_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.13" | |
| - 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: 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/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 bundleRelease | |
| - name: Build signed APK | |
| working-directory: packages/app/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 assembleRelease | |
| - name: Verify Android release artifacts | |
| run: | | |
| AAB_PATH="packages/app/android/app/build/outputs/bundle/release/app-release.aab" | |
| APK_PATH="packages/app/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/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/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/android/app/build/outputs/release-SHA256SUMS.txt | |
| cat packages/app/android/app/build/outputs/release-SHA256SUMS.txt | |
| - name: Stage sideload APK with canonical name | |
| run: | | |
| VERSION="${{ steps.version.outputs.name }}" | |
| APK_SRC="packages/app/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@v7 | |
| with: | |
| name: eliza-android-release | |
| path: | | |
| packages/app/android/app/build/outputs/bundle/release/app-release.aab | |
| packages/app/android/app/build/outputs/apk/release/app-release.apk | |
| packages/app/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/android/app/build/outputs/bundle/release/app-release.aab \ | |
| "Eliza-${VERSION}.aab" | |
| cp packages/app/android/app/build/outputs/apk/release/app-release.apk \ | |
| "Eliza-${VERSION}.apk" | |
| cp packages/app/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: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download AAB | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: eliza-android-release | |
| path: aab/ | |
| - name: Setup Ruby | |
| uses: ruby/setup-ruby@v1 | |
| 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 "ai.elizaos.app" | |
| - 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: ubuntu-24.04 | |
| 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" |