Skip to content

[feature] Play publishing authenticates keylessly, with no credential at rest - #279

Merged
YJack0000 merged 1 commit into
mainfrom
feat/play-wif
Aug 20, 2026
Merged

[feature] Play publishing authenticates keylessly, with no credential at rest#279
YJack0000 merged 1 commit into
mainfrom
feat/play-wif

Conversation

@YJack0000

Copy link
Copy Markdown
Contributor

What this changes

Play publishing authenticates through Workload Identity Federation instead of a service-account key. There is no PLAY_SERVICE_ACCOUNT_JSON secret, and there will not be one.

Why

Setting the service account up hit a wall the plan in #276 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 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-play never parses the credential — it only points GOOGLE_APPLICATION_CREDENTIALS at a file and lets the Google auth library resolve it (main.ts), so external-account credentials drop in exactly where a key used to.

key in a secret WIF
credential at rest yes, indefinitely none
org policy needs an exception satisfied as-is
rotation manual, forever per run
blast radius if the repo is compromised key exfiltrates token expires, and only from this repo

What was provisioned

Done and verified on 2026-08-20, by the script in this PR:

project pathors-play (474482934105) — dedicated, because a Play account links exactly one project and moving it later is painful
APIs androidpublisher, sts, iamcredentials
service account parley-play-publisher@pathors-play.iam.gserviceaccount.com, no project-level IAM roles — everything it may do is granted in the Play Console
pool / provider github / pathorsai, issuer token.actions.githubusercontent.com
fence 1 provider attribute condition assertion.repository_owner=='pathorsAI'
fence 2 roles/iam.workloadIdentityUser for attribute.repository/pathorsAI/parley — that repository and no other

Two 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:

PLAY_WIF_PROVIDER              projects/474482934105/locations/global/workloadIdentityPools/github/providers/pathorsai
PLAY_PUBLISHER_SERVICE_ACCOUNT parley-play-publisher@pathors-play.iam.gserviceaccount.com

How it was verified

  • Ran ./android/scripts/create-play-service-account.sh — it created all of the above against the live project
  • Ran it twice more: reuses project, service account, pool and provider; re-binds and re-publishes the variables. Idempotent, so it is also the repair path
  • gh variable list shows both variables set
  • Resolved google-github-actions/auth v3.0.0's SHA from the GitHub API rather than from memory, and confirmed v3 still exposes credentials_file_path
  • Workflow parses as YAML; permissions is contents: write + id-token: write
  • bunx tsc --noEmit, bunx vitest run
  • The upload itself — cannot be tested until the two Play Console steps below are done. No API exists for them.

The 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.

  1. Play Console → Setup → API access → link the Cloud project pathors-play.
  2. Same page → Service accountsparley-play-publisher@pathors-play.iam.gserviceaccount.comGrant accessRelease manager → restrict to Parley under App permissionsInvite user.

Then android-v0.1.1 proves 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.

… 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.
@YJack0000
YJack0000 merged commit f0843aa into main Aug 20, 2026
3 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant