Skip the trial and error — here's the definitive guide to Chrome extension development in 2026.
Auto-Deploy Your Chrome Extension with GitHub Actions (2026) | ExtensionBooster Publishing a new version of a Chrome extension by hand is a small ritual that gets old fast. Bump the version, run the build, zip the right folder (not the parent folder, not node_modules ), open the Developer Dashboard, drag the file in, click through the warnings, hit submit, and hope you did not forget a step. Do that twice a week across Chrome, Firefox, and Edge and you have invented a part-time job nobody asked for. This guide replaces that ritual with a pipeline. You push a Git tag, and GitHub Actions builds your extension, packages it, and uploads it to the Chrome Web Store on its own. By the end you will have a workflow file you can drop into any extension repo, the exact credentials it needs, and a clear path to add Firefox and Edge to the same job. TL;DR Auto-deploy works by giving a GitHub Actions runner four secrets, then letting it call the Chrome Web Store API to upload and publish your packaged extension. The genuinely fiddly part is one-time credential setup (a Google Cloud OAuth client plus a refresh token), not the workflow YAML. Trigger on a version tag ( v1. 0 ) so a publish only ever happens on a deliberate release, never on every commit. json version must be higher than the live version or the upload is rejected. Bump it before you tag. Going multi-browser is one extra action: PlasmoHQ/bpp publishes to Chrome, Firefox, and Edge from a single step. How auto-deploy actually works There is no magic in the pipeline, just a chain of steps a human used to do. A GitHub Actions runner is a fresh Linux machine that spins up when an event fires. The workflow tells it to: Check out your source code. Install dependencies and run your build. Zip the built output into the exact package the store expects. Authenticate to the Chrome Web Store API and upload the zip. Tell the API to publish (or leave it in draft for manual review). The only thing the store needs to trust the runner is a set of OAuth credentials scoped to your developer account. Once those live in GitHub as encrypted secrets, the runner can do everything you used to do in the dashboard, minus the dragging and clicking. If you have never published this extension manually, do that first. The smoothest path is to ship version one through the build-and-publish walkthrough , confirm it is live, and then automate every release after that. What you need before you automate Gather these once and the rest is copy-paste: Requirement Where it comes from A published item ID The 32-character ID in your extension’s Developer Dashboard URL A reproducible build An npm run build (or pnpm build ) that outputs a clean folder with a valid manifest. json A Google Cloud project console. com , used only to mint API credentials OAuth client ID + secret Created inside that project with the Chrome Web Store API enabled A refresh token Generated once from the client ID and secret The first two are about your project. The last three are about giving CI permission to publish on your behalf. That is the work in this guide. Step 1: Make your build produce a clean ZIP The store does not accept your repo, it accepts a zip whose root contains manifest. The most common deploy failure is zipping one level too high so the manifest ends up inside a subfolder. If you use a framework like WXT or Plasmo, the build already emits a store-ready folder ( . output/chrome-mv3 or build/chrome-mv3-prod ). For a vanilla project, make sure your build copies everything Chrome needs into a single dist/ directory. Test the packaging locally before you ever touch CI: npm run build cd dist && zip -r . zip | head # manifest. json must be at the top level If manifest. json shows up at the root of that listing, your pipeline will package correctly. If it shows up as dist/manifest. json , fix the zip command (zip the contents of dist , not the dist folder itself). Step 2: Get Chrome Web Store API credentials This is the part that scares people away, so here it is in plain steps. You are creating an OAuth client and turning it into a long-lived refresh token. Enable the API and create a client: Open Google Cloud Console and create a new project (name it something like “extension-publish”). In APIs & Services , search for and enable the Chrome Web Store API . Go to OAuth consent screen , choose External , fill the required fields, and add your own Google account as a test user . You do not need to submit for verification. Under Credentials , create an OAuth client ID of type Desktop app . Copy the Client ID and Client secret . Mint a refresh token: The cleanest way is a small helper that walks you through the OAuth handshake and prints the token. From any terminal: npx chrome-webstore-upload-keys It asks for your client ID and secret, opens a consent URL, and after you approve and paste the code back, it prints a refresh token . That token is what lets CI authenticate without a human present. You now hold four values: extension ID , client ID , client secret , and refresh token . Treat the last three like passwords, because that is exactly what they are. Step 3: Store the secrets in GitHub Never put any of these in the workflow file or your repo. Add them as encrypted repository secrets: In your GitHub repo, open Settings → Secrets and variables → Actions . Click New repository secret and add each of the following: Secret name Value CHROME_EXTENSION_ID Your 32-character item ID CHROME_CLIENT_ID OAuth client ID CHROME_CLIENT_SECRET OAuth client secret CHROME_REFRESH_TOKEN The refresh token from Step 2 GitHub encrypts these at rest and masks them in logs. The workflow reads them through the secrets context, so they never appear in plaintext anywhere you can see. Step 4: Write the GitHub Actions workflow Create . github/workflows/publish. This workflow fires only when you push a tag that starts with v , builds the extension, packages it, and publishes to the Chrome Web Store: name : Publish extension on : push : tags : - "v*" # fires on v1. permissions : contents : read # least privilege: the job only reads the repo jobs : publish : runs-on : ubuntu-latest steps : - name : Check out source uses : actions/checkout@v4 - name : Set up Node uses : actions/setup-node@v4 with : node-version : 22 cache : npm - name : Install dependencies run : npm ci - name : Build the extension run : npm run build # must output dist/ with a valid manifest. json - name : Package the build run : cd dist && zip -r . - name : Upload and publish to the Chrome Web Store uses : mnao305/chrome-extension-upload@v5 with : file-path : extension. zip extension-id : ${{ secrets. CHROME_EXTENSION_ID }} client-id : ${{ secrets. CHROME_CLIENT_ID }} client-secret : ${{ secrets. CHROME_CLIENT_SECRET }} refresh-token : ${{ secrets. CHROME_REFRESH_TOKEN }} publish : true A few decisions in here are worth understanding rather than copying blindly: The tag trigger. Publishing on every push to main is how you accidentally ship a half-finished feature to thousands of users. A tag is an explicit “this one is a release” signal. npm ci , not npm install . ci installs exactly what your lockfile pins, so the build that ships is the build you tested. Set this to false if you would rather have CI upload a draft and then click publish yourself in the dashboard. That is a sensible halfway step while you build trust in the pipeline. If you want CI to also run your test suite before it publishes, add a step that runs your end-to-end tests or Puppeteer automation before the upload step. A failing test then blocks the release automatically. Step 5: Cut a release and watch it ship The store rejects any upload whose version is not strictly higher than the live version, so the release flow is: bump, commit, tag, push. Bump the version in manifest. json if you keep them in sync) # 2. Commit the bump git commit -am "release: v1. Tag it and push the tag git tag v1. 0 git push origin main --tags Pushing the tag fires the workflow. Open the Actions tab in GitHub and watch the steps run. A green check means the new version is in the Chrome Web Store review queue. From here, the only thing standing between your code and your users is Google’s review time, which is out of your hands but predictable enough to plan around. To avoid hand-editing two version numbers, keep manifest. json reading its version from package. json in your build, or add a one-line script that syncs them. Forgetting the bump is the single most common reason a first auto-deploy fails with a confusing “version already exists” error. Going multi-browser: Firefox and Edge in the same pipeline Once Chrome is automated, Firefox and Edge are a small addition rather than a second project. The cleanest option is the Browser Platform Publisher action, which targets all three stores from one step: - name : Publish to Chrome, Firefox, and Edge uses : PlasmoHQ/bpp@v3 with : keys : ${{ secrets. BPP_KEYS }} chrome-file : build/chrome-mv3-prod. zip firefox-file : build/firefox-mv2-prod. zip edge-file : build/edge-mv3-prod. zip Here BPP_KEYS is a single JSON secret holding the credentials for each store you publish to: { "chrome" : { "clientId" : ". " , "clientSecret" : ". " , "refreshToken" : ". " }, "firefox" : { "apiKey" : ". " }, "edge" : { "productId" : ". " } } Each store hands out its own credentials. Firefox issues a JWT API key and secret from your Add-on Developer Hub under API Keys . Edge issues credentials from the Microsoft Partner Center Add-ons API settings. Edge is the most particular of the three about its API fields, so when something refuses to authenticate, it is almost always Edge, and its current API docs are the fastest way to confirm the exact field names. Building one source into three browser-specific packages is its own topic. If you are not there yet, our cross-browser frameworks comparison covers which toolchains emit Chrome, Firefox, and Edge builds from a single codebase. Security: publish credentials are production secrets A refresh token that can publish to your store listing is as sensitive as a deploy key. Treat it accordingly: Scope the trigger tightly
The Key Takeaway
This is the kind of content that separates quick learners from production-ready developers. Whether you're building your first extension or optimizing an existing one, these patterns save hours of frustration.
Level Up Your Extension Development
Looking for the complete toolkit?
🔥 ExtensionBooster — Free developer tools for Chrome extension builders:
• Extension Icons Generator — Resize your icon to every required size in seconds
• Screenshot Maker — Produce polished store screenshots that convert
• MV2 to MV3 Converter — Modernize your legacy extension with one click
• Bundle Analyzer — Find and eliminate the bloat slowing down your extension
These are the tools working developers use daily to ship faster and smarter.
Why This Matters
Chrome extensions are experiencing a renaissance in 2026. With Manifest V3 now mandatory and the Chrome Web Store tightening review standards, the bar for quality has never been higher.
Building on proven patterns means:
- Faster development cycles
- Cleaner code that passes store review
- Better user experience and retention
- Less time debugging, more time shipping
Start Building
The gap between a good extension and a great one is often just knowing the right approach. Bookmark this, share it with fellow developers, and when you're ready to level up your workflow, check out the full toolkit at ExtensionBooster.
Happy building! 🚀
Top comments (0)