Author, lint, run, build, and sign a Firefox / Chromium WebExtension using Mozilla's `web-ext` CLI v8. Covers `web-ext lint` (addons-linter wrapper, JSON output for CI), `web-ext run` (temporary install in firefox-desktop / firefox-android / chromium targets with hot-reload), `web-ext build` (deterministic zip), and `web-ext sign` (AMO submission API, listed vs unlisted channels, JWT credentials). Use when the extension targets Firefox (signing is mandatory for distribution) or when cross-browser test runs need a single CLI that drives both Firefox and Chromium against the same source tree.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This plugin was archived by the owner on Oct 11, 2026
Reason: Tile archived: Retired from testland/qa (merged into another skill or removed).
web-ext is Mozilla's reference CLI for WebExtension development.
Per the mozilla/web-ext README, it bundles five
subcommands: run, lint, sign, build, and docs. web-ext
v8 added a dump-config subcommand and reworked the signing flow
to use the AMO submission API by default (per the
web-ext command reference).
This skill wraps web-ext for test runs. Composes with:
manifest-v3-test-surface-reference
for the manifest fields web-ext lint enforces.mv2-to-mv3-migration-test-checklist
for migration-time lint rules.For Playwright-driven MV3 popup / content-script fixtures see
browser-extension-tests.
That skill is Chromium-only and assumes the extension is built;
web-ext is the lint+run+build+sign tool that produces the
artifact and validates it pre-flight on either browser.
.zip for upload to AMO or for CI
artefact archiving.chromium from one CLI for parity
smoke-tests (per --target chromium in we-cmd).Per we-readme, install globally or per-project:
# Global
npm install --global web-ext
# Per-project (preferred for CI determinism)
npm install --save-dev web-extMozilla recommends "the current LTS (long term support) version of NodeJS" (per we-readme) - verify your CI image matches before asserting reproducibility.
web-extmy-extension/
manifest.json
background.js
content.js
popup/
popup.html
popup.js
icons/
web-ext-config.cjs # optional, see belowweb-ext operates on the directory passed via --source-dir (or
-s); per we-cmd, --source-dir is a global option -
available to every subcommand - and --artifacts-dir (default
./web-ext-artifacts) controls where build / sign write zips.
web-ext-config.cjsA config file lets CI assert the same flags every run. Run
web-ext dump-config (new in v8 per we-cmd) to print the
resolved configuration as JSON for diffing.
// web-ext-config.cjs
module.exports = {
sourceDir: './dist',
artifactsDir: './build/artifacts',
run: {
firefox: 'nightly',
startUrl: ['about:debugging#/runtime/this-firefox'],
},
build: {
overwriteDest: true,
filename: 'my-extension-{version}.zip',
},
lint: {
warningsAsErrors: true,
output: 'json',
},
};web-ext lint wraps mozilla/addons-linter and
emits AMO-compatible warnings. Per we-cmd the flag list is:
| Flag | Effect |
|---|---|
--output / -o | json or text |
--metadata | output only metadata as JSON |
--pretty | format JSON output |
--self-hosted | declares self-hosting; disables AMO-related messages |
--boring | disables colored shell output |
--warnings-as-errors / -w | treat warnings as errors |
CI-friendly invocation:
web-ext lint \
--source-dir ./dist \
--output json \
--pretty \
--warnings-as-errors > lint-report.json--warnings-as-errors is the right CI default - by we-cmd this
escalates lint warnings (e.g., unsupported manifest fields, MV2-only
APIs flagged for MV3) into a non-zero exit code.
Per we-cmd, web-ext run builds the extension and installs it
into a fresh temporary Firefox profile, then watches the source
directory for changes and reloads on edit:
web-ext run \
--source-dir ./dist \
--firefox=firefox \
--start-url 'about:debugging#/runtime/this-firefox'Firefox alias values accepted by --firefox (per we-cmd):
firefox, beta, nightly, deved / firefoxdeveloperedition,
or a full path to a Firefox binary.
To pin a profile (e.g., to retain auth state between runs):
web-ext run \
--firefox-profile=qa-profile \
--profile-create-if-missing \
--keep-profile-changes--keep-profile-changes (per we-cmd) persists profile
modifications across runs - useful for snapshot-style tests.
Per we-cmd (the --target flag), web-ext run supports three
targets: firefox-desktop, firefox-android, and chromium. To
smoke-test cross-browser:
web-ext run \
--source-dir ./dist \
--target chromium \
--chromium-binary "$(which chromium)"This is the lowest-effort way to verify a Firefox-developed
extension at least loads on Chromium without a full Playwright
fixture. Deep Chromium-specific test surface lives in
chrome-extension-test-loader
and
playwright-extension-fixtures.
web-ext run \
--target firefox-android \
--android-device emulator-5554 \
--firefox-apk org.mozilla.firefoxPer we-cmd, the --adb-* family of flags wires adb for the
device handshake (--adb-bin, --adb-port, --adb-host,
--adb-device/--android-device, --adb-remove-old-artifacts).
web-ext build \
--source-dir ./dist \
--artifacts-dir ./build/artifacts \
--overwrite-dest \
--filename 'my-extension-{version}.zip'Per we-cmd, --filename / -n defaults to {name}-{version}.zip.
--overwrite-dest / -o is required when the same artefact path
already exists (e.g., re-running build in the same CI job).
Per we-cmd, web-ext sign v8 uses the AMO submission API by
default; --channel is required.
export WEB_EXT_API_KEY='user:12345:1'
export WEB_EXT_API_SECRET='abcdef...'
web-ext sign \
--source-dir ./dist \
--channel listed \
--amo-metadata ./amo-metadata.json \
--upload-source-code ./source.tar.gzChannel semantics (quoted from we-cmd):
"with
listedthe extension 'gets submitted for public listing'; withunlistedit 'gets submitted for signing for self-distribution.'"
| Flag | Effect |
|---|---|
--api-key (env $WEB_EXT_API_KEY) | JWT issuer for AMO API |
--api-secret (env $WEB_EXT_API_SECRET) | JWT secret |
--channel | required; listed or unlisted |
--amo-metadata | path to JSON with AMO listing metadata; required for first listed version |
--upload-source-code | path to source archive (v8 addition) |
--timeout | default 300000 ms |
--approval-timeout | default 900000 ms (v8 addition) |
--amo-base-url | default https://addons.mozilla.org/api/v5/ per we-cmd; an API base path, not a browsable page |
To submit updates, per we-cmd the manifest must include an
extension ID (browser_specific_settings.gecko.id for Firefox).
With --output json --pretty, web-ext lint emits an
addons-linter report:
{
"count": 2,
"summary": { "errors": 1, "notices": 0, "warnings": 1 },
"metadata": { "manifestVersion": 3, "type": "extension", ... },
"errors": [
{
"_type": "error",
"code": "MANIFEST_FIELD_INVALID",
"message": "...",
"description": "...",
"file": "manifest.json",
"line": 12,
"column": 5
}
],
"warnings": [...]
}(Field shape per we-readme's reference to mozilla/addons-linter;
exact fields stable across recent versions but spot-check the
addons-linter changelog before pinning a parser.)
Parse with jq:
jq '.summary.errors + .summary.warnings' lint-report.json
# > 0 means CI should fail under --warnings-as-errorsweb-ext build prints the artefact path to stdout and exits non-zero
on lint failure (build runs lint first). Capture via:
artefact=$(web-ext build -s ./dist -a ./out --overwrite-dest \
| grep 'Your web extension is ready' \
| sed -E 's/.*: (.+)$/\1/')web-ext sign exits zero on signed-and-downloaded; non-zero on AMO
rejection or timeout. Signed .xpi lands in --artifacts-dir.
GitHub Actions example, gated lint + build on every PR + sign on tag:
name: extension-ci
on:
pull_request:
push:
tags: ['v*']
jobs:
lint-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 'lts/*' }
- run: npm ci
- name: Lint
run: npx web-ext lint -s ./dist -o json --pretty -w > lint-report.json
- name: Build
run: npx web-ext build -s ./dist -a ./out --overwrite-dest
- uses: actions/upload-artifact@v4
with:
name: lint+xpi
path: |
lint-report.json
out/*.zip
sign:
if: startsWith(github.ref, 'refs/tags/v')
needs: lint-build
runs-on: ubuntu-latest
env:
WEB_EXT_API_KEY: ${{ secrets.AMO_JWT_ISSUER }}
WEB_EXT_API_SECRET: ${{ secrets.AMO_JWT_SECRET }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 'lts/*' }
- run: npm ci
- name: Sign (listed)
run: |
npx web-ext sign \
-s ./dist \
--channel listed \
--amo-metadata ./amo-metadata.json--warnings-as-errors in the lint step is what gates the PR - per
we-cmd it converts warnings into exit-1.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
web-ext sign without --channel | v8 requires --channel per we-cmd; command refuses to run | Always specify listed or unlisted |
Re-using the same profile (--firefox-profile) without --keep-profile-changes | Profile changes lost between runs; tests appear non-deterministic | Add --keep-profile-changes per we-cmd |
web-ext lint without -w in CI | Warnings silently pass; AMO submission still rejects | Use --warnings-as-errors / -w |
| Signing without an extension ID in manifest | Update submission fails per we-cmd | Add browser_specific_settings.gecko.id (see manifest-v3-test-surface-reference) |
Using --firefox-preview (removed in v8) | Flag removed; command errors | Pin web-ext version or migrate to the supported aliases |
Committing web-ext-artifacts/ | Repo bloats with binaries | Add to .gitignore; rely on CI artefact upload |
Calling web-ext run on production builds | Hot-reload modifies the profile; not a smoke-test surface | Use web-ext build + a separate Playwright fixture |
web-ext lint catches manifest issues and
AMO policy violations; it does not exercise the extension's
behaviour. Pair with playwright-extension-fixtures for
behavioural assertions.web-ext run --target chromium
installs the unpacked extension into a fresh Chromium profile,
but per we-cmd does not provide the AMO-style signing path -
Chrome Web Store has its own publishing flow.web-ext lint does not warn
when a manifest uses a key that's MV3-supported in Chrome but
not Firefox (or vice versa); cross-reference the
manifest-v3-test-surface-reference
matrix manually.--approval-timeout
defaults to 900000 ms (15 min) per we-cmd; AMO can take longer
in queue, requiring follow-up polling.--self-hosted (per
we-cmd) is for enterprise self-distribution; do not enable on
AMO-bound builds.web-ext README (install, basic usage) -
github.com/mozilla/web-ext.web-ext v8 command reference (every flag
on every subcommand) - we-cmd.addons-linter (what web-ext lint wraps) -
github.com/mozilla/addons-linter.manifest-v3-test-surface-reference.chrome-extension-test-loader,
playwright-extension-fixtures.