Post 7 of 9 in "Releasing WebExtension using GitHub Actions" series
In this part we are going to create the workflow that will be responsible for publishing the extension on Chrome Web Store. This part is going to be a bit tricky comparing to the others.
🧱 Prepare
❶ To set up Google Publish API access you need to obtain clientId
, clientSecret
and refreshToken
from Google. These articles can help you to do that:
* Using the Chrome Web Store Publish API
* How to generate Google API keys
🔒 Add to the repository secrets:
G_CLIENT_ID
G_CLIENT_SECRET
G_REFRESH_TOKEN
❷ You should find out the ID of your extension on Chrome Web Store. Normally, it is shown on your Developer Dashboard. It’s not private information, but I prefer to store it in secrets.
🔒 Add to the repository secrets:
G_EXTENSION_ID
❸ Also, we have to create a new personal access token with write access to the repo. The automatically provided token e.g. secrets.GITHUB_TOKEN
can not be used, GitHub prevents this token from being able to fire the workflow_dispatch and repository_dispatch event.
🔒 Add to the repository secrets:
WORKFLOWS_TOKEN
❹ The last preparation step is creating an environment in the repository settings. It’s main purpose is providing a wait timer to delay a workflow run (details will be explained below). Let’s call the environment 12hoursDelay
and set its wait timer equal 720
minutes (12 hours).
publish-on-chrome-web-store workflow
On the one hand, the workflow will be similar to publish-on-firefox-add-ons workflow (described in the previous part): it is also triggered by workflow_dispatch
event (emitted manually or by publish-release-on-tag) and retrieves zip asset in the same way.
On the other hand, it has its own complications because of peculiarity of Webstore publishing. Also, we are going to include the additional job for downloading published crx file from Webstore and attaching it to the release (if any).
The publishing process consists of 2 steps: uploading a new version and publishing the extension. What is the peculiarity I’m speaking about?
It’s a Webstore behavior in the case when you are trying to upload a new version shortly after a previous one was uploaded to the store. In this case API call completes with an error and with the status called IN_REVIEW
(only one uploaded version can be in processing at the time). Unlike other errors, it doesn’t mean that something is wrong with our extension and if we try uploading later it will succeed.
That’s exactly what we are going to do, it only remains to find a good technical solution to do that. If you are interested in learning about existing approaches, please read my “GitHub Actions: implementing deferred steps” post. Here we will use “Dispatched workflow calling itself + wait timer” approach to repeat a new version uploading after 12 hours delay (number was chosen arbitrary).
Let’s observe the workflow diagram and start with creating the workflow file:
As you can see, the main idea is the following:
- We try to upload a new extension version.
- If it succeeded, we proceed to publishing the extension and downloading the published crx file.
- If it didn’t succeed due to IN_REVIEW error, we dispatch the workflow with 12 hours delay to repeat it later. We will limit attempts number by incrementing an attemptNumber and passing it as an input to the workflow.
.github/workflows/publish-on-chrome-webstore.yml :
name: publish-on-chrome-web-store
on:
workflow_dispatch:
inputs:
attemptNumber:
description: 'Attempt number'
required: false
default: '1'
maxAttempts:
description: 'Max attempts'
required: false
default: '10'
environment:
description: 'publish-on-webstore job environment'
required: false
default: ''
jobs:
# We will add 2 jobs here:
# publish-on-webstore:
# ...
# download-published-crx:
# ...
Defined workflow_dispatch event has 3 inputs that can be specified at the time of dispatching the workflow. You will see their usage in the following job. The first time the workflow is triggered, inputs will have their default values.
publish-on-webstore job
publish-on-webstore:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
outputs:
result: ${{ steps.webStorePublish.outcome }}
releaseUploadUrl: ${{ steps.getZipAsset.outputs.releaseUploadUrl }}
steps:
# Validate the inputs and increase the attemptNumber if less than maxAttempts
- name: Get the next attempt number
id: getNextAttemptNumber
uses: cardinalby/js-eval-action@v1
env:
attemptNumber: ${{ github.event.inputs.attemptNumber }}
maxAttempts: ${{ github.event.inputs.maxAttempts }}
with:
expression: |
{
const
attempt = parseInt(env.attemptNumber),
max = parseInt(env.maxAttempts);
assert(attempt && max && max >= attempt);
return attempt < max ? attempt + 1 : '';
}
- uses: actions/checkout@v2
- uses: cardinalby/export-env-action@v1
with:
envFile: './.github/workflows/constants.env'
expand: true
- name: Obtain packed zip
id: getZipAsset
uses: ./.github/workflows/actions/get-zip-asset
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch Google API access token
id: fetchAccessToken
uses: cardinalby/google-api-fetch-token-action@v1
with:
clientId: ${{ secrets.G_CLIENT_ID }}
clientSecret: ${{ secrets.G_CLIENT_SECRET }}
refreshToken: ${{ secrets.G_REFRESH_TOKEN }}
- name: Upload to Google Web Store
id: webStoreUpload
continue-on-error: true
uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@v1
with:
zipFilePath: ${{ env.ZIP_FILE_PATH }}
extensionId: ${{ secrets.G_EXTENSION_ID }}
apiAccessToken: ${{ steps.fetchAccessToken.outputs.accessToken }}
waitForUploadCheckCount: 10
waitForUploadCheckIntervalMs: 180000 # 3 minutes
# Schedule a next attempt if store refused to accept new version because it
# still has a previous one in review
- name: Start the next attempt with the delay
uses: aurelien-baudet/workflow-dispatch@v2
if: |
steps.getNextAttemptNumber.outputs.result &&
steps.webStoreUpload.outputs.inReviewError == 'true'
with:
workflow: ${{ github.workflow }}
token: ${{ secrets.WORKFLOWS_TOKEN }}
wait-for-completion: false
inputs: |
{
"attemptNumber": "${{ steps.getNextAttemptNumber.outputs.result }}",
"maxAttempts": "${{ github.event.inputs.maxAttempts }}",
"environment": "12hoursDelay"
}
- name: Abort on unrecoverable upload error
if: |
!steps.webStoreUpload.outputs.newVersion &&
steps.webStoreUpload.outputs.sameVersionAlreadyUploadedError != 'true'
run: exit 1
- name: Publish on Google Web Store
id: webStorePublish
uses: cardinalby/webext-buildtools-chrome-webstore-publish-action@v1
with:
extensionId: ${{ secrets.G_EXTENSION_ID }}
apiAccessToken: ${{ steps.fetchAccessToken.outputs.accessToken }}
environment: ${{ github.event.inputs.environment }}
sets the environment for the job providing a required delay (via “wait timer” of the environment).- We use js-eval-action as a generic JS code interpreter to validate the inputs and calculated the incremented attemptNumber. It is accessible as result output of the step.
- As usual, at the beginning of each workflow we check out the repo and export env variables from constants.env file.
- After calling get-zip-asset composite action we expect to have zip file with packed and built extension at
env.ZIP_FILE_PATH
path. - We call google-api-fetch-token-action to retrieve Google API access token that is needed for “upload” and “publish” steps.
- We use webext-buildtools-chrome-webstore-upload-action action to upload a new version to Webstore.
continue-on-error: true
flag allows us not to fail immediately, but perform the next “dispatching” step and examine the error after it in a separate step (checking the action’s outputs). - We use aurelien-baudet/workflow-dispatch action to dispatch the workflow in case of
IN_REVIEW
error:workflow: ${{ github.workflow }}
points to the current workflow filetoken: ${{ secrets.WORKFLOWS_TOKEN }}
makes use of the personal access token we created at the preparation stepinputs
is JSON containing values of inputs for workflow_dispatch event:attemptNumber
is an incremented workflow input value read from the validation step.maxAttempts
is the workflow input value passed without changes.environment
is the name of the environment that will be used to delay the execution.
- “Abort on unrecoverable upload error” step complements the “upload” step validating errors and fails the job if we can’t proceed with publishing because the new version was not uploaded. The case when the same version has been already uploaded (
sameVersionAlreadyUploadedError
output indicates that) is the exception - we still can publish it. - Finally, we call webext-buildtools-chrome-webstore-publish-action action to publish the extension.
Finally, we are done. Keep in mind that in the sake of simplicity I omit some details and description of optional inputs of the used actions. There are a lot of things to tune up. Please, read the documentation for corresponding actions to learn more.
In the next part we are going to solve one small but annoying issue with Google API refresh token expiration.