[feature] Play publishing authenticates keylessly, with no credential at rest - #279
Merged
Conversation
… at rest
Setting up the service account hit the wall the plan did not account for:
FAILED_PRECONDITION: Key creation is not allowed on this service account
constraints/iam.disableServiceAccountKeyCreation
The pathors.com organization enforces that constraint. It is Google's default
for new organizations and it is a good one — a Play publishing key is a
long-lived credential that can ship code to users, and `PLAY_SERVICE_ACCOUNT_JSON`
would have parked exactly that in a GitHub secret.
Granting the project an exception to store the key anyway would have been
fighting the org's own security decision to arrive somewhere worse. Workload
Identity Federation gets there with nothing at rest: GitHub mints an OIDC token
per run, Google exchanges it for a credential that impersonates the publisher
account and expires by itself, and there is no key to leak, rotate or find in a
log. `r0adkll/upload-google-play` only points GOOGLE_APPLICATION_CREDENTIALS at
a file and lets the Google auth library resolve it, so the external-account
credentials work exactly where a key used to.
Two fences, because the provider and the impersonation are different questions:
the provider's attribute condition admits only `pathorsAI` repositories, and
`roles/iam.workloadIdentityUser` on the service account admits only
`pathorsAI/parley`. Without the first, any repository on GitHub can present a
token to the provider.
Config moves from a secret to two repository *variables*, since neither value
authorizes anything on its own and being able to read them out of a failed
run's logs is the point.
Provisioned and verified on 2026-08-20 — project `pathors-play` (474482934105),
`androidpublisher`/`sts`/`iamcredentials` enabled, service account with no
project-level IAM roles, pool `github`, provider `pathorsai`. The script does
all of it idempotently; re-running is how to repair one piece.
Still outstanding, and with no API to do it through: linking the Cloud project
in the Play Console, and granting the account Release manager. Linking is what
*enables* the Play Developer API, so it cannot go through the API it enables.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What this changes
Play publishing authenticates through Workload Identity Federation instead of a service-account key. There is no
PLAY_SERVICE_ACCOUNT_JSONsecret, and there will not be one.Why
Setting the service account up hit a wall the plan in #276 did not account for:
The
pathors.comorganization enforces that constraint. It is Google's default for new organizations, and it is a good one — a Play publishing key is a long-lived credential that can ship code to users, and the plan was to park exactly that in a GitHub secret.Granting the project an exception would have meant overriding the org's own security decision in order to arrive somewhere strictly worse. WIF gets to the same place with nothing at rest: GitHub mints an OIDC token per run, Google exchanges it for a credential that impersonates the publisher account and expires on its own.
This works because
r0adkll/upload-google-playnever parses the credential — it only pointsGOOGLE_APPLICATION_CREDENTIALSat a file and lets the Google auth library resolve it (main.ts), so external-account credentials drop in exactly where a key used to.What was provisioned
Done and verified on 2026-08-20, by the script in this PR:
pathors-play(474482934105) — dedicated, because a Play account links exactly one project and moving it later is painfulandroidpublisher,sts,iamcredentialsparley-play-publisher@pathors-play.iam.gserviceaccount.com, no project-level IAM roles — everything it may do is granted in the Play Consolegithub/pathorsai, issuertoken.actions.githubusercontent.comassertion.repository_owner=='pathorsAI'roles/iam.workloadIdentityUserforattribute.repository/pathorsAI/parley— that repository and no otherTwo fences because they answer different questions. Without the first, any repository on GitHub can present a token to the provider.
Config lands in repository variables, not secrets — neither value authorizes anything alone, and reading them out of a failed run's log is how you debug it:
How it was verified
./android/scripts/create-play-service-account.sh— it created all of the above against the live projectgh variable listshows both variables setgoogle-github-actions/authv3.0.0's SHA from the GitHub API rather than from memory, and confirmed v3 still exposescredentials_file_pathpermissionsiscontents: write+id-token: writebunx tsc --noEmit,bunx vitest runThe two steps left, which have no API
Linking a Cloud project to a Play account is what enables the Play Developer API, so it cannot go through the API it enables.
pathors-play.parley-play-publisher@pathors-play.iam.gserviceaccount.com→ Grant access → Release manager → restrict to Parley under App permissions → Invite user.Then
android-v0.1.1proves the whole path. Note the first bundle for the package still has to be uploaded by hand (skip_play_upload=true) — Play refuses a package's first bundle over the API regardless of how it authenticates.