# upload-google-play: shipping Android builds to the Play Console from a workflow

> This GitHub action uploads an .apk or .aab to the Google Play Console through the Play Developer API v3, with staged rollout, in-app update priority, localized release notes and a choice between a JSON key secret and workload identity. It suits a team already on GitHub Actions who wants the release step in the same pipeline as the build, and the service account setup is the part that needs care.

**r0adkll/upload-google-play** — A GitHub action to upload an Android .apk or .aab file to the Google Play Console

- Repository: https://github.com/r0adkll/upload-google-play
- Stars: 1,053 · Forks: 191
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/r0adkll-upload-google-play

## The action handles the edit, not just the upload

This action will help you upload an Android `.apk` or `.aab` (Android App Bundle) file to the Google Play Console using the Google Play Developer API v3.

The detail that makes it more than a file uploader is the edit model. In the Play Developer API, an upload is not a single operation, it is the creation of an edit, the addition of artefacts to that edit, and then a commit that makes it live. The action exposes that lifecycle through its outputs.

`committedEditId` is the unique identifier of the committed edit, and `committedEditIdExpiryTimeSeconds` is the time in seconds until the committed edit expires. The second output exists because Google expires edits, and a workflow that needs to add something to an edit later has to know whether the edit it is holding is still valid.

The `existingEditId` input closes the loop. It is the ID of an existing edit that has not been completed, and if this is supplied, the action will append information to that rather than creating an edit. That is what makes a two step release possible, where a build is uploaded in one job and committed in another once the decision has been made, without the artefacts being uploaded twice.

`versionCodesToRetain` extends the same idea to the artefacts themselves, letting you keep version codes from previous releases rather than having them dropped.

So the action is a thin layer over the API's transactional shape, and the inputs and outputs are the API's vocabulary: a create, an add, a commit, with the ability to resume an edit that is still open.

## Staged rollout, priority and release status

The inputs that control what happens after the upload are the ones that matter for a real release, and they map closely onto the Play Console's own controls.

`tracks` is the list of tracks in which you want to assign the uploaded app, and it defaults to `production`. The documentation notes that the track names are calculated per the APKs and Tracks documentation, and the deprecated `track` input is still present but marked for removal, with a note to switch to `tracks`. The deprecation is spelled out with two examples, a single track:

```yaml
with:
  tracks: production
```

and multiple tracks, comma-separated:

```yaml
with:
  tracks: internal,qa
```

`status` is the release status, defaulting to `completed`, and it is one of `completed`, `inProgress`, `halted` or `draft`, and it cannot be null. The distinction between `completed` and `inProgress` is the difference between a release that goes live immediately and one that goes live as a staged rollout, and it is the switch you flip when you want to hold a build for a moment before the percentage starts climbing.

`userFraction` is the percentage of users who should get the staged version of the app, and it is a decimal between 0.0 and 1.0. `inAppUpdatePriority` is the in-app update priority of the release, where all newly added APKs in the release will be considered at this priority, and it ranges from 0 to 5 with 5 the highest.

`releaseName` is the release name, not required to be unique, and it defaults to whatever the Play Console configures, with a user-friendly update name such as `v1.0.0` as the example.

And `changesNotSentForReview` indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Google Play Console, defaulting to `false`. That flag is a review workflow control, and it is the input that stops a release from entering the review queue when the team wants to submit manually.

## Release notes, mapping files and debug symbols

Three inputs exist to carry the extra artefacts a real release needs, and each solves a specific problem.

`whatsNewDirectory` is the directory of localized whats new files to upload as the release notes, with a strict naming rule. The files contained in the directory MUST use the pattern `whatsnew-<LOCALE>` where LOCALE uses the BCP 47 format. That constraint is worth understanding, because a misnamed file is silently ignored rather than rejected, and the release goes out with no notes for that locale.

`mappingFile` is the mapping.txt file used to de-obfuscate your stack traces from crash reports. This is the R8 or ProGuard mapping, and shipping it to Play is what makes your crash reports readable in the console. A release without it produces a stack trace full of a.b.c and no way to map it back to your source.

`debugSymbols` is the native-debug-symbols.zip file or folder containing your debug symbols, and the description notes it accepts a path to a valid file or a folder.

The `releaseFiles` input is the other side of the same problem, since it accepts multiple files. The release files to upload are given as comma-separated paths, and it supports glob via fast-glob, and it is required. The deprecated `releaseFile` input is still in the table, marked for removal.

`packageName` is the package name, or Application Id, of the app you are uploading, and it is required. The documentation is clear about a constraint that catches people out: the packageName must already exist in the play console account, so make sure you upload a manual apk or aab first through the console. The action cannot create an app, it can only add a version to one that already exists.

So the full set of inputs is a description of a release: what to upload, where to put it, what to call it, how far to roll it out, what notes go with it, and what to attach.

## Authentication: a JSON key or workload identity

The setup is the part of this action with real consequences, because it decides how a Play Store credential lives in your CI.

There are two ways to give the action credentials. `serviceAccountJsonPlainText` takes the contents of your `service-account.json` provided via a secret, and `serviceAccountJson` takes a path to a service account json private key file. Either satisfies the requirement, and the table marks each as required only in the alternative, so you must supply one of the two.

The setup procedure in the documentation is ordered, and the order matters. First, enable the Google Play Android Developer API at the console.cloud.google.com APIs library page and click Enable. Then create a new service account in Google Cloud Platform, navigating to IAM and Admin, Service accounts, Create service account. Pick a name for the new account, and the instruction is explicit: do not grant the account any permissions.

