Automate Android release signing and Google Play upload - #225
Conversation
- Add upload keystore signing to build.gradle via key.properties pattern (falls back to debug signing locally when key.properties is absent) - Inject ANDROID_KEYSTORE_BASE64 / key.properties in CI before the build - Add publish-google-play job that uploads the signed AAB to Internal track using r0adkll/upload-google-play and a Google Play service account secret - Drop "unsigned" suffix from release artifact filenames - Update release notes to reflect Play App Signing model Required new secrets: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_PASSWORD, ANDROID_KEY_ALIAS, GOOGLE_PLAY_SERVICE_ACCOUNT_JSON https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz
- android-release.md: replace "future" signing section with current implementation; update artifact names (drop -unsigned suffix); update step 4 to mention automated Google Play upload - github-secrets.md: add Android keystore secrets (ANDROID_KEYSTORE_BASE64, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD, ANDROID_KEYSTORE_PASSWORD) and Google Play service account secret with full setup walkthrough - ci-cd.md: add Release Android workflow to overview and secrets tables; update production release flow to include Play promotion step - play-store.md: update Quick Start to reflect automated upload via CI; add CI secrets to submission checklist; note first upload must be manual https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz
- Fail fast with a clear error message if ANDROID_KEYSTORE_BASE64 is unset, rather than silently producing an unsigned build that Play rejects - Replace heredoc in key.properties creation with printf to avoid any leading-whitespace ambiguity from YAML indentation - Add prominent warning in android-release.md and play-store.md that the Google Play API cannot create a new app listing: first AAB must be uploaded manually through Play Console before CI automation kicks in https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz
The Flutter app replaces an existing self-signed APK app (ralcock.cbf). For a seamless upgrade, the original signing keystore must be enrolled in Play App Signing as the app signing key before any CI upload runs. - android-release.md: replace generic "First-Time Setup" / "Updating an Existing App" with a concrete migration guide: version code check, pepk-based Play App Signing enrollment, first manual AAB upload, then automated CI flow for all subsequent releases - android-release.md: fix stale package name placeholder → ralcock.cbf - github-secrets.md: clarify that ANDROID_KEYSTORE_BASE64 must be the *original* signing keystore for an existing app (not a newly generated one), with a callout explaining why - play-store.md: replace "new app" Quick Start with a "replacing existing app" flow including the pre-migration checklist and version code check; fix package name placeholder → ralcock.cbf https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR adds Android release-signing and Google Play upload automation to the existing release pipeline, extending the app’s deployment tooling beyond the current web-focused release flow. It wires CI secrets into the Android build, updates Gradle signing behavior, and refreshes the operational docs to describe Play App Signing and Play Console release handling.
Changes:
- Add release signing support in
android/app/build.gradleusingandroid/key.properties, with a local fallback to debug signing when no keystore is configured. - Extend
release-android.ymlto decode the upload keystore in CI, build signed APK/AAB artifacts, rename release outputs, and upload the AAB to Google Play Internal testing. - Update Android/Play Store/CI documentation to describe the new secrets, Play App Signing migration, and the intended first-release/manual-upload workflow.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
docs/tooling/play-store.md |
Updates Play Store setup guidance for replacing the existing Android app and documents the manual first-upload flow. |
docs/tooling/github-secrets.md |
Expands GitHub Actions secret setup to include Android signing and Google Play service account credentials. |
docs/tooling/android-release.md |
Revises Android release docs for signed artifacts, Play App Signing, and CI-driven Play uploads. |
docs/processes/ci-cd.md |
Documents the new Android release workflow and its required secrets/manual promotion steps. |
android/app/build.gradle |
Adds key.properties-based release signing configuration with a debug-signing fallback for local builds. |
.github/workflows/release-android.yml |
Injects signing material, builds signed Android artifacts, creates release assets, and uploads AABs to Google Play. |
| Every push of a `v*` tag will automatically: | ||
| 1. Build the signed AAB | ||
| 2. Upload it to the **Internal track** in Play Console | ||
| 3. The release appears immediately for internal testers | ||
|
|
| > For the **very first Flutter release** you must upload manually: | ||
| > 1. Wait for the GitHub Actions `Release Android` workflow to complete | ||
| > 2. Download the AAB from the GitHub Release | ||
| > 3. In Play Console → **Internal testing** → **Create new release** → upload the AAB | ||
| > 4. Submit and confirm testers can install | ||
| > | ||
| > From the **second release onwards**, CI uploads to Internal track automatically. |
| - name: Create key.properties | ||
| run: | | ||
| printf 'storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=%s\n' \ | ||
| "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ | ||
| "${{ secrets.ANDROID_KEY_PASSWORD }}" \ | ||
| "${{ secrets.ANDROID_KEY_ALIAS }}" \ | ||
| "../upload-keystore.jks" \ | ||
| > android/key.properties |
| - name: Upload to Google Play (Internal track) | ||
| uses: r0adkll/upload-google-play@v1 | ||
| with: | ||
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | ||
| packageName: ralcock.cbf | ||
| releaseFiles: artifacts/aab/app-release.aab | ||
| track: internal | ||
| status: completed |
| - name: Upload to Google Play (Internal track) | ||
| uses: r0adkll/upload-google-play@v1 | ||
| with: | ||
| serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} | ||
| packageName: ralcock.cbf | ||
| releaseFiles: artifacts/aab/app-release.aab | ||
| track: internal | ||
| status: completed |
| ### One-Time Setup: Create the Upload Keystore | ||
|
|
||
| **Go to**: Repository Settings → Secrets and variables → Actions → New repository secret | ||
| Run this once locally and store the `.jks` file somewhere safe (password manager, etc.): | ||
|
|
||
| | Secret Name | Description | How to Get | | ||
| |-------------|-------------|------------| | ||
| | `KEYSTORE_BASE64` | Base64-encoded keystore file | `base64 -i upload-keystore.jks \| tr -d '\n'` | | ||
| | `KEYSTORE_PASSWORD` | Keystore password | The password you used when creating the keystore | | ||
| | `KEY_ALIAS` | Key alias | Usually `upload` (or whatever you specified) | | ||
| | `KEY_PASSWORD` | Key password | Usually same as keystore password | | ||
| ```bash | ||
| keytool -genkey -v -keystore upload-keystore.jks \ | ||
| -keyalg RSA -keysize 2048 -validity 10000 \ | ||
| -alias upload | ||
| ``` |
Workflow fixes: - Validate all four signing secrets upfront (not just ANDROID_KEYSTORE_BASE64) so missing secrets produce clear error messages rather than cryptic Gradle failures - Gate publish-google-play to tag pushes only (if: push + refs/tags/v*) so workflow_dispatch test runs cannot accidentally ship untagged builds to testers - Add continue-on-error: true to publish-google-play so the first tag push (before the app listing exists in Play Console) fails gracefully rather than marking the whole workflow as failed Doc fixes: - android-release.md: replace keytool -genkey instruction with a callout directing readers to use the original signing keystore (generating a new one would break the seamless upgrade path for existing ralcock.cbf users) - github-secrets.md: add first-manual-upload caveat to "What happens when it's configured" section - play-store.md: clarify that the workflow succeeds on first run but skips the Play upload step until the app listing exists https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz
🚀 Cloudflare Pages PreviewYour preview deployment is ready! Preview URL: https://claude-automate-android-rele.staging-cambeerfestival.pages.dev This preview will be automatically updated when you push new commits to this PR. |
…nterpolation Prevents issues with shell metacharacters (e.g. % in printf, special chars in base64 decode) by binding secrets to environment variables rather than expanding them directly into the shell script. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🚀 Cloudflare Pages PreviewYour preview deployment is ready! Preview URL: https://claude-automate-android-rele.staging-cambeerfestival.pages.dev This preview will be automatically updated when you push new commits to this PR. |
(falls back to debug signing locally when key.properties is absent)
using r0adkll/upload-google-play and a Google Play service account secret
Required new secrets: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD,
ANDROID_KEY_PASSWORD, ANDROID_KEY_ALIAS, GOOGLE_PLAY_SERVICE_ACCOUNT_JSON
https://claude.ai/code/session_01LRvsZuXFafSSFLZrcB53fz