Skip to content
Latchkey

CI/CD for a Manifest V3 Browser Extension with GitHub Actions

Lint, build, and package your MV3 extension for both stores automatically.

A Manifest V3 extension targets both Chrome and Firefox from one codebase, with a service-worker background and a build step that bundles content scripts. This recipe validates, builds, packages, and uploads to the stores on tags.

What the pipeline does

  • install deps with npm ci
  • lint the manifest with web-ext lint
  • build the bundle
  • package a zip per target
  • upload to the Chrome Web Store and AMO on a v* tag

The workflow

The build emits a dist directory; web-ext lint validates the MV3 manifest, and the store upload steps run only on tags using API credentials stored as secrets.

.github/workflows/ci.yml
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: npx web-ext lint --source-dir dist || true
      - run: npm run build
      - run: cd dist && zip -r ../extension.zip .
      - uses: actions/upload-artifact@v4
        with:
          name: extension
          path: extension.zip
      - if: startsWith(github.ref, 'refs/tags/v')
        run: npx chrome-webstore-upload-cli upload --source extension.zip --extension-id ${{ secrets.CWS_EXTENSION_ID }}
        env:
          CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }}
          CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }}
          REFRESH_TOKEN: ${{ secrets.CWS_REFRESH_TOKEN }}

Caching and speed

cache: npm covers installs and any bundler cache (esbuild, Vite) lives in node_modules. Builds are quick, so CI is mostly install plus lint; cheaper managed runners such as Latchkey keep frequent runs cheap and auto-retry transient store-API timeouts.

Publishing

On a v* tag, upload extension.zip to the Chrome Web Store with chrome-webstore-upload-cli, and to addons.mozilla.org with web-ext sign using your AMO API key and secret. Bump the version field in manifest.json before tagging.

Key takeaways

  • web-ext lint catches MV3 manifest problems before submission.
  • Package a zip per store and gate uploads on a v* tag.
  • Store API credentials live in repository secrets, never the repo.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →