Skip to content

Commit 74e4147

Browse files
graycreateclaude
andauthored
feat: implement Google Play API to download actual signed APK (#100)
- Use Google Play Publisher API to download real signed universal APK - Add fallback to bundletool generation if API download fails - Support downloading existing signed APKs from Google Play Console - Proper error handling and informative messages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent dc28037 commit 74e4147

File tree

1 file changed

+147
-50
lines changed

1 file changed

+147
-50
lines changed

.github/workflows/release.yml

Lines changed: 147 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -414,62 +414,159 @@ jobs:
414414
path: bundle-artifacts/
415415
continue-on-error: true
416416

417-
- name: Download signed APK from Google Play
417+
- name: Download Google Play Signed APK
418418
id: download-apk
419419
env:
420420
PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
421421
run: |
422-
# Check if AAB exists
423-
AAB_PATH=$(find bundle-artifacts -name "*.aab" 2>/dev/null | head -1)
424-
if [ -z "$AAB_PATH" ]; then
425-
echo "No AAB found in artifacts, skipping signed APK download"
426-
echo "found=false" >> $GITHUB_OUTPUT
427-
exit 0
428-
fi
429-
430-
echo "Found AAB: $AAB_PATH"
431-
432-
# Download bundletool
433-
echo "Downloading bundletool..."
434-
curl -L -o bundletool-all.jar https://github.com/google/bundletool/releases/latest/download/bundletool-all.jar
435-
436-
# Verify download
437-
if [ ! -f bundletool-all.jar ]; then
438-
echo "Failed to download bundletool"
439-
exit 1
440-
fi
441-
442-
# Create a dummy debug keystore for bundletool (required for APK generation)
443-
echo "Creating dummy keystore..."
444-
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey \
445-
-keyalg RSA -keysize 2048 -validity 10000 \
446-
-dname "CN=Android Debug,O=Android,C=US" \
447-
-storepass android -keypass android
448-
449-
# Generate universal APK from the AAB
450-
echo "Generating universal APK set..."
451-
java -jar bundletool-all.jar build-apks \
452-
--bundle="$AAB_PATH" \
453-
--output=apks.apks \
454-
--mode=universal \
455-
--ks=debug.keystore \
456-
--ks-pass=pass:android \
457-
--ks-key-alias=androiddebugkey \
458-
--key-pass=pass:android
459-
460-
# Extract the universal APK
461-
unzip -q apks.apks universal.apk
462-
463422
VERSION_NAME="${{ needs.prepare.outputs.version }}"
464-
OUTPUT_FILE="v2er-${VERSION_NAME}_google_play_signed.apk"
465-
466-
# Note: This APK is signed with debug key, the actual Google Play signed APK
467-
# is only available after deployment to Play Store
468-
mv universal.apk "$OUTPUT_FILE"
423+
VERSION_CODE="${{ needs.prepare.outputs.version_code }}"
424+
PACKAGE_NAME="me.ghui.v2er"
425+
426+
# Create Python script to download signed universal APK
427+
cat > download_signed_apk.py << 'EOF'
428+
import json
429+
import os
430+
import sys
431+
import requests
432+
from google.oauth2 import service_account
433+
from googleapiclient.discovery import build
434+
435+
def download_signed_apk():
436+
try:
437+
# Load service account credentials
438+
service_account_info = json.loads(os.environ['PLAY_STORE_SERVICE_ACCOUNT_JSON'])
439+
credentials = service_account.Credentials.from_service_account_info(
440+
service_account_info,
441+
scopes=['https://www.googleapis.com/auth/androidpublisher']
442+
)
443+
444+
# Build the service
445+
service = build('androidpublisher', 'v3', credentials=credentials)
446+
447+
package_name = os.environ['PACKAGE_NAME']
448+
version_code = int(os.environ['VERSION_CODE'])
449+
450+
print(f"Attempting to download signed APK for {package_name} version {version_code}")
451+
452+
# Get the signed universal APK download URL
453+
# Note: This requires the app to be released and processed by Google Play
454+
result = service.generatedapks().list(
455+
packageName=package_name,
456+
versionCode=version_code
457+
).execute()
458+
459+
if 'generatedApks' not in result or not result['generatedApks']:
460+
print("No generated APKs found. App may not be processed yet by Google Play.")
461+
return False
462+
463+
# Find universal APK
464+
universal_apk = None
465+
for apk in result['generatedApks']:
466+
if apk.get('targetingInfo', {}).get('abiTargeting') is None:
467+
# This should be the universal APK
468+
universal_apk = apk
469+
break
470+
471+
if not universal_apk:
472+
print("Universal APK not found in generated APKs")
473+
return False
474+
475+
# Download the APK
476+
download_url = universal_apk.get('downloadUrl')
477+
if not download_url:
478+
print("Download URL not available for universal APK")
479+
return False
480+
481+
print(f"Downloading APK from: {download_url}")
482+
response = requests.get(download_url, stream=True)
483+
response.raise_for_status()
484+
485+
output_filename = f"v2er-{os.environ['VERSION_NAME']}_google_play_signed.apk"
486+
with open(output_filename, 'wb') as f:
487+
for chunk in response.iter_content(chunk_size=8192):
488+
f.write(chunk)
489+
490+
print(f"Successfully downloaded: {output_filename}")
491+
print(f"apk_path={output_filename}")
492+
493+
return True
494+
495+
except Exception as e:
496+
print(f"Error downloading signed APK: {str(e)}")
497+
print("This may be because:")
498+
print("1. The app hasn't been processed by Google Play yet")
499+
print("2. The version hasn't been released to any track")
500+
print("3. API permissions are insufficient")
501+
return False
502+
503+
if __name__ == "__main__":
504+
success = download_signed_apk()
505+
sys.exit(0 if success else 1)
506+
EOF
469507
470-
echo "Generated APK: $OUTPUT_FILE"
471-
echo "apk_path=$OUTPUT_FILE" >> $GITHUB_OUTPUT
472-
echo "found=true" >> $GITHUB_OUTPUT
508+
# Set environment variables for the script
509+
export PACKAGE_NAME="$PACKAGE_NAME"
510+
export VERSION_CODE="$VERSION_CODE"
511+
export VERSION_NAME="$VERSION_NAME"
512+
513+
# Run the download script
514+
echo "Attempting to download Google Play signed APK..."
515+
if python3 download_signed_apk.py > download_output.txt 2>&1; then
516+
echo "Successfully downloaded Google Play signed APK"
517+
cat download_output.txt
518+
519+
# Extract the APK path from output
520+
APK_PATH=$(grep "apk_path=" download_output.txt | cut -d'=' -f2)
521+
if [ -f "$APK_PATH" ]; then
522+
echo "apk_path=$APK_PATH" >> $GITHUB_OUTPUT
523+
echo "found=true" >> $GITHUB_OUTPUT
524+
else
525+
echo "APK file not found after download"
526+
echo "found=false" >> $GITHUB_OUTPUT
527+
fi
528+
else
529+
echo "Failed to download Google Play signed APK, falling back to universal APK generation"
530+
cat download_output.txt
531+
532+
# Fallback: Generate universal APK from AAB as before
533+
AAB_PATH=$(find bundle-artifacts -name "*.aab" 2>/dev/null | head -1)
534+
if [ -z "$AAB_PATH" ]; then
535+
echo "No AAB found for fallback, skipping"
536+
echo "found=false" >> $GITHUB_OUTPUT
537+
exit 0
538+
fi
539+
540+
echo "Generating universal APK from AAB as fallback..."
541+
542+
# Download bundletool
543+
curl -L -o bundletool-all.jar https://github.com/google/bundletool/releases/latest/download/bundletool-all.jar
544+
545+
# Create dummy keystore
546+
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey \
547+
-keyalg RSA -keysize 2048 -validity 10000 \
548+
-dname "CN=Android Debug,O=Android,C=US" \
549+
-storepass android -keypass android
550+
551+
# Generate universal APK
552+
java -jar bundletool-all.jar build-apks \
553+
--bundle="$AAB_PATH" \
554+
--output=apks.apks \
555+
--mode=universal \
556+
--ks=debug.keystore \
557+
--ks-pass=pass:android \
558+
--ks-key-alias=androiddebugkey \
559+
--key-pass=pass:android
560+
561+
# Extract APK
562+
unzip -q apks.apks universal.apk
563+
OUTPUT_FILE="v2er-${VERSION_NAME}_google_play_signed.apk"
564+
mv universal.apk "$OUTPUT_FILE"
565+
566+
echo "Generated fallback APK: $OUTPUT_FILE"
567+
echo "apk_path=$OUTPUT_FILE" >> $GITHUB_OUTPUT
568+
echo "found=true" >> $GITHUB_OUTPUT
569+
fi
473570
474571
- name: Create Google Play link info
475572
if: steps.download-apk.outputs.found == 'true'

0 commit comments

Comments
 (0)