No results for

TutorialsLast Updated May 21, 20257 min read

How to continuously deploy a Chrome Extension

Ricardo N Feliciano

Community Engineer, CircleCI

Developer B sits at a desk working on an intermediate-level project.

Note from the publisher: You have managed to find some of our old content and it may be outdated and/or incorrect. Try searching in our docs or on the blog for current information.


Google Chrome remains the most popular browser in the world, and its ecosystem of extensions is thriving more than ever. From simple utilities like tab counters to powerful tools for developers and businesses, Chrome Extensions provide opportunities to enhance productivity and tailor user experience. As developers iterate on features, squash bugs, and respond to user feedback, the need for a seamless and automated deployment process becomes clear.

For this guide, I built and published a simple Chrome Extension called Tab Counter. It displays the number of open tabs directly on the extension icon. Lightweight and practical, this minimalist extension serves as a real-world example that’s ideal for learning CI/CD pipelines. You can find it live on the Chrome Web Store, and feel free to substitute it with your own project as you follow along. The source code is also available on GitHub for reference.

We’ll walk through how to build a robust continuous deployment pipeline for a Chrome Extension using CircleCI. Whether you’re building for yourself, an open-source tool, or a team, the ability to push updates reliably without manual overhead is essential and fully achievable using CircleCI, the Chrome Web Store API, and a few well-placed shell commands.

Why automate Chrome extension deployment?

Manually uploading a new zip file to the Chrome Web Store each time you fix a bug or make an improvement gets old very quickly, especially when you’re deploying often. Automation ensures every update is delivered reliably, securely, and consistently. It reduces the potential for human error, saves time, and lets you focus on building rather than deploying. As your workflow matures, having a CI/CD process in place becomes not just a nice-to-have, but an expectation for scalable software development.

Prerequisites

Before we start, make sure you have the following:

Set up Google OAuth credentials

To publish your extension automatically, you’ll need to set up Google OAuth credentials. This allows CircleCI to authenticate with the Chrome Web Store API and perform actions like uploading and publishing your extension.

To begin, visit the Google Cloud Console and create a new project. Once created, make sure it’s selected in the top project dropdown.

Create project

Enable the Chrome Web Store API

In the API Library, search for and enable the Chrome Web Store API. This gives your project access to the publishing endpoints needed to deploy extensions.

enable api

Generate OAuth credentials

Go to APIs & Services, then Credentials. Click Create Credentials → OAuth client ID.

Create credentials

You may need to configure the OAuth consent screen first. Choose External for the user type, and fill in the required fields like application name and support email. You can skip Scopes and Test Users for now.

When creating the OAuth client ID, select the application type, choose Desktop App, even though there’s a “Chrome Extension” option available in the dropdown. This might seem like the right choice, but it’s not intended for automated tools like CI/CD pipelines. “Chrome Extension” is meant for OAuth flows directly from within extensions, while “Desktop App” is appropriate for tools like CircleCI that perform external API requests.

Give your client a name like Chrome CI/CD App.

Create client ID

Once created, copy your Client ID and Client Secret — you’ll need these for authentication.

Authorize and get refresh token

Before continuing, you may encounter an error like:

“Access blocked: [App Name] has not completed the Google verification process.”

This happens because your OAuth client is still in testing mode. Google restricts apps in testing mode to authorised test users only.

To resolve this, go to the Google Cloud Console, open the OAuth consent screen under APIs & Services -> Audience, and scroll to the Test users section. Add the Google account you’ll use for CircleCI deployment and save your changes. This grants the necessary access to complete the OAuth flow while the app remains in test mode.

Now, to authorize the app and get a token, visit the URL from the next code block in your browser. Replace $CLIENT_ID with your actual value:

https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=$CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob
Copy to clipboard

Approve the permissions. You’ll receive a temporary code. This is your authorization code. Exchange that for a refresh_token using this curl command:

curl "https://accounts.google.com/o/oauth2/token" \
 -d "client_id=$CLIENT_ID" \
 -d "client_secret=$CLIENT_SECRET" \
 -d "code=$CODE" \
 -d "grant_type=authorization_code" \
 -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" | jq '.refresh_token'
Copy to clipboard

