Back to Blog

Continuous release of a chrome extension using CircleCI

on June 27, 2018

We have recently worked on many chrome extensions. Releasing new chrome extensions manually gets tiring after a while.

So, we thought about automating it with CircleCI, similar to continuous deployment.

We are using the following configuration in circle.yml to continuously release chrome extensions from the master branch.

1workflows:
2  version: 2
3  main:
4    jobs:
5      - test:
6          filters:
7            branches:
8              ignore: []
9      - build:
10          requires:
11            - test
12          filters:
13            branches:
14              only: master
15      - publish:
16          requires:
17            - build
18          filters:
19            branches:
20              only: master
21
22version: 2
23jobs:
24  test:
25    docker:
26      - image: cibuilds/base:latest
27    steps:
28      - checkout
29      - run:
30          name: "Install Dependencies"
31          command: |
32            apk add --no-cache yarn
33            yarn
34      - run:
35          name: "Run Tests"
36          command: |
37            yarn run test
38  build:
39    docker:
40      - image: cibuilds/chrome-extension:latest
41    steps:
42      - checkout
43      - run:
44          name: "Install Dependencies"
45          command: |
46            apk add --no-cache yarn
47            apk add --no-cache zip
48            yarn
49      - run:
50          name: "Package Extension"
51          command: |
52            yarn run build
53            zip -r build.zip build
54      - persist_to_workspace:
55          root: /root/project
56          paths:
57            - build.zip
58
59  publish:
60    docker:
61      - image: cibuilds/chrome-extension:latest
62    environment:
63      - APP_ID: <APP_ID>
64    steps:
65      - attach_workspace:
66          at: /root/workspace
67      - run:
68          name: "Publish to the Google Chrome Store"
69          command: |
70            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)
71            curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -X PUT -T /root/workspace/build.zip -v "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}"
72            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"

We have created three jobs named as test, build and publish and used these jobs in our workflow to run tests, build the extension, and publish them to the chrome store, respectively. Every step requires the previous step to run successfully.

Let's check each job one by one.

1test:
2  docker:
3    - image: cibuilds/base:latest
4  steps:
5    - checkout
6    - run:
7        name: "Install Dependencies"
8        command: |
9          apk add --no-cache yarn
10          yarn
11    - run:
12        name: "Run Tests"
13        command: |
14          yarn run test

We use cibuilds docker image for this job. First, we do a checkout to the branch and then use yarn to install dependencies. Alternatively, we can use npm to install dependencies as well. Then, as the last step, we are use yarn run test to run tests. We can skip this step if running tests is not needed.

1build:
2  docker:
3    - image: cibuilds/chrome-extension:latest
4  steps:
5    - checkout
6    - run:
7        name: "Install Dependencies"
8        command: |
9          apk add --no-cache yarn
10          apk add --no-cache zip
11          yarn
12    - run:
13        name: "Package Extension"
14        command: |
15          yarn run build
16          zip -r build.zip build
17    - persist_to_workspace:
18        root: /root/project
19        paths:
20          - build.zip

For building chrome extensions, we use the chrome-extension image. Here, we also first do a checkout and then, install dependencies using yarn. Note, we are install zip utility along with yarn because we need to zip our chrome extension before publishing it in next step. Also, we are not generating version numbers on our own. The version number will be picked from the manifest file. This step assumes that we have a task named build in package.json to build our app.

The Chrome store rejects multiple uploads with the same version number. So, we have to make sure to update the version number, which should be unique in the manifest file before this step.

In the last step, we use persist_to_workspace to make build.zip available for the next step, publishing.

1publish:
2  docker:
3    - image: cibuilds/chrome-extension:latest
4  environment:
5    - APP_ID: <APP_ID>
6  steps:
7    - attach_workspace:
8        at: /root/workspace
9    - run:
10        name: "Publish to the Google Chrome Store"
11        command: |
12          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)
13          curl -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "x-goog-api-version: 2" -X PUT -T /root/workspace/build.zip -v "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${APP_ID}"
14          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"

For publishing of the chrome extension, we use the chrome-extension image.

We need APP_ID, CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN/ACCESS_TOKEN to publish our app to the chrome store.

APP_ID needs to be fetched from Google Webstore Developer Dashboard. APP_ID is unique for each app whereas CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN/ACCESS_TOKEN can be used for multiple apps. Since APP_ID is generally public, we specify that in the yml file. CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN/ACCESS_TOKEN are stored as private environment variables using CircleCI UI. For cases when our app is unlisted in the chrome store, we need to store APP_ID as a private environment variable.

CLIENT_ID and CLIENT_SECRET need to be fetched from Google API console. There, we need to select a project and then click on the credentials tab. If there is no project, we need to create one and then access the credentials tab.

REFRESH_TOKEN needs to be fetched from Google API. It also defines the scope of access for Google APIs. We need to refer to Google OAuth2 for obtaining the refresh token. We can use any language library.

In the first step of the publish job, we are attaching a workspace to access build.zip, which was created previously. Now, by using all the required tokens obtained previously, we need to obtain an access token from Google OAuth API, which must be used to push the app to the chrome store. Then, we make a PUT request to the Chrome store API to push the app, and then use the same API again, to publish the app.

Uploading via API has one more advantage over manual upload. Manual upload generally takes up to 1 hour to show the app in the chrome store. Whereas uploading using Google API generally reflects the app within 5-10 minutes, considering app does not go for a review by Google.