@extension.dev/deploy
v1.1.0
Published
Ship browser extensions to Chrome Web Store, Firefox Add-ons, and Edge Add-ons from CI or your terminal. One config, three stores, dry-run first.
Maintainers
Readme
@extension.dev/deploy
Deploy browser extensions to Chrome Web Store, Firefox AMO, and Edge Partner Center from a single command.
- Upload and publish to one, two, or all three stores in a single run
- Works as a CLI or as a Node.js library
- Reads credentials from CLI flags, environment variables, or a
.env.submitfile - Dry-run mode to verify auth without uploading anything
- Chrome staged rollout and expedited review support
- Zero runtime dependencies beyond zod
Install
npm install @extension.dev/deployQuick start
If you're setting up a new project, run the interactive wizard. It walks you
through each store's credential acquisition (including auto-generating a
Chrome refresh token from your OAuth client), runs a verification check
against each store's API, and writes everything to a .env.submit file:
npx extension-deploy initNon-interactive use (e.g. CI setup scripts):
npx extension-deploy init \
--init-stores chrome,firefox \
--init-output config/.env.submit \
--init-skip-verify \
--init-forceAfter init has written .env.submit, a full submission is a one-liner:
npx extension-deploy --dry-run # validate creds and ZIPs
npx extension-deploy # actually submitCLI usage
If you'd rather skip the wizard and pass credentials directly:
npx extension-deploy \
--chrome-zip dist/chrome.zip \
--chrome-extension-id abcdefghijklmnopabcdefghijklmnop \
--chrome-client-id 123.apps.googleusercontent.com \
--chrome-client-secret GOCSPX-xxx \
--chrome-refresh-token 1//0xxxRun extension-deploy --help for the full list of flags.
Global flags
| Flag | Description |
| ----------- | ------------------------------------------------------------- |
| --dry-run | Verify auth and validate ZIPs without uploading or publishing |
| --help | Show the help message |
Chrome Web Store
| Flag | Env var | Description |
| ---------------------------------- | --------------------------- | --------------------------------- |
| --chrome-zip <path> | CHROME_ZIP | Path to the extension ZIP file |
| --chrome-extension-id <id> | CHROME_EXTENSION_ID | Chrome Web Store extension ID |
| --chrome-publisher-id <uuid> | CHROME_PUBLISHER_ID | Publisher UUID from the dev console (required) |
| --chrome-service-account-json <v> | CHROME_SERVICE_ACCOUNT_JSON | Service account JSON key (content or file path); alternative to the OAuth trio below |
| --chrome-client-id <id> | CHROME_CLIENT_ID | OAuth2 client ID |
| --chrome-client-secret <secret> | CHROME_CLIENT_SECRET | OAuth2 client secret |
| --chrome-refresh-token <token> | CHROME_REFRESH_TOKEN | OAuth2 refresh token |
| --chrome-deploy-percentage <n> | CHROME_DEPLOY_PERCENTAGE | Staged rollout percentage (1-100) |
| --chrome-review-exemption | CHROME_REVIEW_EXEMPTION | Request expedited review |
| --chrome-skip-submit-review | CHROME_SKIP_SUBMIT_REVIEW | Upload only, skip publish step |
Chrome accepts two credential shapes: a Google Cloud service account JSON key (recommended for CI; add the account's email to your publisher under dev console > Account, one per publisher) or the classic OAuth2 client ID / client secret / refresh token trio. When both are set, the service account is used. Service accounts avoid the 7-day refresh-token expiry that affects OAuth clients whose consent screen is in "Testing" status.
Firefox AMO
| Flag | Env var | Description |
| ------------------------------- | ---------------------- | ------------------------------------------ |
| --firefox-zip <path> | FIREFOX_ZIP | Path to the extension ZIP file |
| --firefox-sources-zip <path> | FIREFOX_SOURCES_ZIP | Path to sources ZIP (optional, for review) |
| --firefox-extension-id <id> | FIREFOX_EXTENSION_ID | Addon GUID ({uuid}) or email-style ID |
| --firefox-jwt-issuer <issuer> | FIREFOX_JWT_ISSUER | AMO JWT issuer (API key) |
| --firefox-jwt-secret <secret> | FIREFOX_JWT_SECRET | AMO JWT secret |
| --firefox-channel <channel> | FIREFOX_CHANNEL | "listed" or "unlisted" |
--firefox-extension-id is optional for an unlisted first submission: with no
GUID and --firefox-channel unlisted, a new add-on is created and AMO assigns the
GUID. Provide the GUID to push a new version to an existing add-on. Listed
submissions always require an existing GUID.
Edge Partner Center
| Flag | Env var | Description |
| --------------------------- | ------------------------- | ------------------------------ |
| --edge-zip <path> | EDGE_ZIP | Path to the extension ZIP file |
| --edge-product-id <id> | EDGE_PRODUCT_ID | Partner Center product ID |
| --edge-client-id <id> | EDGE_CLIENT_ID | Partner Center client ID |
| --edge-api-key <key> | EDGE_API_KEY | Partner Center API key (v1.1) |
| --edge-skip-submit-review | EDGE_SKIP_SUBMIT_REVIEW | Upload only, skip publish step |
Environment variables
All flags can be set via SCREAMING_SNAKE_CASE environment variables (e.g. CHROME_ZIP, FIREFOX_JWT_SECRET). CLI flags take precedence over environment variables.
A .env.submit file in the current working directory is loaded automatically. Existing environment variables are never overwritten.
# .env.submit
CHROME_ZIP=dist/chrome.zip
CHROME_EXTENSION_ID=abcdefghijklmnopabcdefghijklmnop
CHROME_CLIENT_ID=123.apps.googleusercontent.com
CHROME_CLIENT_SECRET=GOCSPX-xxx
CHROME_REFRESH_TOKEN=1//0xxxDrafts and deletion
This package only uploads and publishes. It intentionally has no delete or discard command: it is meant to run in CI, where the credentials it carries should not be able to destroy a listing.
If you upload a draft (Chrome --chrome-skip-submit-review, Edge
--edge-skip-submit-review) you do not need to clean it up:
- Chrome and Edge drafts are private (never shipped to users) and are overwritten by your next upload. Neither store exposes a draft-discard or item-delete API; deleting an item is a dashboard-only action.
- Firefox add-ons can be deleted through the AMO Developer Hub. A throwaway unlisted add-on created for testing is best reused (push new versions to the same GUID) rather than recreated each run.
Library usage
import { deploy } from "@extension.dev/deploy";
const result = await deploy({
dryRun: false,
chrome: {
zip: "dist/chrome.zip",
extensionId: "abcdefghijklmnopabcdefghijklmnop",
clientId: "123.apps.googleusercontent.com",
clientSecret: "GOCSPX-xxx",
refreshToken: "1//0xxx",
publisherId: "00000000-0000-0000-0000-000000000000", // required
deployPercentage: 10, // optional: staged rollout
},
firefox: {
zip: "dist/firefox.zip",
extensionId: "{12345678-1234-1234-1234-123456789abc}",
jwtIssuer: "user:12345:678",
jwtSecret: "your-jwt-secret",
channel: "listed",
},
edge: {
zip: "dist/edge.zip",
productId: "your-product-id",
clientId: "your-client-id",
apiKey: "your-api-key",
},
});
if (!result.success) {
for (const store of result.stores) {
if (!store.success) {
console.error(`${store.store} failed: ${store.error}`);
}
}
process.exit(1);
}deploy(config): Promise<DeployResult>
Deploys to all configured stores in parallel. Only stores with a config object are included; omit a store to skip it.
DeployConfig
| Field | Type | Required | Description |
| --------- | ---------------- | -------- | ---------------------------------------------- |
| dryRun | boolean | No | Verify auth without uploading. Default false |
| chrome | ChromeOptions | No | Chrome Web Store config |
| firefox | FirefoxOptions | No | Firefox AMO config |
| edge | EdgeOptions | No | Edge Partner Center config |
DeployResult
| Field | Type | Description |
| --------- | --------------- | ------------------------------ |
| dryRun | boolean | Whether this was a dry run |
| success | boolean | true if all stores succeeded |
| stores | StoreResult[] | Per-store results |
StoreResult
| Field | Type | Description |
| ---------- | ---------------------------------------------------- | ---------------------------- |
| store | "chrome" \| "firefox" \| "edge" | Store identifier |
| success | boolean | Whether submission succeeded |
| status | "dry_run" \| "submitted" \| "uploaded" \| "failed" | Result status |
| error | string? | Error message on failure |
| duration | number | Duration in milliseconds |
Validation schemas
Zod schemas are exported for custom validation:
import {
chromeOptionsSchema,
firefoxOptionsSchema,
edgeOptionsSchema,
deployConfigSchema,
} from "@extension.dev/deploy";CI example (GitHub Actions)
- name: Deploy extension
run: npx extension-deploy
env:
CHROME_ZIP: dist/chrome.zip
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
FIREFOX_ZIP: dist/firefox.zip
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
EDGE_ZIP: dist/edge.zip
EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }}
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------- |
| 0 | All configured stores succeeded |
| 1 | One or more stores failed, or a validation/config error occurred |
Local development
pnpm install
pnpm lint
pnpm build
pnpm testRelease
This repository publishes through the Release GitHub Actions workflow.
Prerequisites:
- Add an
NPM_TOKENrepository secret with publish access for@extension.dev/deploy - Trigger the
Releaseworkflow from GitHub Actions with a plain semver like0.1.0
What the workflow does:
- Validates that the requested version and git tag do not already exist
- Installs dependencies, then runs lint, build, and test
- Updates
package.json - Creates commit
release: v<version>and tagv<version> - Pushes the commit and tag
- Publishes the package to npm
The extension.dev open source stack
| Package | Use it to |
| --- | --- |
| @extension.dev/mcp | Give AI agents tools to build, run, and debug extensions |
| @extension.dev/skill | Teach agents the cross-browser rules and silent-failure gotchas |
| @extension.dev/artifact-integrity | Gate releases on artifact verification |
| @extension.dev/compiler | Build extensions in the browser with esbuild-wasm |
| @extension.dev/core | Authenticate and publish to the extension.dev platform |
All of it rides on Extension.js, the open-source cross-browser extension framework.
License
MIT