Copy and store the refresh_token securely. This token can be used to obtain short-lived access tokens without needing to log in again.

Retrieve Extension App ID

To get your Chrome Extension’s App ID, open its Chrome Web Store page and copy the last part of the URL. That ID uniquely identifies your extension when uploading and publishing from the API.

Build your CI pipeline with CircleCI

To automate the deployment process, set up a CircleCI pipeline that builds and deploys your Chrome Extension.

Create a new file in your repository called .circleci/config.yml. Paste this content into it:

version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Install Dependencies
          command: |
            sudo apt-get update
            sudo apt-get install -y curl jq zip
      - run:
          name: Package Extension
          command: zip -r tab-counter.zip . -x '*.git*' -x '*.circleci*' -x '*README.md' -x '*test/*'
      - run:
          name: Upload & Publish to Chrome Web Store
          command: |
            ACCESS_TOKEN=$(curl -s -X POST https://accounts.google.com/o/oauth2/token \
              -d client_id=${CLIENT_ID} \
              -d client_secret=${CLIENT_SECRET} \
              -d refresh_token=${REFRESH_TOKEN} \
              -d grant_type=refresh_token \
              -d redirect_uri=urn:ietf:wg:oauth:2.0:oob | jq -r .access_token)

            UPLOAD_RESPONSE=$(curl -s -w "%{http_code}" -o upload_output.json \
              -H "Authorization: Bearer ${ACCESS_TOKEN}" \
              -H "x-goog-api-version: 2" \
              -X PUT -T tab-counter.zip \
              "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}")

            if [[ "$UPLOAD_RESPONSE" -ne 200 ]]; then
              echo "❌ Upload failed:"
              cat upload_output.json
              exit 1
            fi

            PUBLISH_RESPONSE=$(curl -s -w "%{http_code}" -o publish_output.json \
              -H "Authorization: Bearer ${ACCESS_TOKEN}" \
              -H "x-goog-api-version: 2" \
              -H "Content-Length: 0" \
              -X POST \
              "https://www.googleapis.com/chromewebstore/v1.1/items/${APP_ID}/publish")

            if [[ "$PUBLISH_RESPONSE" -ne 200 ]]; then
              echo "❌ Publish failed:"
              cat publish_output.json
              exit 1
            fi

workflows:
  deploy:
    jobs:
      - build
Copy to clipboard

This is a working example that you can modify for your Chrome extension. This configuration checks out your code, installs tools like curl, jq, and zip, bundles your extension into a zip archive while ignoring Git-related files, and then publishes it to the Chrome Web Store.

It handles authentication by exchanging a stored refresh_token for a short-lived access token, which is then used to authorize the upload and release. Once everything succeeds, your extension goes live, no manual steps needed

Save this file, commit your changes and push to your GitHub repository.

Log in to CircleCI, select your project, enter the branch housing your .circleci/config.yml file, and click Set Up Project.

CircleCI config

CircleCI will automatically detect the configuration file and start building your project. This would failed to deploy at the moment, as the required environment variables are not set yet.

CircleCI build

Set up environment variables

In CircleCI, go to your project’s Project Settings → Environment Variables and add the following:

  • CLIENT_ID
  • CLIENT_SECRET
  • REFRESH_TOKEN
  • APP_ID

The APP_ID is your extension’s unique identifier from the Chrome Web Store URL. Defining it as an environment variable makes your configuration cleaner and easier to manage.

Rerun the build, and it should complete successfully. You can check the logs to see the upload and publish steps.

CircleCI build success

Remember to always update your version number with every new deployment to avoid errors from the Chrome Web Store API.

Advanced practices

Handling development vs. production builds

During development, it’s common to load your extension in unpacked mode. But when both development and production versions are loaded in the same browser, you might end up with duplicate icons and versions, making it hard to distinguish them.

To avoid this, configure your manifest.json file with placeholder or dev-specific values like a version of 9.9.9.9 or an alternate icon:

{
  "manifest_version": 3,
  "name": "Tab Counter",
  "version": "9.9.9.9",
  "action": {
    "default_icon": "dev-icon.png"
  }
}
Copy to clipboard

Then, just before building the extension in your CI pipeline, automatically replace those values using a short Bash script that increments the patch version and sets the production icon:

