Skip to main content
Automate lint, build, and end-to-end (E2E) checks so extension changes stay releasable. Use continuous integration (CI) to run the same quality gates on every pull request and release branch.

CI capabilities

CapabilityWhat it gives you
Quality gatesEnforce lint, build, and test checks before merge
Browser-target confidenceBuild and validate multiple browser targets consistently
Reproducible environmentsLock toolchain versions across local and CI runs
Debuggable failuresPublish test reports and artifacts for investigation

Core pipeline stages

  1. Install dependencies
  2. Run lint/check commands
  3. Build browser targets
  4. Run automated tests (including Playwright where applicable)
  5. Upload artifacts/reports

Minimal GitHub Actions example

name: ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm extension build --browser=chromium

Build matrix example

strategy:
  matrix:
    browser: [chromium, firefox]
steps:
  - run: pnpm extension build --browser=${{ matrix.browser }}

Playwright in CI

  • Install browsers in CI job (playwright install or equivalent).
  • Keep retries slightly higher on CI than local.
  • Publish Playwright reports/artifacts for failed runs.
  • Use dist/extension-js/<browser>/ready.json as readiness contract before launching Playwright.
  • Avoid parsing human-readable logs in CI automation scripts.
  • Prefer the built-in wait commands: extension dev --wait --browser=<browser> (watch/dev) or extension start --wait --browser=<browser> (production/start).
  • For scripts or CI automation, prefer --wait-format=json.

Example readiness gate

pnpm extension start --wait --browser=chromium --wait-timeout=60000 --wait-format=json

Test handlers without a browser UI

You don’t always need Playwright. For event handlers — the toolbar action, keyboard commands — Extension.js can fire them directly against a dev session and you assert on the side effect. It’s deterministic and headless, so it’s a reliable gate.
- run: pnpm extension dev --browser=chromium --allow-control --wait --wait-format=json
- run: pnpm extension open action --browser=chromium --output json
- run: |
    pnpm extension storage get --key lastClickedAt --browser=chromium --output json \
      | node -e 'process.exit(JSON.parse(require("fs").readFileSync(0)).value?.lastClickedAt ? 0 : 1)'
The same approach replays keyboard commands (extension open command --name <cmd>). See Trigger actions and commands for the result payloads and the one caveat (replay carries no user gesture, so activeTab is not granted).

Docker and container CI

When CI runs inside Docker or a dev container, pass --host 0.0.0.0 so external processes can reach the dev server. If another process already occupies the default port, use --port 0 for automatic port assignment.
pnpm extension dev --host 0.0.0.0 --port 0 --no-browser
For slow or resource-constrained containers, increase browser transport timeouts via environment variables (see browser transport tuning variables).

Common pitfalls

  • CI scripts diverging from local scripts over time
  • Building only one browser target while shipping to multiple engines
  • Missing artifact upload for failed E2E jobs
  • Mixing unpinned Node/package manager versions across workflows
Your repository includes an E2E workflow example at:
  • .github/workflows/e2e.yml

Best practices

  • Keep CI commands close to local scripts to reduce drift.
  • Fail fast on lint/config errors before expensive browser jobs.
  • Cache dependencies and browser binaries when possible.
  • Version lock CI Node and package manager versions for reproducibility.

Next steps