Oore CI
Build and shipDeliver builds

Publish to Google Play

Upload a signed App Bundle to a Google Play track with a staged rollout and release notes.

Turn on Google Play publishing for a pipeline so a successful Android build uploads its signed App Bundle to the track you choose. The runner that built the app uploads it from its own host, so the bundle never passes through the daemon.

What you need

  • Project developer, maintainer, instance admin, or instance owner access. Storing a shared instance secret needs instance admin or owner access.
  • An app that already exists in Play Console, with one release uploaded by hand. Google Play does not let the API create an app or upload its first release. Deliver to testers on Google Play walks through that first upload.
  • A pipeline that builds a release App Bundle and signs it with your upload key. See Add Android signing.
  • A Google Cloud service account with a JSON key, invited in Play Console under Users and permissions with release permissions for the app.
  • Outbound HTTPS from the runner host to oauth2.googleapis.com and androidpublisher.googleapis.com. The daemon host needs the same for credential checks and rollout controls.

1. Store the service account key as a secret

Store the whole JSON key file as a secret named PLAY_SERVICE_ACCOUNT. When all your apps are in one Play Console developer account, store it once as an instance secret: open Settings > Store credentials and, under Google Play service account, select Add service account. New pipelines then choose it on the Publishing tab without another upload. A project or pipeline secret with the same name overrides it.

From the API, any scope works. A project secret is shared by all of the project’s pipelines:

jq -n --rawfile value play-service-account.json \
  '{name: "PLAY_SERVICE_ACCOUNT", value: $value}' |
  curl -X POST "$OORE_URL/v1/projects/$PROJECT_ID/secrets" \
    -H "Authorization: Bearer $OORE_TOKEN" \
    -H 'Content-Type: application/json' \
    -d @-

See Use secrets in a pipeline for scopes and rotation.

2. Configure publishing

  1. Open the pipeline and its Publishing tab.
  2. Under Google Play, turn on Upload to Google Play after each successful build.
  3. Under Credentials, choose the secret for Service account key.
  4. Under Release, choose the Track and the Rollout: Full rollout, Staged rollout with an Initial rollout (%), or Draft. Choose the Release notes source.
  5. Under Advanced, enter the app’s application ID in Expected package.
  6. Select Turn on Google Play publishing, or Save changes when publishing was already on.

The readiness list at the top of the section shows what is still missing, such as release signing or an App Bundle build. While uploads are on and the Android command is Oore’s default, Oore builds an App Bundle as well as the APK. An Android command override must build the bundle itself, such as flutter build appbundle --release.

For automation, send the same settings with the Update Google Play publish settings operation:

curl -X PUT "$OORE_URL/v1/pipelines/$PIPELINE_ID/publish/google-play" \
  -H "Authorization: Bearer $OORE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled": true,
    "service_account_secret": "PLAY_SERVICE_ACCOUNT",
    "track": "production",
    "rollout_percent": 5,
    "release_notes": "changelog"
  }'

The response includes service_account_secret_present, which is false until the named secret exists in a scope this pipeline can see.

FieldDefaultMeaning
enabledfalseUpload after each successful build of this pipeline.
service_account_secretName of the secret holding the service account JSON key.
trackinternal, closed, open, production, or the name of a custom closed track. closed is Play’s alpha and open is beta.
package_nameAABExpected application id. When set, a bundle for a different package fails the upload.
rollout_percent100Initial staged rollout. Below 100 creates an in-progress release for that share of users. The internal track does not support staged rollouts.
draftfalseCreate a draft release instead of rolling out.
release_notesnonenone, changelog (the build changelog) or repo_files.
release_notes_localeen-USLanguage for changelog release notes.
release_notes_dirFor repo_files: a repository directory with one <locale>.txt file per language, such as release-notes/en-US.txt.
upload_mappingtrueUpload build/app/outputs/mapping/<variant>/mapping.txt when the build produced it.
in_app_update_priorityIn-app update priority, 0 to 5.

