DEV Community

ExtensionBooster
ExtensionBooster

Posted on

Auto-Deploy Your Chrome Extension with GitHub Actions (2026) | ExtensionBooster

Auto-Deploy Your Chrome Extension with GitHub Actions (2026) | ExtensionBooster

Extension development has a lot of sharp edges. Here's my take on navigating this effectively in 2026.

The Key Points

  • 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

The Details

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.

This is a condensed version of a deeper guide. The full article covers additional context and examples.


Worth Bookmarking

If you're working with Chrome extensions, you'll eventually need tools for:

  • Generating icons at multiple sizes
  • Creating store screenshots that look professional
  • Converting MV2 extensions to MV3
  • Analyzing bundle size

ExtensionBooster has free versions of all of these. I keep it bookmarked for every extension project.


Top comments (0)