Google Chrome is the most-used browser on the Internet. And people are creating Chrome Extensions for all kinds of use-cases. Within 24 hours of Twitter announcing their 280 character tweets test, a new Chrome Extension collapsing tweets back to 140 characters was born. Within days of the hurricane in Puerto Rico, a $0.99 extension called Donate to Puerto Rico was created, showcasing beautiful images of the island, with all proceeds going towards charities helping rehabilitate the island.

In a space that moves this quickly, we can automate the delivery of features, bug fixes, and security patches with Continuous Deployment. Google’s Chrome Developer Docs has a lot of information but doesn’t include anything on Continuous Integration or provide examples for automated deployment. We’ll cover one example in this post along with how we can tackle development environment challenges and versioning.

Getting Started

Note: The examples and screenshots from this blog post are based off a Chrome extension for CircleCI I wrote called Pointless. You can find it at the Google Chrome Store and on GitHub. If you see the name “pointless” in certain commands or screenshots, it’s referring to the name of the Chrome Extension. It should be replaced with your own where appropriate.

To get started, we need to create and configure a Google API project and collect a few key bits of information. Having curl and jq installed on your system will make completing these steps easier.

  1. Create a new Google API project. Here’s a direct link. Step 1 Screenshot
  2. Make sure the project just created is the currently selected project in the dropdown at the top of the screen. Step 2 Screenshot
  3. Search for “Chrome Web Store API” in the API search bar and select that API. Step 3 Screenshot
  4. Enable the API. Step 4 Screenshot
  5. Click the “Create credentials” blue button. Step 5 Screenshot
  6. Don’t create an API key (the default). Instead, click the “client ID” link. Step 6 Screenshot
  7. For application type, choose “Other” and in the text field that appears, name this new client. Feel free to use “CircleCI”. Step 7 Screenshot
  8. A “client ID” and “client secret” will be provided. Save this information. They’ll be needed shortly.
  9. Visit the following URL in your browser, replacing $CLIENT_ID with the “client ID” from the previous step.
    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
    
  10. After accepting the permission popup, you’ll be presented with what Google simply calls a “code”. Save this information.

At this point, we should have 3 key bits of information. A client_ID, client_secret, and a code. The next thing needed is what is called a “refresh token” and we can get that by using curl and jq in a terminal. Enter the following command in a terminal replacing $CLIENT_ID, $CLIENT_SECRET, and $CODE with the actual values you have:

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

A “refresh token” will be returned. Save this information.

The last item needed is actually the easiest to get, the Chrome extension’s Application ID. Visit the Chrome extension in the Chrome Store. Take a look at the URL in the browser, the last part of the path (the text after the last forward slash) is the Application ID. Save this information.

Chrome Store Screenshot

Basic Setup

Let’s start with a basic example: automatically deploying a Chrome extension via the master branch. The full, basic example CircleCI Config file can be found at Appendix A.

Starting The Configuration File

We’re going to start out with a CircleCI config file like this:

version: 2
jobs:
  build:
    docker:
      - image: ubuntu:16.04
    environment:
      - APP_ID: <INSERT-APP-ID>

We’re using CircleCI 2.0 with the Ubuntu 16.04 Docker image as our build environment. The “Application ID” that we obtained in the “Getting Started” section is then set here so that we can use it later during the build process. Make sure to replace <INSERT-APP-ID> with your actual application ID.

Saving Our Secrets

Our build requires several environment variables to work. APP_ID is one of those variables and we set it in the config because it isn’t considered to be secret. The application ID of a Chrome extension is public knowledge. There are three more that we need to set, but we will keep these secret for security reasons. Make sure to create and set the private environment variables CLIENT_ID, CLIENT_SECRET, and REFRESH_TOKEN via the CircleCI UI.

The Body of Our Configuration

    steps:
      - checkout
      - run:
          name: "Install Dependencies"
          command: |
            apt-get update
            apt-get -y install curl jq
            # You can also install Yarn, NPM, or anything else you need to use to build and test your extension.

As most jobs on CircleCI, we first checkout the code and then install dependencies. Here, we install curl and jq, the same tools we used earlier, into our container so that we can use them for the publishing process. Any tools needed for your test should be installed here as well.

      - run:
          name: "Run Tests"
          command: echo "Run any tests here."

Here is where you’d run your tests. As this blog post is about deployment, and test strategy can be affected by your specific project and is a big topic, we’re not going to cover it in this post. With enough interest, we may do a separate post on testing for both Chrome extensions and Firefox plugins in the future.

      - run:
          name: "Package Extension"
          command: git archive -o pointless.zip HEAD

The Google Chrome Store API requires extensions to be packages as a .ZIP file in order to be uploaded. Here the git subcommand archive is used to create that zip from the project. It’s beneficial here to use git archive instead of zip to create the zip file because the former will ignore the .git directory, which we don’t want in our package as it’s just bloat. One less command to install.

Publishing

      - run:
          name: "Upload & Publish Extension to the Google Chrome Store"
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              ACCESS_TOKEN=$(curl "https://accounts.google.com/o/oauth2/token" -d "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token&redirect_uri=urn:ietf:wg:oauth:2.0:oob" | jq -r .access_token)
              curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -X PUT -T pointless.zip -v "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}"
              curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -H "Content-Length: 0" -X POST -v "https://www.googleapis.com/chromewebstore/v1.1/items/${APP_ID}/publish"
            fi

