Release #33
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: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g., v1.0.0)' | |
| required: true | |
| type: string | |
| track: | |
| description: 'Play Store release track' | |
| required: false | |
| type: choice | |
| default: 'internal' | |
| options: | |
| - internal | |
| - alpha | |
| - beta | |
| - production | |
| status: | |
| description: 'Play Store release status' | |
| required: false | |
| type: choice | |
| default: 'draft' | |
| options: | |
| - draft | |
| - completed | |
| permissions: | |
| contents: write | |
| jobs: | |
| prepare: | |
| name: Prepare Release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| version_code: ${{ steps.version_info.outputs.version_code }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Determine version | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="${{ github.event.inputs.version }}" | |
| else | |
| VERSION="${{ github.ref_name }}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Version: $VERSION" | |
| - name: Extract version code | |
| id: version_info | |
| run: | | |
| # Extract version code from config.gradle | |
| VERSION_CODE=$(grep "versionCode:" config.gradle | sed 's/.*versionCode: \([0-9]*\).*/\1/') | |
| echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT | |
| echo "Version Code: $VERSION_CODE" | |
| build-apk: | |
| name: Build Release APK | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| - name: Decode Keystore | |
| if: ${{ vars.ENABLE_SIGNING == 'true' && env.KEYSTORE_BASE64 != '' }} | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| run: | | |
| echo "$KEYSTORE_BASE64" | base64 --decode > app/keystore.jks | |
| - name: Debug keystore info | |
| if: ${{ vars.ENABLE_SIGNING == 'true' }} | |
| run: | | |
| echo "Keystore file exists: $([ -f app/keystore.jks ] && echo 'Yes' || echo 'No')" | |
| echo "Keystore size: $([ -f app/keystore.jks ] && ls -la app/keystore.jks | awk '{print $5}' || echo 'N/A')" | |
| echo "Key alias configured: ${{ secrets.KEY_ALIAS != '' && 'Yes' || 'No' }}" | |
| - name: Build release APK | |
| run: | | |
| if [ "${{ vars.ENABLE_SIGNING }}" = "true" ] && [ -f "app/keystore.jks" ]; then | |
| echo "Building signed release APK" | |
| echo "Using key alias: ${{ secrets.KEY_ALIAS }}" | |
| ./gradlew assembleRelease \ | |
| -Pandroid.injected.signing.store.file=${{ github.workspace }}/app/keystore.jks \ | |
| -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \ | |
| -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \ | |
| -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} | |
| else | |
| echo "Building unsigned release APK" | |
| ./gradlew assembleRelease --stacktrace | |
| fi | |
| - name: Clean up keystore | |
| if: always() | |
| run: | | |
| rm -f app/keystore.jks | |
| - name: Upload release APK | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-apk | |
| path: app/build/outputs/apk/**/*.apk | |
| retention-days: 30 | |
| - name: APK Summary | |
| run: | | |
| echo "## APK Build Results :package:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| APK_PATH=$(find app/build/outputs/apk -name "*.apk" | grep -E "release" | head -1) | |
| if [ -f "$APK_PATH" ]; then | |
| APK_SIZE=$(du -h "$APK_PATH" | cut -f1) | |
| echo "- APK Size: $APK_SIZE" >> $GITHUB_STEP_SUMMARY | |
| echo "- APK Name: \`$(basename "$APK_PATH")\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Signed: ${{ vars.ENABLE_SIGNING == 'true' && 'Yes' || 'No' }}" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "No APK found" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| build-aab: | |
| name: Build Release Bundle | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| - name: Decode Keystore | |
| if: ${{ vars.ENABLE_SIGNING == 'true' && env.KEYSTORE_BASE64 != '' }} | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| run: | | |
| echo "$KEYSTORE_BASE64" | base64 --decode > app/keystore.jks | |
| - name: Build release bundle | |
| run: | | |
| if [ "${{ vars.ENABLE_SIGNING }}" = "true" ] && [ -f "app/keystore.jks" ]; then | |
| echo "Building signed release bundle" | |
| ./gradlew bundleRelease \ | |
| -Pandroid.injected.signing.store.file=${{ github.workspace }}/app/keystore.jks \ | |
| -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \ | |
| -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \ | |
| -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} | |
| else | |
| echo "Skipping bundle build - signing not configured" | |
| fi | |
| - name: Generate debug symbols | |
| if: ${{ vars.ENABLE_SIGNING == 'true' }} | |
| run: | | |
| echo "Checking for debug symbols..." | |
| find app/build/outputs -name "*.zip" -type f | grep -i debug || echo "No debug symbol zips found" | |
| find app/build/outputs -name "*symbols*" -type f || echo "No symbol files found" | |
| ls -la app/build/outputs/bundle/release/ || true | |
| - name: Clean up keystore | |
| if: always() | |
| run: | | |
| rm -f app/keystore.jks | |
| - name: Upload release bundle | |
| if: ${{ vars.ENABLE_SIGNING == 'true' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-bundle | |
| path: | | |
| app/build/outputs/bundle/**/*.aab | |
| app/build/outputs/mapping/release/mapping.txt | |
| retention-days: 30 | |
| release: | |
| name: Create GitHub Release | |
| needs: [prepare, build-apk, build-aab] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download APK artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-apk | |
| path: release-artifacts/ | |
| - name: Download AAB artifact | |
| if: ${{ vars.ENABLE_SIGNING == 'true' }} | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-bundle | |
| path: release-artifacts/ | |
| continue-on-error: true | |
| - name: Prepare release assets | |
| id: assets | |
| run: | | |
| # Find APK | |
| APK_PATH=$(find release-artifacts -name "*.apk" | grep -E "release" | head -1) | |
| if [ -f "$APK_PATH" ]; then | |
| APK_NAME="v2er-${{ needs.prepare.outputs.version }}.apk" | |
| mv "$APK_PATH" "$APK_NAME" | |
| echo "apk_path=$APK_NAME" >> $GITHUB_OUTPUT | |
| fi | |
| # Find AAB | |
| AAB_PATH=$(find release-artifacts -name "*.aab" 2>/dev/null | head -1) | |
| if [ -f "$AAB_PATH" ]; then | |
| AAB_NAME="v2er-${{ needs.prepare.outputs.version }}.aab" | |
| mv "$AAB_PATH" "$AAB_NAME" | |
| echo "aab_path=$AAB_NAME" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Generate changelog | |
| id: changelog | |
| run: | | |
| echo "## What's Changed" > CHANGELOG.md | |
| echo "" >> CHANGELOG.md | |
| # Get commits since last tag | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -n "$LAST_TAG" ]; then | |
| git log --pretty=format:"* %s by @%an" "$LAST_TAG"..HEAD >> CHANGELOG.md | |
| else | |
| git log --pretty=format:"* %s by @%an" -10 >> CHANGELOG.md | |
| fi | |
| echo "" >> CHANGELOG.md | |
| echo "" >> CHANGELOG.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LAST_TAG}...${{ needs.prepare.outputs.version }}" >> CHANGELOG.md | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare.outputs.version }} | |
| name: Release ${{ needs.prepare.outputs.version }} | |
| body_path: CHANGELOG.md | |
| draft: false | |
| prerelease: false | |
| files: | | |
| ${{ steps.assets.outputs.apk_path }} | |
| ${{ steps.assets.outputs.aab_path }} | |
| fail_on_unmatched_files: false | |
| play-store-upload: | |
| name: Upload to Play Store | |
| needs: [prepare, build-aab] | |
| if: ${{ vars.ENABLE_PLAY_STORE_UPLOAD == 'true' && vars.ENABLE_SIGNING == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download AAB artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-bundle | |
| path: release-artifacts/ | |
| - name: Find bundle and symbols | |
| id: find-files | |
| run: | | |
| AAB_PATH=$(find release-artifacts -name "*.aab" | head -1) | |
| echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT | |
| # Look for debug symbols | |
| SYMBOLS_PATH=$(find release-artifacts -name "native-debug-symbols.zip" 2>/dev/null | head -1) | |
| if [ -n "$SYMBOLS_PATH" ]; then | |
| echo "symbols_path=$SYMBOLS_PATH" >> $GITHUB_OUTPUT | |
| echo "Found debug symbols at: $SYMBOLS_PATH" | |
| else | |
| echo "No debug symbols found" | |
| fi | |
| # Look for ProGuard/R8 mapping file | |
| MAPPING_PATH=$(find release-artifacts -name "mapping.txt" 2>/dev/null | head -1) | |
| if [ -n "$MAPPING_PATH" ]; then | |
| echo "mapping_path=$MAPPING_PATH" >> $GITHUB_OUTPUT | |
| echo "Found ReTrace mapping file at: $MAPPING_PATH" | |
| else | |
| echo "No ReTrace mapping file found" | |
| fi | |
| - name: Determine release track and status | |
| id: release-config | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TRACK="${{ github.event.inputs.track }}" | |
| STATUS="${{ github.event.inputs.status }}" | |
| else | |
| # Default for tag pushes | |
| TRACK="internal" | |
| STATUS="draft" | |
| fi | |
| echo "track=$TRACK" >> $GITHUB_OUTPUT | |
| echo "status=$STATUS" >> $GITHUB_OUTPUT | |
| echo "Deploying to track: $TRACK with status: $STATUS" | |
| - name: Create whatsnew directory | |
| run: | | |
| mkdir -p whatsnew | |
| # Generate release notes | |
| echo "Release ${{ needs.prepare.outputs.version }}" > whatsnew/whatsnew-en-US | |
| echo "" >> whatsnew/whatsnew-en-US | |
| # Get recent commits | |
| git log --pretty=format:"• %s" -5 >> whatsnew/whatsnew-en-US | |
| # Chinese version | |
| echo "版本 ${{ needs.prepare.outputs.version }}" > whatsnew/whatsnew-zh-CN | |
| echo "" >> whatsnew/whatsnew-zh-CN | |
| git log --pretty=format:"• %s" -5 >> whatsnew/whatsnew-zh-CN | |
| - name: Upload to Play Store (with debug symbols) | |
| if: steps.find-files.outputs.symbols_path != '' | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} | |
| packageName: me.ghui.v2er | |
| releaseFiles: ${{ steps.find-files.outputs.aab_path }} | |
| track: ${{ steps.release-config.outputs.track }} | |
| status: ${{ steps.release-config.outputs.status }} | |
| debugSymbols: ${{ steps.find-files.outputs.symbols_path }} | |
| mappingFile: ${{ steps.find-files.outputs.mapping_path }} | |
| whatsNewDirectory: whatsnew/ | |
| continue-on-error: true | |
| id: upload-with-symbols | |
| - name: Upload to Play Store (without debug symbols) | |
| if: steps.find-files.outputs.symbols_path == '' || steps.upload-with-symbols.outcome == 'failure' | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} | |
| packageName: me.ghui.v2er | |
| releaseFiles: ${{ steps.find-files.outputs.aab_path }} | |
| track: ${{ steps.release-config.outputs.track }} | |
| status: ${{ steps.release-config.outputs.status }} | |
| mappingFile: ${{ steps.find-files.outputs.mapping_path }} | |
| whatsNewDirectory: whatsnew/ | |
| - name: Play Store Upload Summary | |
| if: success() | |
| run: | | |
| echo "## Play Store Upload Complete :rocket:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Track**: ${{ steps.release-config.outputs.track }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status**: ${{ steps.release-config.outputs.status }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Package**: me.ghui.v2er" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "[View in Play Console](https://play.google.com/console/u/0/app/me.ghui.v2er)" >> $GITHUB_STEP_SUMMARY | |
| download-signed-apk: | |
| name: Download Google Play Signed APK | |
| needs: [prepare, play-store-upload] | |
| if: ${{ vars.ENABLE_PLAY_STORE_UPLOAD == 'true' && vars.ENABLE_SIGNING == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.x' | |
| - name: Install dependencies | |
| run: | | |
| pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib requests | |
| - name: Check and wait for Google Play processing | |
| env: | |
| PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} | |
| run: | | |
| VERSION_CODE="${{ needs.prepare.outputs.version_code }}" | |
| PACKAGE_NAME="me.ghui.v2er" | |
| # Create Python script to check if APK exists | |
| cat > check_apk_exists.py << 'EOF' | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from google.oauth2 import service_account | |
| from googleapiclient.discovery import build | |
| def check_apk_exists(): | |
| try: | |
| # Load service account credentials | |
| service_account_info = json.loads(os.environ['PLAY_STORE_SERVICE_ACCOUNT_JSON']) | |
| credentials = service_account.Credentials.from_service_account_info( | |
| service_account_info, | |
| scopes=['https://www.googleapis.com/auth/androidpublisher'] | |
| ) | |
| # Build the service | |
| service = build('androidpublisher', 'v3', credentials=credentials) | |
| package_name = os.environ['PACKAGE_NAME'] | |
| version_code = int(os.environ['VERSION_CODE']) | |
| print(f"Checking if signed APK exists for {package_name} version {version_code}") | |
| # Try to get the generated APKs list | |
| result = service.generatedapks().list( | |
| packageName=package_name, | |
| versionCode=version_code | |
| ).execute() | |
| if 'generatedApks' not in result or not result['generatedApks']: | |
| print(f"No generated APKs found for version {version_code}") | |
| return False | |
| print(f"Found {len(result['generatedApks'])} generated APK groups") | |
| # Check if we can find a universal APK | |
| for apk in result['generatedApks']: | |
| if 'generatedUniversalApk' in apk: | |
| universal_apk = apk['generatedUniversalApk'] | |
| download_id = universal_apk.get('downloadId') | |
| if download_id: | |
| print(f"✅ Universal APK found with downloadId: {download_id}") | |
| return True | |
| # Fallback: check split APKs for base module | |
| if 'generatedSplitApks' in apk: | |
| split_apks = apk['generatedSplitApks'] | |
| for split_apk in split_apks: | |
| if (split_apk.get('moduleName') == 'base' and | |
| 'splitId' not in split_apk): | |
| download_id = split_apk.get('downloadId') | |
| if download_id: | |
| print(f"✅ Base APK found with downloadId: {download_id}") | |
| return True | |
| print("❌ No suitable APK found") | |
| return False | |
| except Exception as e: | |
| print(f"Error checking APK: {str(e)}") | |
| return False | |
| if __name__ == "__main__": | |
| exists = check_apk_exists() | |
| sys.exit(0 if exists else 1) | |
| EOF | |
| # Set environment variables for the script | |
| export PACKAGE_NAME="$PACKAGE_NAME" | |
| export VERSION_CODE="$VERSION_CODE" | |
| # Check if APK already exists | |
| echo "Checking if Google Play signed APK is ready..." | |
| if python3 check_apk_exists.py; then | |
| echo "✅ APK is already available, skipping wait" | |
| else | |
| echo "⏳ APK not ready yet, waiting for Google Play to process..." | |
| # Smart waiting with periodic checks | |
| MAX_WAIT=600 # Maximum 10 minutes | |
| CHECK_INTERVAL=30 # Check every 30 seconds | |
| elapsed=0 | |
| while [ $elapsed -lt $MAX_WAIT ]; do | |
| sleep $CHECK_INTERVAL | |
| elapsed=$((elapsed + CHECK_INTERVAL)) | |
| echo "⏱️ Waited ${elapsed}s, checking again..." | |
| if python3 check_apk_exists.py; then | |
| echo "✅ APK is now available after ${elapsed}s" | |
| break | |
| fi | |
| if [ $elapsed -ge $MAX_WAIT ]; then | |
| echo "⚠️ Maximum wait time (${MAX_WAIT}s) reached" | |
| echo "APK may still be processing, will attempt download anyway" | |
| fi | |
| done | |
| fi | |
| # AAB artifact not needed for Google Play signed APK download | |
| - name: Download Google Play Signed APK | |
| id: download-apk | |
| env: | |
| PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} | |
| run: | | |
| VERSION_NAME="${{ needs.prepare.outputs.version }}" | |
| VERSION_CODE="${{ needs.prepare.outputs.version_code }}" | |
| PACKAGE_NAME="me.ghui.v2er" | |
| # Create Python script to download signed universal APK | |
| cat > download_signed_apk.py << 'EOF' | |
| import json | |
| import os | |
| import sys | |
| import requests | |
| from google.oauth2 import service_account | |
| from googleapiclient.discovery import build | |
| def download_signed_apk(): | |
| try: | |
| # Load service account credentials | |
| service_account_info = json.loads(os.environ['PLAY_STORE_SERVICE_ACCOUNT_JSON']) | |
| credentials = service_account.Credentials.from_service_account_info( | |
| service_account_info, | |
| scopes=['https://www.googleapis.com/auth/androidpublisher'] | |
| ) | |
| # Build the service | |
| service = build('androidpublisher', 'v3', credentials=credentials) | |
| package_name = os.environ['PACKAGE_NAME'] | |
| version_code = int(os.environ['VERSION_CODE']) | |
| print(f"Attempting to download signed APK for {package_name} version {version_code}") | |
| # Step 1: Get the generated APKs list to find downloadId | |
| print("Getting generated APKs list...") | |
| result = service.generatedapks().list( | |
| packageName=package_name, | |
| versionCode=version_code | |
| ).execute() | |
| if 'generatedApks' not in result or not result['generatedApks']: | |
| print("No generated APKs found. App may not be processed yet by Google Play.") | |
| return False | |
| print(f"Found {len(result['generatedApks'])} generated APKs") | |
| # Debug: Print all APK structures | |
| for i, apk in enumerate(result['generatedApks']): | |
| print(f"APK {i} structure:") | |
| for key, value in apk.items(): | |
| print(f" {key}: {value}") | |
| print() | |
| # Find universal APK using the correct API structure | |
| download_id = None | |
| universal_apk = None | |
| # First, try to find a universal APK in generatedUniversalApk | |
| for apk in result['generatedApks']: | |
| if 'generatedUniversalApk' in apk: | |
| universal_apk = apk['generatedUniversalApk'] | |
| download_id = universal_apk.get('downloadId') | |
| print(f"Found universal APK: {universal_apk}") | |
| break | |
| # Fallback: Look for base module in split APKs if no universal APK found | |
| if not download_id: | |
| print("No generatedUniversalApk found, trying split APKs...") | |
| for apk in result['generatedApks']: | |
| if 'generatedSplitApks' in apk: | |
| split_apks = apk['generatedSplitApks'] | |
| print(f"Found {len(split_apks)} split APKs") | |
| # Print all base modules for debugging | |
| base_candidates = [] | |
| for split_apk in split_apks: | |
| if split_apk.get('moduleName') == 'base': | |
| base_candidates.append(split_apk) | |
| print(f"Base APK: variantId={split_apk.get('variantId')}, splitId={split_apk.get('splitId', 'None')}") | |
| # Try variantId=2 first (based on previous observations) | |
| for split_apk in split_apks: | |
| if (split_apk.get('moduleName') == 'base' and | |
| split_apk.get('variantId') == 2 and | |
| 'splitId' not in split_apk): | |
| download_id = split_apk.get('downloadId') | |
| universal_apk = split_apk | |
| print(f"Found base APK (variantId=2): {universal_apk}") | |
| break | |
| # Try other variants if variantId=2 not found | |
| if not download_id: | |
| for split_apk in split_apks: | |
| if (split_apk.get('moduleName') == 'base' and | |
| 'splitId' not in split_apk): | |
| download_id = split_apk.get('downloadId') | |
| universal_apk = split_apk | |
| print(f"Found base APK (variantId={split_apk.get('variantId')}): {universal_apk}") | |
| break | |
| if download_id: | |
| break | |
| if not download_id: | |
| print("No universal or base APK found") | |
| print("Available APK structure:") | |
| print(json.dumps(result['generatedApks'], indent=2)) | |
| return False | |
| print(f"Found universal APK with downloadId: {download_id}") | |
| # Step 2: Download the APK using the downloadId | |
| print("Downloading APK binary...") | |
| # Use alt=media to get the actual binary content instead of metadata | |
| download_request = service.generatedapks().download( | |
| packageName=package_name, | |
| versionCode=version_code, | |
| downloadId=download_id | |
| ) | |
| # Add alt=media parameter correctly (URL already has query params, so use &) | |
| if '?' in download_request.uri: | |
| download_request.uri += '&alt=media' | |
| else: | |
| download_request.uri += '?alt=media' | |
| output_filename = f"v2er-{os.environ['VERSION_NAME']}_google_play_signed.apk" | |
| # Use media download with googleapiclient.http to handle binary content | |
| import io | |
| from googleapiclient.http import MediaIoBaseDownload | |
| file_io = io.BytesIO() | |
| downloader = MediaIoBaseDownload(file_io, download_request) | |
| done = False | |
| while done is False: | |
| status, done = downloader.next_chunk() | |
| if status: | |
| print(f"Download progress: {int(status.progress() * 100)}%") | |
| # Write to file | |
| with open(output_filename, 'wb') as f: | |
| f.write(file_io.getvalue()) | |
| print(f"Successfully downloaded: {output_filename}") | |
| print(f"apk_path={output_filename}") | |
| return True | |
| except Exception as e: | |
| print(f"Error downloading signed APK: {str(e)}") | |
| print("This may be because:") | |
| print("1. The app hasn't been processed by Google Play yet") | |
| print("2. The version hasn't been released to any track") | |
| print("3. API permissions are insufficient") | |
| return False | |
| if __name__ == "__main__": | |
| success = download_signed_apk() | |
| sys.exit(0 if success else 1) | |
| EOF | |
| # Set environment variables for the script | |
| export PACKAGE_NAME="$PACKAGE_NAME" | |
| export VERSION_CODE="$VERSION_CODE" | |
| export VERSION_NAME="$VERSION_NAME" | |
| # Run the download script | |
| echo "Attempting to download Google Play signed APK..." | |
| if python3 download_signed_apk.py > download_output.txt 2>&1; then | |
| echo "Successfully downloaded Google Play signed APK" | |
| cat download_output.txt | |
| # Extract the APK path from output | |
| APK_PATH=$(grep "apk_path=" download_output.txt | cut -d'=' -f2) | |
| if [ -f "$APK_PATH" ]; then | |
| echo "apk_path=$APK_PATH" >> $GITHUB_OUTPUT | |
| echo "found=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "APK file not found after download" | |
| echo "found=false" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "Failed to download Google Play signed APK" | |
| cat download_output.txt | |
| echo "found=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create Google Play link info | |
| if: steps.download-apk.outputs.found == 'true' | |
| id: play-link | |
| run: | | |
| VERSION_NAME="${{ needs.prepare.outputs.version }}" | |
| VERSION_CODE="${{ needs.prepare.outputs.version_code }}" | |
| # Create info file about Google Play signing | |
| cat > "v2er-${VERSION_NAME}_google_play_signed_info.txt" << EOF | |
| Google Play Signed APK Information | |
| ================================== | |
| Version: ${VERSION_NAME} | |
| Version Code: ${VERSION_CODE} | |
| Package: me.ghui.v2er | |
| The APK attached (v2er-${VERSION_NAME}_google_play_signed.apk) is a universal APK | |
| generated from the AAB (Android App Bundle) that was uploaded to Google Play. | |
| When downloaded from Google Play Store, the APK will be signed with Google Play's | |
| app signing certificate instead of the upload certificate. | |
| Internal Testing Link: | |
| https://play.google.com/apps/test/me.ghui.v2er/${VERSION_CODE} | |
| Note: Access to internal testing track required. | |
| EOF | |
| echo "info_path=v2er-${VERSION_NAME}_google_play_signed_info.txt" >> $GITHUB_OUTPUT | |
| - name: Upload signed APK to GitHub Release | |
| if: steps.download-apk.outputs.found == 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare.outputs.version }} | |
| files: | | |
| ${{ steps.download-apk.outputs.apk_path }} | |
| ${{ steps.play-link.outputs.info_path }} | |
| fail_on_unmatched_files: false | |
| - name: Summary | |
| if: always() | |
| run: | | |
| echo "## Google Play Signed APK :package:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.download-apk.outputs.found }}" = "true" ]; then | |
| echo "✅ **Universal APK generated successfully**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version Code**: ${{ needs.prepare.outputs.version_code }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **File**: v2er-${{ needs.prepare.outputs.version }}_google_play_signed.apk" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Notes" >> $GITHUB_STEP_SUMMARY | |
| echo "- This APK is generated from the AAB uploaded to Google Play" >> $GITHUB_STEP_SUMMARY | |
| echo "- When installed from Play Store, it will use Google Play's signing certificate" >> $GITHUB_STEP_SUMMARY | |
| echo "- The APK has been uploaded to the GitHub Release" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ **Google Play signed APK download failed**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "This may be because:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Google Play is still processing the upload" >> $GITHUB_STEP_SUMMARY | |
| echo "- The version hasn't been released to any track yet" >> $GITHUB_STEP_SUMMARY | |
| echo "- API permissions are insufficient" >> $GITHUB_STEP_SUMMARY | |
| fi |