Signing and Publishing to AMO from CI
Release a Firefox extension from CI with web-ext sign — AMO API keys, listed versus unlisted channels, submitting source code for minified builds, and handling the asynchronous review.
Table of Contents
Every Firefox extension must be signed by Mozilla before release Firefox will install it, whether it is listed on addons.mozilla.org or distributed yourself. web-ext sign does the upload, validation and signing in one command, which makes it natural to run in CI — as long as the pipeline handles the two things that surprise people: listed submissions go into a human review queue and do not return a signed file immediately, and a minified or bundled build must be accompanied by its source code. This guide is part of CI and release automation.
Listed and unlisted channels
Step-by-step
1. Get AMO API credentials
On addons.mozilla.org, open Tools → Manage API Keys and generate a JWT issuer and secret. Store them as CI secrets, never in the repository — the rules for publishing credentials are in managing store credentials in CI secrets.
2. Pin the add-on id in the Firefox manifest
1{
2 "browser_specific_settings": {
3 "gecko": {
4 "id": "reader@example.com",
5 "strict_min_version": "121.0"
6 }
7 }
8}
Execution context: parsed by Firefox and by AMO at upload. Without a pinned id, AMO assigns one on first upload and every later upload must match it — pin it before the first submission. The per-target manifest is generated as in generating a manifest per browser target.
3. Lint before you sign
1npx web-ext lint --source-dir dist/firefox --warnings-as-errors
Execution context: the CI job, as a gate before signing. The same validator runs on AMO; catching its complaints locally saves a round trip and a rejected version number. --warnings-as-errors is strict but pays off — warnings about unsafe innerHTML or unknown manifest keys are exactly what a reviewer will ask about.
4. Sign an unlisted build for testing channels
1npx web-ext sign \
2 --source-dir dist/firefox \
3 --artifacts-dir artifacts \
4 --channel unlisted \
5 --api-key "$AMO_JWT_ISSUER" \
6 --api-secret "$AMO_JWT_SECRET"
7ls artifacts/*.xpi
Execution context: the CI job. The unlisted channel returns a signed .xpi within minutes, which makes it suitable for beta testers or internal distribution. The version number is consumed permanently even for unlisted uploads, so bump it for every signing run.
5. Submit a listed version with its source
For a public release, submit to the listed channel. If the build is bundled or minified, AMO requires the original source and exact build instructions.
1git archive --format=zip -o artifacts/source.zip HEAD
2
3npx web-ext sign \
4 --source-dir dist/firefox \
5 --artifacts-dir artifacts \
6 --channel listed \
7 --upload-source-code artifacts/source.zip \
8 --approval-timeout 0 \
9 --api-key "$AMO_JWT_ISSUER" \
10 --api-secret "$AMO_JWT_SECRET"
Execution context: the CI job. --approval-timeout 0 tells web-ext to submit and return rather than wait — listed review can take hours or days, and a CI job that waits that long will time out. git archive produces the source exactly as committed, without node_modules or build output, which is what reviewers expect.
6. Include build instructions a reviewer can follow
1<!-- BUILD.md, included in source.zip -->
2Requirements: Node 20.x, npm 10.x, Linux or macOS.
3
4 npm ci
5 npm run build:firefox
6
7The output in dist/firefox/ is byte-identical to the submitted package.
Execution context: a file at the repository root. Reviewers rebuild from these instructions and diff the result against your upload. A build that is not reproducible — timestamps in the bundle, unpinned dependencies — produces a diff and a delayed review; the reproducibility habits are in webpack configuration for MV3 extensions.
Keeping the version number honest
AMO consumes a version number the moment it is uploaded, on either channel, and never lets it be reused. That interacts badly with a CI pipeline that retries on failure: a job that uploads 2.4.1, fails in a later step, and is re-run will be rejected because 2.4.1 already exists.
The robust arrangement is to separate building a release candidate from uploading it, and to make the upload step idempotent by checking first:
1VERSION=$(jq -r .version dist/firefox/manifest.json)
2if curl -sf "https://addons.mozilla.org/api/v5/addons/addon/reader@example.com/versions/$VERSION/" \
3 -H "Authorization: JWT $(node scripts/amo-jwt.mjs)" >/dev/null; then
4 echo "version $VERSION already on AMO — skipping upload"
5 exit 0
6fi
Execution context: the CI job, before web-ext sign. The JWT is generated from the same issuer and secret; a 200 response means the version exists and the upload would fail anyway. Skipping cleanly lets a re-run complete the remaining steps instead of failing at the start.
For unlisted beta builds, a common pattern is to append a build number as a fourth version component — 2.4.1.37 — so every CI run has a unique version without touching the public version sequence.
What AMO reviewers look for in an automated submission
Automation changes nothing about what review checks, but it does change what a reviewer sees: a submission with no human-written notes, often at an odd hour, from a pipeline. A few habits make those submissions easier to approve.
Fill the reviewer notes. web-ext sign accepts release notes and reviewer notes through AMO’s API metadata; a one-paragraph note saying what changed and pointing at BUILD.md saves the reviewer from reconstructing it from a diff. Keep permissions stable between versions where you can — a version that adds a permission gets closer scrutiny, and bundling a permission change into an otherwise routine release delays the whole thing. And keep third-party code identifiable: vendored libraries in their own directory with their original filenames and licences are faster to review than the same code folded into a single bundle, because reviewers can match them against known releases.
None of this is specific to CI, but a pipeline is the natural place to enforce it — a step that fails when the manifest’s permission list differs from the previous release without an explicit override turns “we accidentally added a permission” from a review delay into a build failure.
Cross-browser variation
- Firefox (AMO):
web-ext signwith JWT credentials; listed and unlisted channels; source upload required for minified builds; version numbers are single-use. - Chrome Web Store: OAuth-based API with upload and publish endpoints and a staged rollout percentage, described in automating Chrome Web Store uploads with the API.
- Safari / App Store: App Store Connect submission of the containing app, typically through Fastlane with an App Store Connect API key.
- All three: submission is automatable; approval is not. Design the pipeline to end at “submitted” and treat approval as an external event.
Verification
- Run the unlisted signing path against a test add-on and confirm a signed
.xpiappears inartifacts/. - Install the signed file in a release Firefox build via
about:addons → Install Add-on From Fileand confirm it loads without a signature warning. - For a listed submission, confirm the version appears in the AMO developer hub with the source attached.
- Re-run the job for an already-uploaded version and confirm it skips rather than fails:
1./scripts/amo-publish.sh
2# version 2.4.1 already on AMO — skipping upload
Execution context: the CI job or your shell. A failure here means the idempotence check is missing and a retry will always break.
FAQ
Do self-distributed Firefox extensions need signing?
Yes. Release and Beta Firefox refuse unsigned extensions. Use the unlisted channel to get a signature without an AMO listing.
Why was my listed submission rejected for source code?
Either the source was missing, or the reviewer could not reproduce the build. Include BUILD.md, pin Node and dependency versions, and remove anything environment-dependent from the output.
Can CI wait for approval and then do something?
Not within a single job — listed review is too slow. Poll the AMO API from a scheduled job instead, and act when the version’s status changes to public.
Should the Chrome and Firefox releases share a version number?
Yes. One version across targets makes error reports, support conversations and changelogs line up. If Firefox needs a re-upload after a rejection, bump both — a Chrome release that skips a number costs nothing.
Related
- Testing extensions in Firefox with web-ext — the same tool for local testing.
- Publishing to Firefox add-ons and Safari — the manual submission process this automates.
- Managing store credentials in CI secrets — protecting the JWT issuer and secret.
- CI and release automation — the parent guide.