CI/CD for a Chrome Extension with GitHub Actions
Build, zip, and publish your MV3 extension to the Chrome Web Store automatically.
A Manifest V3 extension is a web bundle with a manifest.json. The build produces a zip, and the Chrome Web Store API uploads and publishes it. This recipe validates on PRs and publishes on a version tag.
What the pipeline does
- install deps with npm ci
- lint and test
- build the extension bundle
- zip the dist directory
- upload and publish to the Chrome Web Store on a tag
The workflow
The build emits dist/. On a version tag, the chrome-webstore-upload-cli uploads the zip and publishes it using OAuth client credentials and a refresh token stored as secrets.
name: CI
on:
push:
branches: [main]
tags: ['v*']
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint --if-present
- run: npm test --if-present
- run: npm run build
- run: cd dist && zip -r ../extension.zip .
- if: startsWith(github.ref, 'refs/tags/v')
run: |
npx chrome-webstore-upload-cli@3 upload --source extension.zip --auto-publish
env:
EXTENSION_ID: ${{ secrets.CWS_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CWS_REFRESH_TOKEN }}Caching and speed
cache: npm handles installs. Extension bundles are small, so builds are fast and CI is dominated by install; cheaper managed runners such as Latchkey keep frequent PR runs inexpensive and auto-retry transient failures.
Deploying
The Web Store review process runs after the upload, so a publish is a submission, not an instant rollout. Bump the version in manifest.json on every release since the store rejects duplicate versions. Store the OAuth credentials as repository secrets, never in the repo.
Key takeaways
- Build, zip dist/, then upload to the Chrome Web Store.
- Bump manifest.json version on every release.
- Store Web Store OAuth credentials as repo secrets.