Most of the publishing actions I looked at require a long-lived OAuth refresh token in repository secrets, and that credential can publish every extension of the publisher. Publish to Chrome Web Store, a GitHub Action at v1.2.0 since 26 September 2026, gets a 30-minute access token for each release through GitHub OIDC and Workload Identity Federation instead. It uses only the Chrome Web Store API v2, checks the store state before writing, and has no runtime dependencies.
Google's v1.1 reference says the old API is deprecated and only supported until 15 October 2026.
The credential problem
The chromewebstore OAuth scope covers every item of a publisher, not one extension, and once the store approves an upload, Chrome's auto-update delivers it to every user. Google Cloud can trust GitHub's OIDC tokens through Workload Identity Federation, and the Chrome Web Store accepts a service account linked to the publisher.
The release job exchanges its OIDC token for an access token that lasts 30 minutes:
tag push, approved in the chrome-web-store environment
↓ GitHub OIDC token (repository, ref and environment)
Google Workload Identity provider
↓ checks owner ID, repository ID, tag ref and environment
service account linked to the publisher
↓ access token, chromewebstore scope, 30 minutes
publish-to-chrome-web-store
publish:
runs-on: ubuntu-latest
environment: chrome-web-store
permissions:
id-token: write
concurrency:
group: chrome-web-store
cancel-in-progress: false
steps:
- id: auth
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ vars.CWS_WIF_PROVIDER }}
service_account: ${{ vars.CWS_SERVICE_ACCOUNT }}
token_format: access_token
access_token_scopes: https://www.googleapis.com/auth/chromewebstore
access_token_lifetime: 1800s
create_credentials_file: false
export_environment_variables: false
- uses: hamzahamidi/publish-to-chrome-web-store@v1
with:
access-token: ${{ steps.auth.outputs.access_token }}
publisher-id: your-publisher-id
item-id: your-32-letter-extension-id
zip: extension.zip
Build the ZIP in a separate job without id-token: write, so build dependencies never run next to the token.
The threat model:
- Protected: the authority to update the publisher's items.
- Removed: the long-lived store credential in GitHub secrets.
- Still trusted: your release workflow, this action, GitHub, Google Cloud and the service account.
- Still broad: the service account can act on every item of the publisher, not one extension.
Most of this protection comes from the workflow, not the action. The provider decides which repository, ref and environment get a token, and the environment decides who approves the job. On a private repository, GitHub offers required reviewers only with GitHub Enterprise (GitHub's reference); without one, limit who can push release tags, as the README describes. The action also accepts an access token minted any other way, or a client ID, client secret and refresh token that it exchanges itself. It cannot tell how a credential was issued. The README has the setup in eight steps. My Google Cloud project for it has no billing account.
Why not call the API yourself
Uploading a ZIP and calling publish takes two requests. Doing it safely on every tag takes more, because the v2 API leaves several things to the caller.
publish takes no version. It submits whatever package was uploaded last, and fetchStatus does not say which package sits in the draft. If two runs upload to the same item, one can submit the other's package. The action reads the status again after submitting and fails if a different version went to review, but only one writer per item prevents it. The README makes that a requirement, and the concurrency group in the job above keeps two releases from uploading at once.
The store refuses many uploads, and says so late. A version in review blocks new packages, and so does an approved version waiting to be published. An upload whose version is not higher than the published one is rejected. The action reads the status first and stops before uploading, with a message that names the blocking version. A version that is already published or in review is skipped, so re-running a release is safe.
Uploads can finish later. The upload call may answer that processing is still in progress. The action polls the status every 10 seconds, up to 30 times, and fails without submitting if processing takes longer. It waits for an earlier run's upload instead of racing it. When the upload completes at once, it refuses to submit if the store read a different version from the package than the one sent.
dry-run: true reads the ZIP and the store status, reports what the run would do, and stops before uploading.
Optional controls
Partial rollout. deploy-percentage submits a version to a share of users, and a later run raises it. Google allows this only for items with over 10,000 seven-day active users, and the percentage can only go up. Raising without a package needs rollout-only: true. An empty zip, such as a mistyped step output, then fails the run instead of turning a release into a rollout change.
Signed uploads. Once an item is opted in to Verified CRX Uploads, the store accepts only a CRX signed with the developer's RSA key, so a leaked store token alone cannot upload a package the developer did not sign. A companion action, hamzahamidi/publish-to-chrome-web-store/sign@v1, signs the ZIP and makes no network request. The recommended workflow runs it in a separate job whose environment holds the key and never receives the store token. Keep the key in a secret of an environment limited to release tags: GitHub states that any user with write access to a repository can read all of its repository secrets. For the same ZIP and key, the output is byte-identical to Chrome's --pack-extension, which CI checks on every change.
Why trust it with the token
-
The source runs as is. About 900 lines of TypeScript importing only Node.js built-ins, with no bundle and no
dist/. Node 24 strips the types when it loads the files. -
Two hosts. Credentials go only to
chromewebstore.googleapis.com, and tooauth2.googleapis.comfor the refresh token route. Redirects are refused, and the README lists every request. - Clean logs. Credentials are masked before the first log line, and store responses are escaped so they cannot start a workflow command.
-
Checked releases. CI runs the tests on Linux, Windows and macOS, and separate jobs run the action from its
action.ymlagainst a mock store. Releases are immutable, andv1moves to the newest release through a workflow.
Limits
What the action does not do:
- It does not publish a staged version or cancel a pending review. Do both in the dashboard.
- It detects a competing upload only after the fact. Keep one writer per item.
- It refuses ZIP64 archives.
What Google imposes on any publishing tool:
- A Google Cloud project, since Google only issues store tokens to an OAuth client or a service account.
- One service account per publisher, able to manage every item of that publisher.
- No API call to create an item or change its visibility. After a visibility change, publish once by hand.
- Partial rollout only above 10,000 seven-day active users, and only upward.
- Replacing a lost signing key through support, which can take up to a week.
Use it
The Marketplace listing and the README have the setup. Start with dry-run: true. Yoke 0.1.7 was the first release submitted through it, on 26 September 2026.