- run:
    name: Set Production Icon & Version
    command: |
      current_version=$(jq -r '.version' manifest.json)
      major=$(echo "$current_version" | cut -d. -f1)
      minor=$(echo "$current_version" | cut -d. -f2)
      patch=$(echo "$current_version" | cut -d. -f3)
      new_version="$major.$minor.$((patch + 1))"
      jq --arg v "$new_version" '.version = $v | .action.default_icon = "icon.png"' manifest.json | sponge manifest.json
Copy to clipboard

This keeps your versioning consistent and avoids accidental reuse of an existing published version.

Use Git tags to control deployments

Rather than deploy on every commit to main, you can require a semantic version Git tag like v1.2.3 to trigger a release. This adds control and avoids unintended deployments.

Here’s how to configure that:

workflows:
  release:
    jobs:
      - build:
          filters:
            tags:
              only: /^v.*/
      - publish:
          requires:
            - build
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /^v.*/
Copy to clipboard

This workflow defines two jobs, build and publish that only run when you push a Git tag starting with v, like v1.0.2. The build job handles the packaging, while publish runs afterward to deploy the extension. Note that this pattern assumes you’ve split your pipeline into two separate jobs. If you’re keeping everything in one build job, you don’t need the publish job block.

Use workspaces for multi-step workflows

If your packaging and publishing happen in separate jobs, use persist_to_workspace and attach_workspace to share the zipped extension between them.

Complete the CircleCI configuration example

Here’s what a full production-ready .circleci/config.yml file might look like:

version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Install Dependencies
          command: |
            sudo apt-get update
            sudo apt-get install -y curl jq zip moreutils

      - run:
          name: Set Production Icon & Version
          command: |
            current_version=$(jq -r '.version' manifest.json)
            major=$(echo "$current_version" | cut -d. -f1)
            minor=$(echo "$current_version" | cut -d. -f2)
            patch=$(echo "$current_version" | cut -d. -f3)
            new_version="$major.$minor.$((patch + 1))"
            jq --arg v "$new_version" '.version = $v | .action.default_icon = "icon.png"' manifest.json | sponge manifest.json
      - run:
          name: Package Extension
          command: zip -r tab-counter.zip . -x '*.git*' -x '*.circleci*' -x '*README.md' -x '*test/*'
      - run:
          name: Upload & Publish to Chrome Web Store
          command: |
            ACCESS_TOKEN=$(curl -s -X POST https://accounts.google.com/o/oauth2/token \
              -d client_id=${CLIENT_ID} \
              -d client_secret=${CLIENT_SECRET} \
              -d refresh_token=${REFRESH_TOKEN} \
              -d grant_type=refresh_token \
              -d redirect_uri=urn:ietf:wg:oauth:2.0:oob | jq -r .access_token)

            UPLOAD_RESPONSE=$(curl -s -w "%{http_code}" -o upload_output.json \
              -H "Authorization: Bearer ${ACCESS_TOKEN}" \
              -H "x-goog-api-version: 2" \
              -X PUT -T tab-counter.zip \
              "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}")

            if [[ "$UPLOAD_RESPONSE" -ne 200 ]]; then
              echo "❌ Upload failed:"
              cat upload_output.json
              exit 1
            fi

            PUBLISH_RESPONSE=$(curl -s -w "%{http_code}" -o publish_output.json \
              -H "Authorization: Bearer ${ACCESS_TOKEN}" \
              -H "x-goog-api-version: 2" \
              -H "Content-Length: 0" \
              -X POST \
              "https://www.googleapis.com/chromewebstore/v1.1/items/${APP_ID}/publish")

            if [[ "$PUBLISH_RESPONSE" -ne 200 ]]; then
              echo "❌ Publish failed:"
              cat publish_output.json
              exit 1
            fi

workflows:
  deploy:
    jobs:
      - build
Copy to clipboard

Conclusion

That’s it! Your Chrome Extension now has a modern, automated deployment pipeline. With CircleCI handling packaging and publishing, you’ve eliminated manual steps and reduced the chance of deployment mistakes.

From here, you can iterate confidently, scale faster, and focus on building features instead of managing releases.

Happy shipping!

Copy to clipboard