How to Continuously Deploy a Chrome Extension
Community Engineer, CircleCI
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 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.
- Create a new Google API project. Here’s a direct link.
- Make sure the project just created is the currently selected project in the dropdown at the top of the screen. ![Step 2 Screenshot]!(//images.ctfassets.net/il1yandlcjgk/3xWD0Xn6II5Qjr4L0spEfC/7b9dff5747f4a05bf49611038adbe28a/step-2.png)
- Search for “Chrome Web Store API” in the API search bar and select that API.
- Enable the API.
- Click the “Create credentials” blue button.
- Don’t create an API key (the default). Instead, click the “client ID” link.
- For application type, choose “Other” and in the text field that appears, name this new client. Feel free to use “CircleCI”.
- A “client ID” and “client secret” will be provided. Save this information. They’ll be needed shortly.
- 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
- 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.
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 execution 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