# Separate workflow for building, packaging, and publishing the Android APK. # Runs independently from the desktop package-and-publish workflow. name: Package Android on: workflow_dispatch: inputs: publish: description: 'Publish to release (requires main branch)' type: boolean default: false workflow_run: workflows: ['Version Bump'] types: - completed branches: - main pull_request: branches: - main env: IS_PR: ${{ github.event_name == 'pull_request' }} SHOULD_PUBLISH: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish == 'true') }} PUBLISH_REPO: alphahumanxyz/alphahuman XGH_TOKEN: ${{ secrets.XGH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: get-version: runs-on: ubuntu-latest outputs: package-version: ${{ steps.extract-version.outputs.package-version }} tag-name: ${{ steps.extract-version.outputs.tag-name }} should-publish: ${{ steps.extract-version.outputs.should-publish }} release-name: ${{ steps.extract-version.outputs.release-name }} steps: - uses: actions/checkout@v4 with: fetch-depth: 1 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20.x - name: Extract version and tag id: extract-version run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") TAG_NAME="v${PACKAGE_VERSION}" RELEASE_NAME="AlphaHuman v${PACKAGE_VERSION}" echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT echo "release-name=$RELEASE_NAME" >> $GITHUB_OUTPUT echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT echo "Extracted version: $PACKAGE_VERSION" echo "Generated tag: $TAG_NAME" check-release: runs-on: ubuntu-latest needs: get-version outputs: release-id: ${{ steps.find-release.outputs.release-id }} should-skip: ${{ steps.find-release.outputs.should-skip }} steps: - name: Find existing release id: find-release env: TAG_NAME: ${{ needs.get-version.outputs.tag-name }} run: | if [ "$SHOULD_PUBLISH" != "true" ]; then echo "Publishing disabled (non-main branch or PR)" echo "should-skip=false" >> $GITHUB_OUTPUT echo "release-id=" >> $GITHUB_OUTPUT exit 0 fi echo "Looking for release with tag: $TAG_NAME" RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: Bearer $XGH_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/$PUBLISH_REPO/releases/tags/$TAG_NAME") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') if [ "$HTTP_CODE" = "200" ]; then RELEASE_ID=$(echo "$BODY" | jq -r '.id') IS_DRAFT=$(echo "$BODY" | jq -r '.draft') # Check if Android AAB is already uploaded AAB_EXISTS=$(echo "$BODY" | jq -r '.assets[]? | select(.name | endswith(".aab")) | .name' | head -1) if [ -n "$AAB_EXISTS" ]; then echo "Android AAB already exists in release: $AAB_EXISTS" echo "should-skip=true" >> $GITHUB_OUTPUT echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT else echo "Release found (ID: $RELEASE_ID, draft: $IS_DRAFT), no AAB yet" echo "should-skip=false" >> $GITHUB_OUTPUT echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT fi elif [ "$HTTP_CODE" = "404" ]; then echo "No release found for $TAG_NAME — will build artifact only (no publish)" echo "should-skip=false" >> $GITHUB_OUTPUT echo "release-id=" >> $GITHUB_OUTPUT else echo "Warning: Failed to check release (HTTP $HTTP_CODE)" echo "should-skip=false" >> $GITHUB_OUTPUT echo "release-id=" >> $GITHUB_OUTPUT fi build-android: name: Build and package Android AAB needs: [get-version, check-release] if: ${{ !failure() && !cancelled() && needs.check-release.outputs.should-skip != 'true' }} environment: ${{ github.ref == 'refs/heads/main' && 'Production' || '' }} runs-on: ubuntu-22.04 permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 1 submodules: true token: ${{ secrets.XGH_TOKEN_READ }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - name: Setup Node.js 24.x uses: actions/setup-node@v4 with: node-version: 24.x cache: 'yarn' - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android - name: Setup Java 17 uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Install Android NDK run: sdkmanager --install "ndk;27.0.12077973" - name: Set NDK path run: echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV - name: Cache Cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git key: ${{ runner.os }}-android-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-android-cargo- - name: Cache node modules id: yarn-cache uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-build- - name: Install dependencies if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile - name: Cache skills node modules id: skills-yarn-cache uses: actions/cache@v4 with: path: skills/node_modules key: ${{ runner.os }}-skills-${{ hashFiles('skills/yarn.lock') }} restore-keys: | ${{ runner.os }}-skills- - name: Install skills dependencies if: steps.skills-yarn-cache.outputs.cache-hit != 'true' run: cd skills && yarn install --frozen-lockfile - name: Build skills run: cd skills && yarn build - name: Build frontend run: yarn build env: NODE_ENV: production VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }} VITE_TELEGRAM_BOT_USERNAME: ${{ secrets.VITE_TELEGRAM_BOT_USERNAME }} VITE_TELEGRAM_BOT_ID: ${{ secrets.VITE_TELEGRAM_BOT_ID }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} VITE_SKILLS_GITHUB_REPO: ${{ vars.VITE_SKILLS_GITHUB_REPO }} VITE_DEBUG: ${{ vars.VITE_DEBUG }} - name: Initialize Tauri Android run: npx tauri android init - name: Decode keystore if: needs.get-version.outputs.should-publish != '' env: ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} run: | echo "$ANDROID_KEYSTORE" | base64 --decode > ${{ github.workspace }}/release.jks - name: Build Android AAB (signed) if: needs.get-version.outputs.should-publish != '' env: BASE_URL: ${{ vars.BASE_URL }} ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/release.jks ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: npx tauri android build --aab true - name: Build Android AAB (unsigned) if: needs.get-version.outputs.should-publish == '' env: BASE_URL: ${{ vars.BASE_URL }} run: npx tauri android build --aab true - name: Find AAB id: find-aab run: | AAB_ROOT="src-tauri/gen/android" echo "Searching for AABs in $AAB_ROOT..." find "$AAB_ROOT" -name '*.aab' -type f | sort AAB_PATH=$(find "$AAB_ROOT" -name '*.aab' -type f | head -1) if [ -z "$AAB_PATH" ]; then echo "No AAB found" exit 1 fi AAB_FILENAME=$(basename "$AAB_PATH") echo "Selected AAB: $AAB_PATH" echo "path=$AAB_PATH" >> $GITHUB_OUTPUT echo "filename=$AAB_FILENAME" >> $GITHUB_OUTPUT - name: Upload AAB artifact uses: actions/upload-artifact@v4 with: name: android-aab path: ${{ steps.find-aab.outputs.path }} - name: Upload AAB to release if: needs.get-version.outputs.should-publish == 'true' && needs.check-release.outputs.release-id != '' env: RELEASE_ID: ${{ needs.check-release.outputs.release-id }} run: | curl -X POST -H "Authorization: Bearer $XGH_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary "@${{ steps.find-aab.outputs.path }}" \ "https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=${{ steps.find-aab.outputs.filename }}" # Requires secret GOOGLE_PLAY_SERVICE_ACCOUNT_JSON and vars.GOOGLE_PLAY_UPLOAD_ENABLED = 'true' # Track: internal | alpha | beta | production (use vars.GOOGLE_PLAY_TRACK to override) - name: Upload AAB to Google Play if: needs.get-version.outputs.should-publish == 'true' && vars.GOOGLE_PLAY_UPLOAD_ENABLED == 'true' uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} packageName: com.alphahuman.app releaseFiles: ${{ steps.find-aab.outputs.path }} track: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }} releaseName: AlphaHuman v${{ needs.get-version.outputs.package-version }} status: completed