Skip to content
Solu-Techs
Menu
Android CI/CD 11 min read

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:

  1. Create or reuse a Google Cloud service account in a project you control.
  2. Enable the Google Play Android Developer API for that Cloud project.
  3. Create a JSON key (or equivalent credential) for the service account — this becomes a GitHub secret later.
  4. In Google Play Console, invite or link that service account so it can act on your developer account / apps.
  5. 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 — serviceAccountJsonPlainText supplies 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 — internal targets 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:

  1. Always (or usually) land builds on Internal testing
  2. Optionally create or update a production release as a draft for the same AAB or a later promoted build
  3. 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.

  • Android CI/CD 10 min read

    How to Automate Android App Releases with GitHub Actions

    A practical guide to Android CI/CD with GitHub Actions — from release-branch triggers and secure signing to AAB builds, Internal testing, and production drafts on Google Play.

    • Android CI/CD
    • GitHub Actions
    • Google Play
    • Android release automation
    • AAB
    • Android signing
    • Firebase Analytics