How to Deploy an Android App to Google Play with GitHub Actions
Automate Android App Bundle uploads to Google Play with GitHub Actions — service-account auth, signing, internal testing tracks, and production drafts.
- Author
- Solu-Techs
- Google Play deployment
- GitHub Actions
- Android App Bundle
- Google Play API
- Android signing
- Internal testing
- versionCode
Manually building an Android App Bundle (AAB), signing it on a laptop, and uploading it through the Google Play Console works — until version codes collide, the wrong track is selected, or the person with the keystore is unavailable. Repeating that ritual for every candidate build is slow and easy to get wrong.
GitHub Actions can take that deployment path off your machine: a push or release event builds a signed AAB, authenticates to the Google Play Developer API with a service account, and uploads the bundle to a chosen track.
This tutorial focuses on the Google Play deployment stage of an Android CI/CD pipeline — from authentication and signing through Internal testing and a controlled production draft. It is based on the same kind of Play delivery pattern Solu-Techs uses when shipping its own apps. Paths, package names, and secret names must match your project; treat the YAML as a working template, not a blind copy-paste.
If you need the broader release-automation picture (release branches, multi-app pipelines, and CI environment design), see How to Automate Android App Releases with GitHub Actions. For the service overview, see Android deployment automation.
What You Need
Before you wire up Android app deployment to Google Play, confirm you have:
- An Android project that can produce a release AAB (Gradle
bundle*tasks) - A GitHub repository where the project (or a releaseable module) lives
- Access to the app in Google Play Console
- A Google Play service account with API access to that app (details below)
- Android signing configuration for release builds (upload key / keystore)
- GitHub Actions secrets for signing material and Play credentials
- A clear application ID (package name) that matches the Play Console listing
You do not need every optional Play track on day one. A solid first goal is: automate upload to Internal testing, then decide how production drafts are prepared.
How the GitHub Actions → Google Play Flow Works
The deployment path is a straight line from repository event to Play track:
GitHub (push / release)
→ GitHub Actions
→ Gradle build
→ signed AAB
→ Google Play API (service account)
→ Internal testing / production draft
| Stage | What it does |
|---|---|
| GitHub event | Starts the workflow (for example a release branch push or a published release) |
| GitHub Actions | Runs a known JDK / Android SDK / Gradle environment on a runner |
| Gradle build | Assembles a release Android App Bundle |
| Signing | Applies your upload/app signing keystore from CI secrets |
versionCode / versionName |
Ensures the upload is accepted by Play (version codes must increase) |
| Google Play API | Authenticates with a service account and uploads the AAB |
| Track | Places the release on Internal testing, closed testing, or production (often as a draft) |
Each stage should be explicit in the workflow. Hiding signing or track selection in undocumented local steps is how manual releases drift.
1. Configure Google Play API Access
Automated uploads use the Google Play Android Developer API, typically authenticated with a Google Cloud service account that is linked to your Play Console.
At a high level:
- Create or reuse a Google Cloud service account in a project you control.
- Enable the Google Play Android Developer API for that Cloud project.
- Create a JSON key (or equivalent credential) for the service account — this becomes a GitHub secret later.
- In Google Play Console, invite or link that service account so it can act on your developer account / apps.
- Grant the minimum permissions needed to upload releases and manage the tracks you actually use (for example release management on the target app). Avoid overly broad admin rights if a narrower role is enough.
Exact Console labels change over time; the important outcomes are: API enabled, service account linked to Play, and permission to create releases for the correct package name.
Until the service account can see the app and create releases, every GitHub Actions upload will fail with an authentication or permission error — even if the AAB itself is perfect.
2. Prepare the Android App for Automated Deployment
Play uploads are picky about identity and versioning. Align these before you automate:
Application ID
The application ID (package name) in Gradle must match the app in Play Console. A mismatch is one of the most common “API looks fine, upload fails” failures.
versionCode and versionName
Every Google Play upload for a given package needs a versionCode that is strictly greater than any previously uploaded code. versionName is the human-readable label.
android {
defaultConfig {
applicationId "com.example.yourapp"
versionCode 42
versionName "1.4.2"
}
}
In CI, either bump versionCode in the workflow before building, or generate it from a known source of truth (properties file, Git tag scheme, and so on). Skipping this step is the fastest way to get “version code already used” rejections.
Release build and AAB
Google Play expects an Android App Bundle for modern uploads. Your pipeline should run the appropriate Gradle bundle task (for example bundleRelease, or a flavor-specific variant) and produce a signed .aab under the module’s build/outputs/bundle/... path.
Signing
Release signing must use your upload key (when Play App Signing is enabled) or the appropriate release keystore for your setup. Configure Gradle signing so the CI job can point at a keystore file and passwords restored from secrets — not from committed files.
3. Store Secrets Securely in GitHub Actions
Signing keys and Play API credentials must not live in Git. Commit history is a long-lived leak surface, even in private repositories.
Typical secrets for this pipeline:
| Secret (example name) | Purpose |
|---|---|
ANDROID_KEYSTORE_BASE64 |
Keystore bytes encoded for safe storage |
ANDROID_KEYSTORE_PASSWORD |
Keystore password |
ANDROID_KEY_ALIAS |
Key alias |
ANDROID_KEY_PASSWORD |
Key password |
PLAY_SERVICE_ACCOUNT_JSON |
Service account JSON for the Play Developer API |
Store them as GitHub Actions secrets (repository or organization). The workflow decodes or writes them onto the runner only for the job, then relies on Gradle and the Play upload action to consume them. Never echo secret values into logs.
4. Build the Android App in GitHub Actions
Here is a practical build-oriented job: check out the repo, set up Java, restore signing material, and produce a signed AAB. Adjust module paths, JDK version, and Gradle tasks to your project.
name: Deploy Android to Google Play
on:
push:
branches:
- 'release/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle
- name: Decode keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 -d > ${{ runner.temp }}/upload-keystore.jks
- name: Build signed release bundle
env:
KEYSTORE_PATH: ${{ runner.temp }}/upload-keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: ./gradlew bundleRelease --stacktrace
# Wire KEYSTORE_* into your Gradle signing config via
# gradle.properties, environment properties, or a small init script.
- name: Upload AAB as CI artifact
uses: actions/upload-artifact@v4
with:
name: app-release-aab
path: app/build/outputs/bundle/release/*.aab
if-no-files-found: error
Pin JDK and Gradle Plugin versions that your project already supports. If the repository also uses Node/Yarn for shared tooling, add those setup steps the same way you would in any other CI job — they are optional for a pure Android module.
5. Upload the AAB to Google Play
This is the core of GitHub Actions Android deployment to Play: authenticate with the service account, point at the AAB, set the package name, and choose a track.
A widely used approach is a dedicated Play upload action. Example using Internal testing as the primary destination:
- name: Upload to Google Play (Internal testing)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.yourapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: internal
status: completed
What each input is doing:
- Authentication —
serviceAccountJsonPlainTextsupplies the Play API credential from secrets - Package / application ID — must match Play Console and
applicationId - AAB path — must match the file Gradle actually produced (flavors change this path)
- Track —
internaltargets the Internal testing track
Replace com.example.yourapp and the AAB path with your real values. Action versions and input names can evolve; check the action’s current documentation when you implement this in a live repository.
Alternative tooling (for example Fastlane supply) can perform the same API upload. The important part is a single, logged CI step that owns authentication and track selection — not a manual Console click after every build.
6. Deploy to the Internal Testing Track First
Internal testing is the right default for automation. It lets a small trusted group install the build quickly so you can validate packaging, signing, and smoke behavior before anything approaches production users.
In the example above, track: internal with a completed status publishes the release on Internal testing automatically. That is intentional for candidate builds: you want frequent, low-friction delivery to testers.
What automation should not do by default is treat every successful CI run as a live production ship. A practical policy — and the one Solu-Techs uses in its own release machinery — is:
- Upload candidate builds to Internal testing routinely
- Prepare production as a draft when you are ready for a controlled go-live review in Play Console
That keeps the mechanical upload reliable while production publication stays deliberate.
7. Moving From Internal Testing to Production
Google Play separates audiences with tracks:
| Track | Typical role |
|---|---|
| Internal testing | Fast validation with a small tester list |
| Closed testing | Broader pre-production (when you use it) |
| Production | What end users receive |
An automated pipeline can:
- Always (or usually) land builds on Internal testing
- Optionally create or update a production release as a draft for the same AAB or a later promoted build
- Leave the final “roll out to production” decision to a human in Play Console
Example production draft step (illustrative — combine with your Internal upload policy carefully so you do not surprise yourself):
- name: Create production draft
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.yourapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: production
status: draft
Do not assume every project should auto-complete production releases. Blind production publication removes the last review gate. Draft-first production is usually the safer automation default.
8. Example Production GitHub Actions Workflow
The following workflow shows the full sequence in one place: build → sign → AAB → Google Play Internal testing → optional production draft. All secret names and package paths are placeholders. Adapt them to your module layout, signing Gradle config, and Play Console setup.
name: Android Google Play deploy
on:
push:
branches:
- 'release/**'
jobs:
play-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle
# Optional but recommended: bump versionCode for this release run
# before assembling the bundle so Play accepts the upload.
- name: Decode upload keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 -d > ${{ runner.temp }}/upload-keystore.jks
- name: Assemble signed AAB
env:
KEYSTORE_PATH: ${{ runner.temp }}/upload-keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: ./gradlew bundleRelease --stacktrace
- name: Retain AAB artifact
uses: actions/upload-artifact@v4
with:
name: release-aab
path: app/build/outputs/bundle/release/*.aab
- name: Deploy to Internal testing
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.yourapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: internal
status: completed
- name: Prepare production draft
# Keep this step gated (workflow_dispatch input, environment
# approval, or a dedicated production branch) if you only want
# drafts for selected releases.
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.yourapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: production
status: draft
This mirrors a careful production-oriented policy: Internal testing receives the build for validation; production is prepared as a draft rather than published live by default. Gate the draft step if not every release branch should create one.
9. Common Problems
Practical failures you will hit — and how to think about them:
versionCode already exists
Play rejects uploads that reuse a version code. Automate the bump, or read the highest code already on Play / in your version source of truth before building.
Invalid or insufficient service-account permissions
The account must be linked in Play Console and allowed to manage releases for that app. Re-check API enablement, invitation/linking, and role scope.
Package name mismatch
applicationId, the Play listing, and the upload action’s packageName must agree exactly.
Signing configuration errors
Wrong password, alias, or keystore file usually fails at the Gradle signing step. Confirm secrets decode correctly on the runner and match the upload key registered with Play App Signing.
AAB not found
Flavor or module paths differ (app/build/outputs/bundle/paidRelease/...). Fail the job if the file is missing (if-no-files-found: error) so you do not upload an empty path.
Google Play API authentication errors
Corrupt JSON secret, wrong Cloud project, or disabled Developer API. Validate the credential outside CI once, then store the same JSON in secrets.
Wrong track
Uploading to production when you meant internal (or the reverse) is a process bug. Keep track names explicit in YAML and review them in PR diffs.
Release rejected by Google Play
Policy, store listing, or review issues are separate from CI mechanics. The pipeline can deliver the binary; Console / policy compliance still applies.
10. How This Fits Into a Complete Android CI/CD Pipeline
Google Play deployment is one stage of a larger release system. A complete Android CI/CD path usually also includes automated tests, consistent versioning, secure signing, AAB generation, artifact retention, release-branch conventions, and — for some teams — configuration for multiple apps or brands.
This article stayed on the Play delivery path on purpose. For the wider release-automation guide — triggers, CI environment design, multi-app thinking, and how those stages fit together — read How to Automate Android App Releases with GitHub Actions.
11. When to Automate Google Play Deployment
Automation pays off when:
- Your team releases frequently enough that Console uploads become a bottleneck
- You ship multiple apps or brands with the same packaging pattern
- You maintain multiple environments or tracks (Internal vs production drafts)
- Manual releases depend on one laptop’s keystore and local Gradle setup
- An agency or platform team manages several Android apps for clients
- You want reproducible uploads: same signing path, same track policy, logged every time
If that matches how you ship today, Solu-Techs builds and maintains Android release pipelines — including GitHub Actions, signing, AAB generation, and Google Play track delivery — based on the same approach we use for our own applications. Explore Android deployment automation or contact us with a short description of your current Play release process.
Next step
If your Android team is still building AABs manually, managing signing credentials by hand, or uploading releases to Google Play manually, Solu-Techs can build and maintain the release pipeline for you.
Start with Android deployment automation, or get in touch to discuss your repository and Play Console setup.