That last point surprises people who are used to granting roles. The service account needs no Google Cloud permissions of its own, because its authority comes from being invited into the Play Console, which is the next step.

For the credential itself there are two routes. The account key in GitHub secrets is described as simpler: open the service account, click the keys tab, add a new key of JSON type, and the JSON file downloads automatically. Store its content as a GitHub secret such as `SERVICE_ACCOUNT_JSON`, then set `serviceAccountJsonPlainText: ${{ SERVICE_ACCOUNT_JSON }}`.

The other route is workload identity authentication, described as more secure and recommended by GCP. You configure a workload identity provider in the same project as the service account, then run a step to obtain short-lived access credentials:

```yaml
- id: auth
  uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: <project>/.../workloadIdentityPools/<provider>
    service_account: <service-account>@<project>.iam.gserviceaccount.com
```

and then set `serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}`.

The difference is that the first route stores a long-lived private key in your CI secrets, and the second stores nothing permanent at all, because the credentials are minted for the job and expire. The documentation marks the second as recommended by Google, and for a credential that can push to production, that is the right default.

## The Play Console side, and the internals of the action itself

The final setup step is granting the service account its authority, and it happens in the Play Console rather than in Google Cloud.

Open the Play Console, pick your developer account, open Users and permissions, click invite new user and add the email of the service account created in the previous step. Then grant permissions to the app that you want the service account to deploy in `app permissions`.

The `app permissions` wording is the fine print worth reading twice. The service account is granted permission at the app level, not the account level, so if your team ships several apps you need a separate grant for each one you want this account to deploy. It also means the credential can only publish what it has been given, which is the property you want from a CI credential.

The repository is a TypeScript GitHub action, and the layout is the standard one. There is an `action.yml` at the root, `src/` for the TypeScript sources, `lib/` for the built output, `__tests__/` for the tests, a `jest.config.ts`, a `tsconfig.json` and an `.eslintrc.cjs`.

The build script is `ncc build src/main.ts -m -o lib/`, and the `lib/` directory is committed. That is deliberate for a GitHub action, because the runner executes the bundled JavaScript rather than compiling the TypeScript, and checking in the build means the action works without the consumer installing a toolchain.

The dependency list explains the feature set. `@googleapis/androidpublisher` at `^35.3.0` is the Play Developer API client, `google-auth-library` at `^10.6.2` handles the credential, `fast-glob` at `^3.3.3` expands the release file patterns, `@actions/core` at `^2.0.3` is the action's own input and output plumbing, `jszip` at `^3.10.1` handles the archive formats, and `p-timeout` at `^7.0.1` gives the API calls a deadline, which matters because a Play API edit that hangs is a workflow that hangs.

The dev dependencies are a modern toolchain, with jest at 30 and ts-jest for the tests, typescript at `^6.0.3`, eslint at `^10.2.1` with the typescript-eslint plugin, and `@vercel/ncc` for the bundler.

The licence is MIT, and the version history shows v1 from 2019, v1.1.4 on 2026-04-20 and v1.1.5 on 2026-04-21, so this is a long lived action with a recent patch.

## Conclusion

upload-google-play fits a team that builds Android on GitHub Actions and wants the Play upload to be one step in the same workflow, because the action handles the edit lifecycle, the track assignment and the release notes, and because it accepts either a JSON key secret or workload identity. Skip it if you build elsewhere, since this is a GitHub action and not a script, and be deliberate about which authentication route you take, because the long-lived JSON key is simpler while the short-lived credentials are what Google recommends. Before you wire it up, follow the setup order in the documentation, since the API has to be enabled, the service account created and then invited into the Play Console with app permissions, and note the release is v1.1.5 with the last push on 2026-10-12.

## FAQ

### What does the upload-google-play action do?

It uploads an Android .apk or .aab file to the Google Play Console using the Google Play Developer API v3, handling the edit lifecycle, track assignment, staged rollout and localized release notes.

### How do I authenticate the upload-google-play action?

Either with `serviceAccountJsonPlainText` holding the contents of your service-account.json from a GitHub secret, or with `serviceAccountJson` pointing at a key file, which is the route to use with workload identity authentication and short-lived credentials.

### How do I do a staged rollout with upload-google-play?

Set `status` to `inProgress` and give a `userFraction` between 0.0 and 1.0, and use `inAppUpdatePriority` between 0 and 5 to set the priority of the release. The default status is `completed`, which publishes immediately.

### What are the required inputs for upload-google-play?

The release files as comma-separated paths supporting glob, the service account credential in one of two forms, the package name which must already exist in the Play Console, and the tracks to assign the upload to.

### Can I add artefacts to a Play edit that was already created?

Yes. Supply `existingEditId` with the ID of an edit that has not been completed, and the action appends to it rather than creating a new one. The outputs `committedEditId` and `committedEditIdExpiryTimeSeconds` report the resulting edit and when it expires.

## Sources

- [Issues](https://github.com/r0adkll/upload-google-play/issues)
- [License: MIT](https://github.com/r0adkll/upload-google-play/blob/master/LICENSE)
- [r0adkll/upload-google-play on GitHub](https://github.com/r0adkll/upload-google-play)
- [README](https://github.com/r0adkll/upload-google-play/blob/master/README.md)
- [Releases](https://github.com/r0adkll/upload-google-play/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/r0adkll-upload-google-play