The Chrome Store API uses an “access token” for authenticated operations. An access token is only valid for 40 minutes however. Luckily, we have a “refresh token” instead that we use to request a fresh access token from the Chrome Store. With that access token, we then upload the zip, and then finally, publish that upload as our new release.

The whole thing is wrapped in a Bash if block so that we only publish new versions of our extension when this build is via the master branch.

This was a basic example of publishing a Google Chrome Extension using Continuous Deployment. Again, the complete example config file can be found at Appendix A. If you’d like to see some more advanced techniques, keep reading.

Advanced Setup

Maintaining Environment Separation

During development of an extension, it’s likely that it would be tested in Chrome “unpacked”. The can lead to an annoying problem. While the in-development version of the extension is loaded along with the “production” version, the extension appears in duplicate. The same browser action icon, version number, etc will show up. We can solve this.

In the extension’s manifest.json file, the one that is committed in git, a development version of the icon can be set. This can be an icon that is different in color or simply some sort of change to indicate it isn’t the production release of the extension. The same can be done with the version. The development version can be something generic and a production version number can be dropped in before release. Here’s a snippet of a manifest.json file utilizing this method and a CircleCI “step” showing how the extension can be made “production ready” during a build.

manifest.json Snippet

{
	"manifest_version": 2,
	"name": "Pointless - a CircleCI Chrome Extension",
	"short_name": "Pointless",
	"version": "9.9.9.9",
	"version_name": "dev-version",
    
	"browser_action":{
		"default_icon": "development-icon.png"
	}
}

CircleCI Config Example Step

      - run:
          name: "Make Extension Production Ready"
          command: |
            jq '.version = "new-version"' manifest.json | sponge manifest.json
            jq '.browser_action.default_icon = "icon.png"' manifest.json | sponge manifest.json

You can view a complete CircleCI config example with the advanced setup techniques in Appendix B.

Deploy With Git Tags

Instead of deploying a new version of the Chrome extension upon every successful build of master, it can be done when a new Git tag is pushed. In order to accomplish this, the CircleCI config will need to be adjusted for Workflows. The single “job” can be split into a “build” and “publish” job and Workflow filters used to indicate the type of Git tag we want to use. Here’s what that Workflow’s specific lines would look like:

workflows:
  version: 2
  main:
    jobs:
      - build:
          filters:
            tags:
              only: /.*/
      - publish:
          requires:
            - build
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /.*/

In this example, the extension ZIP will be built in the build job and published in the publish job. CircleCI’s Workspaces are used to move the ZIP file through the Workflow. You can view a complete CircleCI config example with the advanced setup techniques in Appendix B.

Custom Docker Image

The Docker image used in the basic setup example, ubuntu:16.04, is a fairly large image. Using a smaller, more targeted image means faster builds. A pre-made Docker image, based on Docker Alpine, can be used for Chrome Extensions by using cibuilds/chrome-extension. For more information, check out the Docker Hub page or the GitHub repo.

Appendix

Appendix A

Complete CircleCI Configuration File (.circleci/config.yml) demonstrating the minimum setup needed to deploy a Chrome extension to the Google Chrome Store.

version: 2
jobs:
  build:
    docker:
      - image: ubuntu:16.04
    environment:
      - APP_ID: <INSERT-APP-ID>
    steps:
      - checkout
      - run:
          name: "Install Dependencies"
          command: |
            apt-get update
            apt-get -y install curl jq
            # You can also install Yarn, NPM, or anything else you need to use to build and test your extension.
      - run:
          name: "Run Tests"
          command: echo "Run any tests here."
      - run:
          name: "Package Extension"
          command: git archive -o pointless.zip HEAD
      - run:
          name: "Upload & Publish Extension to the Google Chrome Store"
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              ACCESS_TOKEN=$(curl "https://accounts.google.com/o/oauth2/token" -d "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token&redirect_uri=urn:ietf:wg:oauth:2.0:oob" | jq -r .access_token)
              curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -X PUT -T pointless.zip -v "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}"
              curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -H "Content-Length: 0" -X POST -v "https://www.googleapis.com/chromewebstore/v1.1/items/${APP_ID}/publish"
            fi

Appendix B

workflows:
  version: 2
  main:
    jobs:
      - build:
          filters:
            tags:
              only: /.*/
      - publish:
          requires:
            - build
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /.*/


version: 2
jobs:
  build:
    docker:
      - image: cibuilds/chrome-extension:latest
    steps:
      - checkout
      - run:
          name: "Install Dependencies"
          command: echo "You can also install Yarn, NPM, or anything else you need to use to build and test your extension."
      - run:
          name: "Make Extension Production Ready"
          command: |
            jq '.version = "new-version"' manifest.json | sponge manifest.json
            jq '.browser_action.default_icon = "icon.png"' manifest.json | sponge manifest.json
      - run:
          name: "Run Tests"
          command: echo "Run any tests here."
      - run:
          name: "Package Extension"
          command: git archive -o pointless.zip HEAD
      - persist_to_workspace:
          root: /root/project
          paths:
            - pointless.zip

  publish:
    docker:
      - image: cibuilds/chrome-extension:latest
    environment:
      - APP_ID: <INSERT-APP-ID>
    steps:
      - attach_workspace:
          at: /root/workspace
      - run:
          name: "Publish to the Google Chrome Store"
          command: publish /root/workspace/pointless.zip