In recent times we worked on many chrome extensions. Releasing new chrome extensions manually gets tiring afer a while.

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

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

workflows:
  version: 2
  main:
    jobs:
      - test:
          filters:
            branches:
              ignore: []
      - build:
          requires:
            - test
          filters:
            branches:
              only: master
      - publish:
          requires:
            - build
          filters:
            branches:
              only: master

version: 2
jobs:
  test:
    docker:
      - image: cibuilds/base:latest
    steps:
      - checkout
      - run:
          name: "Install Dependencies"
          command: |
            apk add --no-cache yarn
            yarn
      - run:
          name: "Run Tests"
          command: |
            yarn run test
  build:
    docker:
      - image: cibuilds/chrome-extension:latest
    steps:
      - checkout
      - run:
          name: "Install Dependencies"
          command: |
            apk add --no-cache yarn
            apk add --no-cache zip
            yarn
      - run:
          name: "Package Extension"
          command: |
            yarn run build
            zip -r build.zip build
      - persist_to_workspace:
          root: /root/project
          paths:
            - build.zip

  publish:
    docker:
      - image: cibuilds/chrome-extension:latest
    environment:
      - APP_ID: <APP_ID>
    steps:
      - attach_workspace:
          at: /root/workspace
      - run:
          name: "Publish to the Google Chrome Store"
          command: |
            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 /root/workspace/build.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"

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 to chrome store respectively. Every step requires the previous step to run successfully.

Let’s check each job one by one.

test:
  docker:
    - image: cibuilds/base:latest
  steps:
    - checkout
    - run:
        name: "Install Dependencies"
        command: |
          apk add --no-cache yarn
          yarn
    - run:
        name: "Run Tests"
        command: |
          yarn run test

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

build:
  docker:
    - image: cibuilds/chrome-extension:latest
  steps:
    - checkout
    - run:
        name: "Install Dependencies"
        command: |
          apk add --no-cache yarn
          apk add --no-cache zip
          yarn
    - run:
        name: "Package Extension"
        command: |
          yarn run build
          zip -r build.zip build
    - persist_to_workspace:
        root: /root/project
        paths:
          - build.zip

For building chrome extension we are using chrome-extension image. Here again, first we are doing a checkout and then installing dependencies using yarn. Note that we are installing 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.

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 are using persist_to_workspace to make build.zip available to next step for publishing.

publish:
  docker:
    - image: cibuilds/chrome-extension:latest
  environment:
    - APP_ID: <APP_ID>
  steps:
    - attach_workspace:
        at: /root/workspace
    - run:
        name: "Publish to the Google Chrome Store"
        command: |
          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 /root/workspace/build.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"

For publishing, of chrome extension, we are using chrome-extension image.

We need APP_ID, CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN/ACCESS_TOKEN to publish our app to 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 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 on 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 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 Google OAuth2 for obtaining the refresh token. We can use any language library.

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

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