Auto-Deploy Your Chrome Extension with GitHub Actions (2026) | ExtensionBooster
Automating Chrome extension deployment saves hours of manual work. Here's how to set it up properly.
The highlights
- 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
Here's the thing
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. 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. 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. 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. 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. 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. 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. ci installs exactly what your lockfile pins, so the build that ships is the build you tested.
💡 Quick tip: Start with a simple CI pipeline — you can always add complexity later.
If you build Chrome extensions
You'll eventually need tools for icons, screenshots, MV2→MV3 conversion, and bundle analysis.
ExtensionBooster has free versions of all of these — I keep it bookmarked for every extension project.
Top comments (0)