ci: unify deploy workflow - builds APKs + AAB, creates GitHub release… #79
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: Deploy to Play Store & Indus App Store | |
| on: | |
| push: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| # Cancel any in-progress runs for the same branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Write permission needed for GitHub releases | |
| permissions: | |
| contents: write | |
| actions: write | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| env: | |
| # Offset to ensure version codes exceed any previously uploaded ones | |
| # Current Play Store version is 51, so starting from 52 | |
| VERSION_CODE_OFFSET: 52 | |
| # Major.Minor prefix for versionName (versionName = VERSION_PREFIX.BUILD_NUMBER) | |
| VERSION_PREFIX: '1.3' | |
| steps: | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 1. Checkout the repository | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history for commit log generation | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 2. Set up Java (required for Android/Gradle) | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: gradle | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 3. Compute Version Code and Version Name | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Compute Version | |
| id: version | |
| env: | |
| OFFSET: ${{ env.VERSION_CODE_OFFSET }} | |
| RUN_NUM: ${{ github.run_number }} | |
| PREFIX: ${{ env.VERSION_PREFIX }} | |
| run: | | |
| # Use shell arithmetic with defaults | |
| OFFSET=${OFFSET:-52} | |
| RUN_NUM=${RUN_NUM:-1} | |
| PREFIX=${PREFIX:-1.3} | |
| echo "DEBUG: OFFSET='$OFFSET', RUN_NUM='$RUN_NUM', PREFIX='$PREFIX'" | |
| BUILD_NUMBER=$(( RUN_NUM + OFFSET )) | |
| VERSION_NAME="${PREFIX}.${BUILD_NUMBER}" | |
| echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_OUTPUT | |
| echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT | |
| echo "APP_VERSION_CODE=$BUILD_NUMBER" >> $GITHUB_ENV | |
| echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV | |
| echo "================================================" | |
| echo "Computed versionCode: $BUILD_NUMBER (Run: $RUN_NUM + Offset: $OFFSET)" | |
| echo "Computed versionName: $VERSION_NAME" | |
| echo "================================================" | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 4. Generate Release Notes | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Generate Release Notes | |
| id: generate-notes | |
| run: | | |
| # Extract the latest commit message for release metadata | |
| COMMIT_MSG=$(git log -1 --pretty=format:"%s") | |
| COMMIT_HASH=$(git log -1 --pretty=format:"%h") | |
| BUILD_DATE=$(date -u +"%Y-%m-%d") | |
| # Write to release_description.txt (artifact-level description) | |
| cat > release_description.txt <<EOF | |
| Build: v${{ env.APP_VERSION_NAME }} (code ${{ env.APP_VERSION_CODE }}) | |
| Date: ${BUILD_DATE} | |
| Commit: ${COMMIT_HASH} | |
| ${COMMIT_MSG} | |
| EOF | |
| # Update whatsnew for Play Store listing | |
| cat > distribution/whatsnew/whatsnew-en-US <<EOF | |
| Version ${{ env.APP_VERSION_NAME }} - Dynamic versioning update | |
| • Implemented CI/CD dynamic versioning | |
| • No more version conflicts in git | |
| • Automatic version increments per build | |
| • Improved deployment reliability | |
| EOF | |
| # Set output for GitHub Release | |
| echo "release_notes<<EOF" >> $GITHUB_OUTPUT | |
| cat distribution/whatsnew/whatsnew-en-US >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| echo "=== Release Description ===" | |
| cat release_description.txt | |
| echo "" | |
| echo "=== What's New ===" | |
| cat distribution/whatsnew/whatsnew-en-US | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 5. Decode the Android Keystore from secrets | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Decode Keystore | |
| env: | |
| ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| run: | | |
| echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > keystore.jks | |
| cp keystore.jks app/keystore.jks | |
| echo "Keystore decoded successfully" | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 6. Create Play Store JSON Key | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Create Play Store JSON Key | |
| env: | |
| PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }} | |
| run: | | |
| echo "$PLAY_STORE_JSON_KEY" > play-store-key.json | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 7. Make gradlew executable | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Make gradlew executable | |
| run: chmod +x gradlew | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 8. Build Release AAB and APKs | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Build Release AAB | |
| env: | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| ANDROID_KEYSTORE_FILE: keystore.jks | |
| APP_VERSION_CODE: ${{ env.APP_VERSION_CODE }} | |
| APP_VERSION_NAME: ${{ env.APP_VERSION_NAME }} | |
| CI: true | |
| run: | | |
| echo "Building AAB — versionCode=${{ env.APP_VERSION_CODE }}, versionName=${{ env.APP_VERSION_NAME }}" | |
| ./gradlew bundlePlaystoreRelease \ | |
| -PAPP_VERSION_CODE=${{ env.APP_VERSION_CODE }} \ | |
| -PAPP_VERSION_NAME=${{ env.APP_VERSION_NAME }} | |
| - name: Build Release APKs | |
| env: | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| ANDROID_KEYSTORE_FILE: keystore.jks | |
| APP_VERSION_CODE: ${{ env.APP_VERSION_CODE }} | |
| APP_VERSION_NAME: ${{ env.APP_VERSION_NAME }} | |
| CI: true | |
| run: | | |
| echo "Building APKs — versionCode=${{ env.APP_VERSION_CODE }}, versionName=${{ env.APP_VERSION_NAME }}" | |
| ./gradlew assemblePlaystoreRelease \ | |
| -PAPP_VERSION_CODE=${{ env.APP_VERSION_CODE }} \ | |
| -PAPP_VERSION_NAME=${{ env.APP_VERSION_NAME }} | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 9. Verify Build Outputs | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Verify Build Outputs | |
| run: | | |
| echo "=== AAB Files ===" | |
| ls -lh app/build/outputs/bundle/playstoreRelease/*.aab | |
| echo "=== APK Files ===" | |
| ls -lh app/build/outputs/apk/playstore/release/*.apk || echo "No APK files found" | |
| echo "=== Mapping File ===" | |
| ls -lh app/build/outputs/mapping/playstoreRelease/mapping.txt || echo "No mapping file found" | |
| echo "=== Version ===" | |
| echo "versionCode: ${{ env.APP_VERSION_CODE }}" | |
| echo "versionName: ${{ env.APP_VERSION_NAME }}" | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 10. Upload AAB and APKs as build artifacts | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Upload AAB artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-aab-v${{ env.APP_VERSION_NAME }} | |
| path: app/build/outputs/bundle/playstoreRelease/app-playstoreRelease.aab | |
| retention-days: 30 | |
| - name: Upload APK artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-apks-v${{ env.APP_VERSION_NAME }} | |
| path: app/build/outputs/apk/playstore/release/*.apk | |
| retention-days: 30 | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 11. Deploy to Google Play Internal Track | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Upload to Play Store | |
| id: upload-play-store | |
| continue-on-error: true | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJson: play-store-key.json | |
| packageName: com.yourname.pdftoolkit | |
| releaseFiles: app/build/outputs/bundle/playstoreRelease/*.aab | |
| track: internal | |
| status: completed | |
| mappingFile: app/build/outputs/mapping/playstoreRelease/mapping.txt | |
| debugSymbols: app/build/intermediates/merged_native_libs/playstoreRelease/mergePlaystoreReleaseNativeLibs/out/lib | |
| whatsNewDirectory: distribution/whatsnew | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 12. Deploy to Indus App Store | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Upload to Indus App Store | |
| id: upload-indus-store | |
| continue-on-error: true | |
| run: | | |
| AAB_FILE=$(ls app/build/outputs/bundle/playstoreRelease/*.aab | head -n 1) | |
| KEYSTORE_FILE="keystore.jks" | |
| echo "Uploading to Indus App Store..." | |
| echo "AAB File: $AAB_FILE" | |
| echo "Keystore: $KEYSTORE_FILE" | |
| # Create multipart form data request | |
| RESPONSE=$(curl -X POST \ | |
| "https://developer-api.indusappstore.com/devtools/aab/upgrade/com.yourname.pdftoolkit" \ | |
| -H "Authorization: ${{ secrets.INDUS_APP_STORE_KEY }}" \ | |
| -F "file=@${AAB_FILE}" \ | |
| -F "file=@${KEYSTORE_FILE}" \ | |
| -F "keyPassword=${{ secrets.KEY_PASSWORD }}" \ | |
| -F "keystoreAlias=${{ secrets.KEY_ALIAS }}" \ | |
| -F "keystorePassword=${{ secrets.KEYSTORE_PASSWORD }}" \ | |
| -w "\n%{http_code}" \ | |
| -s) | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -n1) | |
| BODY=$(echo "$RESPONSE" | sed '$d') | |
| echo "HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| # Check for success (2xx) | |
| if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then | |
| echo "✓ Successfully uploaded to Indus App Store" | |
| echo "indus_status=success" >> $GITHUB_OUTPUT | |
| exit 0 | |
| # Check for 409 (app under review) | |
| elif [ "$HTTP_CODE" -eq 409 ]; then | |
| echo "⚠ Indus App Store: App is currently under review" | |
| echo "Skipping upload - previous version is being reviewed" | |
| echo "indus_status=skipped" >> $GITHUB_OUTPUT | |
| exit 0 | |
| # Check for 400 (version already exists or other validation error) | |
| elif [ "$HTTP_CODE" -eq 400 ]; then | |
| echo "⚠ Indus App Store: Version validation error" | |
| echo "Response: $BODY" | |
| echo "indus_status=failed" >> $GITHUB_OUTPUT | |
| exit 1 | |
| else | |
| echo "✗ Failed to upload to Indus App Store" | |
| echo "Response body: $BODY" | |
| echo "indus_status=failed" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 13. Check Deployment Results | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Check Deployment Results | |
| run: | | |
| echo "=== Deployment Summary ===" | |
| echo "Play Store: ${{ steps.upload-play-store.outcome }}" | |
| echo "Indus App Store: ${{ steps.upload-indus-store.outcome }}" | |
| INDUS_STATUS="${{ steps.upload-indus-store.outputs.indus_status }}" | |
| echo "Indus Status Detail: $INDUS_STATUS" | |
| # Fail the job only if BOTH deployments failed | |
| if [[ "${{ steps.upload-play-store.outcome }}" == "failure" && "${{ steps.upload-indus-store.outcome }}" == "failure" ]]; then | |
| # But don't fail if Indus was skipped due to review | |
| if [[ "$INDUS_STATUS" == "skipped" ]]; then | |
| echo "INFO: Indus App Store skipped (under review), but Play Store failed" | |
| exit 1 | |
| fi | |
| echo "ERROR: Both store deployments failed!" | |
| exit 1 | |
| fi | |
| if [[ "${{ steps.upload-play-store.outcome }}" == "failure" ]]; then | |
| echo "WARNING: Play Store deployment failed, but Indus App Store succeeded" | |
| fi | |
| if [[ "${{ steps.upload-indus-store.outcome }}" == "failure" ]]; then | |
| if [[ "$INDUS_STATUS" == "skipped" ]]; then | |
| echo "INFO: Indus App Store skipped - app is under review" | |
| else | |
| echo "WARNING: Indus App Store deployment failed, but Play Store succeeded" | |
| fi | |
| fi | |
| echo "At least one deployment succeeded or was skipped" | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 14. Create GitHub Release with APKs and AAB | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Create GitHub Release | |
| if: success() | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ env.APP_VERSION_NAME }} | |
| name: Release v${{ env.APP_VERSION_NAME }} | |
| body: | | |
| ## PDF Toolkit v${{ env.APP_VERSION_NAME }} (Build ${{ env.APP_VERSION_CODE }}) | |
| ### What's New | |
| ${{ steps.generate-notes.outputs.release_notes }} | |
| ### Deployment Status | |
| - Play Store: ${{ steps.upload-play-store.outcome == 'success' && '✅ Deployed' || '❌ Failed' }} | |
| - Indus App Store: ${{ steps.upload-indus-store.outputs.indus_status == 'success' && '✅ Deployed' || steps.upload-indus-store.outputs.indus_status == 'skipped' && '⏭️ Skipped (Under Review)' || '❌ Failed' }} | |
| ### Build Info | |
| - Version Code: ${{ env.APP_VERSION_CODE }} | |
| - Version Name: ${{ env.APP_VERSION_NAME }} | |
| - Min SDK: 26 (Android 8.0) | |
| - Target SDK: 35 (Android 15) | |
| - Build Number: ${{ github.run_number }} | |
| ### Downloads | |
| **APK Files** - For sideloading on Android devices | |
| **AAB File** - Android App Bundle for app stores | |
| files: | | |
| app/build/outputs/bundle/playstoreRelease/*.aab | |
| app/build/outputs/apk/playstore/release/*.apk | |
| app/build/outputs/mapping/playstoreRelease/mapping.txt | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # 15. Post-deploy summary | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| - name: Deployment Summary | |
| if: success() | |
| run: | | |
| echo "## ✅ Deployment Successful!" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| **App** | PDF Toolkit |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Package** | com.yourname.pdftoolkit |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Track** | Internal Testing |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Version Name** | ${{ env.APP_VERSION_NAME }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Version Code** | ${{ env.APP_VERSION_CODE }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Build Number** | ${{ github.run_number }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Commit** | ${{ github.sha }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Triggered by** | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY |