-
Notifications
You must be signed in to change notification settings - Fork 47
feat: implement Google Play API to download actual signed APK #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -414,62 +414,159 @@ jobs: | |||||
| path: bundle-artifacts/ | ||||||
| continue-on-error: true | ||||||
|
|
||||||
| - name: Download signed APK from Google Play | ||||||
| - name: Download Google Play Signed APK | ||||||
| id: download-apk | ||||||
| env: | ||||||
| PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} | ||||||
| run: | | ||||||
| # Check if AAB exists | ||||||
| AAB_PATH=$(find bundle-artifacts -name "*.aab" 2>/dev/null | head -1) | ||||||
| if [ -z "$AAB_PATH" ]; then | ||||||
| echo "No AAB found in artifacts, skipping signed APK download" | ||||||
| echo "found=false" >> $GITHUB_OUTPUT | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| echo "Found AAB: $AAB_PATH" | ||||||
|
|
||||||
| # Download bundletool | ||||||
| echo "Downloading bundletool..." | ||||||
| curl -L -o bundletool-all.jar https://github.com/google/bundletool/releases/latest/download/bundletool-all.jar | ||||||
|
|
||||||
| # Verify download | ||||||
| if [ ! -f bundletool-all.jar ]; then | ||||||
| echo "Failed to download bundletool" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| # Create a dummy debug keystore for bundletool (required for APK generation) | ||||||
| echo "Creating dummy keystore..." | ||||||
| keytool -genkey -v -keystore debug.keystore -alias androiddebugkey \ | ||||||
| -keyalg RSA -keysize 2048 -validity 10000 \ | ||||||
| -dname "CN=Android Debug,O=Android,C=US" \ | ||||||
| -storepass android -keypass android | ||||||
|
|
||||||
| # Generate universal APK from the AAB | ||||||
| echo "Generating universal APK set..." | ||||||
| java -jar bundletool-all.jar build-apks \ | ||||||
| --bundle="$AAB_PATH" \ | ||||||
| --output=apks.apks \ | ||||||
| --mode=universal \ | ||||||
| --ks=debug.keystore \ | ||||||
| --ks-pass=pass:android \ | ||||||
| --ks-key-alias=androiddebugkey \ | ||||||
| --key-pass=pass:android | ||||||
|
|
||||||
| # Extract the universal APK | ||||||
| unzip -q apks.apks universal.apk | ||||||
|
|
||||||
| VERSION_NAME="${{ needs.prepare.outputs.version }}" | ||||||
| OUTPUT_FILE="v2er-${VERSION_NAME}_google_play_signed.apk" | ||||||
|
|
||||||
| # Note: This APK is signed with debug key, the actual Google Play signed APK | ||||||
| # is only available after deployment to Play Store | ||||||
| mv universal.apk "$OUTPUT_FILE" | ||||||
| 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}") | ||||||
|
|
||||||
| # Get the signed universal APK download URL | ||||||
| # Note: This requires the app to be released and processed by Google Play | ||||||
| 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 | ||||||
|
|
||||||
| # Find universal APK | ||||||
| universal_apk = None | ||||||
| for apk in result['generatedApks']: | ||||||
| if apk.get('targetingInfo', {}).get('abiTargeting') is None: | ||||||
| # This should be the universal APK | ||||||
| universal_apk = apk | ||||||
| break | ||||||
|
|
||||||
| if not universal_apk: | ||||||
| print("Universal APK not found in generated APKs") | ||||||
| return False | ||||||
|
|
||||||
| # Download the APK | ||||||
| download_url = universal_apk.get('downloadUrl') | ||||||
| if not download_url: | ||||||
| print("Download URL not available for universal APK") | ||||||
| return False | ||||||
|
|
||||||
| print(f"Downloading APK from: {download_url}") | ||||||
| response = requests.get(download_url, stream=True) | ||||||
|
||||||
| response = requests.get(download_url, stream=True) | |
| response = requests.get(download_url, stream=True, timeout=30, verify=True) |
Copilot
AI
Sep 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output redirection captures both stdout and stderr, making it difficult to distinguish between normal output and error messages. Consider using separate handling for stdout and stderr or using more explicit error handling.
Copilot
AI
Sep 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback APK is being named with '_google_play_signed' suffix even though it's signed with a debug certificate. This naming is misleading and could confuse users about the actual signing status of the APK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using heredoc to create a Python script inline poses security risks. Consider storing the Python script as a separate file in the repository to improve maintainability and security review capabilities.