Release notes longer than Play’s 500-character limit are shortened.

3. Test the credentials

On the pipeline’s Publishing tab, select Test credentials under Credentials to check the key without running a build. The daemon exchanges the key for a token, then opens a Play edit for the app and discards it, which needs the same Play Console access as an upload and changes nothing. A failure shows Google’s reason, such as a rejected key or a service account that is not invited for the app.

The check opens the app named in the Expected package field. When that is empty, it uses the saved package_name, then the package of the pipeline’s latest Google Play release. From the API, use the Test Google Play credentials operation:

curl -X POST "$OORE_URL/v1/pipelines/$PIPELINE_ID/publish/google-play/test-credentials" \
  -H "Authorization: Bearer $OORE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"service_account_secret": "PLAY_SERVICE_ACCOUNT", "package_name": "com.example.app"}'

The response has ok, a message with what passed or the exact failure, and the key’s service_account_email.

Oore keeps the pipeline’s latest check, and the Publishing tab shows it with when it ran. It is returned as last_credentials_check with the Google Play publish settings until the key behind its secret name changes. Rotating the secret, deleting it, or adding a narrower secret with the same name clears it.

4. Run a build

After the build, signing and artifact steps succeed, the runner adds a publish-play step that:

  1. Picks the release App Bundle under build/app/outputs/bundle and reads its package name, versionCode and versionName.
  2. Exchanges the service account key for an access token.
  3. Reads the version codes Play already holds and stops if the bundle’s versionCode is not higher than the highest one on the target track, or was already uploaded. The error names both numbers. Set build_number_offset and build with --build-number=$OORE_VERSION_CODE to start above the store; see the .oore.yaml reference.
  4. Opens a Play edit, uploads the bundle and mapping.txt, sets the track release with the configured status and rollout, and commits the edit.

If any earlier step fails, nothing is uploaded. If a Play call fails, the publish-play step fails with the Play API’s error text, and the edit is discarded so nothing is left half-staged in Play Console.

After fixing the cause, such as a missing Play Console permission, retry the upload to upload the stored bundle without building again.

Verify the result

  • The build shows a succeeded publish-play step, and its log names the track and rollout.
  • Play Console shows the version on the chosen track at the chosen rollout.
  • The audit log has a store_upload_completed entry for the build.

Store health on Home

Home lists every app a pipeline publishes to Google Play: each package named in a pipeline’s Expected package, or reached by a passing Test credentials check, and each package Oore has released. A pipeline that sets neither and has not released yet does not name an app, so Home cannot list it; fill in Expected package to add it.

For each app, the daemon reads the user-perceived crash rate and ANR rate over the last 28 days from the Play Developer Reporting API, with the top crash and ANR clusters. Home shows them against Google Play’s bad-behavior thresholds of 1.09% and 0.47%. The numbers refresh every six hours with the service account of the pipeline that made the newest release, or, before the first release, the pipeline whose Google Play settings were saved last.

The service account needs View app information for the app in Play Console, and the daemon host needs outbound HTTPS to playdeveloperreporting.googleapis.com. Without them, Home shows why the numbers could not be read. A failed read is retried after 15 minutes; after fixing access, choose Refresh in Store health to read the store at once.

Troubleshooting

publish-play fails because the version code is not higher

The message names both numbers. Set Build number offset above the highest version code on Google Play, and pass --build-number=$OORE_VERSION_CODE to Flutter.

publish-play fails with a Play API error

Fix the cause, such as a missing Play Console permission, then retry the upload. The stored bundle is uploaded again without rebuilding.

Test credentials fails

The message gives Google’s reason. Invite the service account for this app in Play Console, wait a few minutes for Google to process the invitation, and test again.

Limits

  • One App Bundle per publish. A pipeline that builds several release flavors fails with a list of the bundles it found.

Next step

Promote builds between environments to move a tested version from internal testing to production without rebuilding it.