Historical context
Mixmax's Chrome extension historically operated as a dual-component system: a "core" shell and a "source" frontend. Under Manifest V2, this architecture allowed for seamless updates by loading dynamic JavaScript, minimizing the need for Chrome Web Store releases. However, the introduction of Manifest V3 necessitated a complete overhaul of our release process given the increased dependency on Google's approval system.
Understanding Our Starting Point
Before redesigning our pipeline, we conducted a thorough analysis of our existing process, which revealed several challenges:
Existing Pain Points
- Complex Branch Management: We had 4 different branches (master, override-staging, beta, and production) that often led to frequent merge conflicts
- Fragile Deployment Process: The upload/deploy process was tightly coupled with the build process, making retries difficult
- Version Control Issues: Semantic-release sometimes failed to increase version numbers correctly and the beta/production builds had different version numbers
- Google's Approval Process: No visibility of whether the deployment is approved
- Roll-out Complications: Developers without access to the Webstore couldn’t manage partial roll-outs
Core Requirements
After analyzing our situation, we established clear constraints across three key areas:
Testing Needs
- Ability to test without Chrome Store involvement
- Maintain a reference build for automated QA testing
Release Management
- Version numbers must always increment and remain consistent between channels (public or beta)
- Cannot upload new versions until previous ones are approved
- Need flexibility in managing roll-out percentages
- Must support static asset releases to different environments (more on this later)
Environment Support
- Must work across local, staging, QA, and production environments
- Support for both beta and production release channels
The Reimagined Pipeline
Our solution addressed these challenges through three main components:
Single Branch Strategy
- Single master branch instead of four
- Preview builds generated directly from pull request branches
Automated Workflows
- Separate workflows for beta and production releases
- Automatic release of static assets with QA assertion
- Manual triggers to ensure control over the release (gate)
- Built-in support for partial roll-outs
Weekly Release Cycle (manual)
- Monday: Beta to production (10% roll-out) + New master to beta
- Tuesday: Increase to 30% if there are no issues
- Wednesday: Increase to 100% if stable
Here is a visual summary of how that translates to actual continuous integration steps (after merging a code):
Technical Implementation Details
The Two-Step Release Process
If you look closely at the image above, you might be asking yourself why it is required to build, deploy, and run smoke tests on the extension for different environments, and why would the deployment phase happen before the “gate” steps. That is a good question! Here is why:
The extension is actually released in 2 steps:
- A release of static assets
- A release of the Chrome extension in the webstore
What does it mean to “release static assets” and why it is needed?
Our Chrome extension modifies 3rd-party websites. It does so by grabbing specific selectors and mutating the DOM of those websites. Because we don’t control those 3rd-party websites, unexpected changes can break our extension. To be able to quickly react to those changes, we separate the critical bits and deploy them separately: CSS and page selectors are statically deployed and that happens as fast as possible!
So, to recap the image above, as soon as the new code lands on master
, it builds & releases the static assets to staging
. And then, it runs smoke tests against it. This type of change (a static asset) is backward compatible: The newer version of the static assets will run with the old version of the Chrome extension until that newer version reaches the user.
GitHub actions
The real magic sauce is how we combined Github actions with this new process.
In summary, those are the CI files that compose the process:
-
ci.yml
— the main one# ci.yml
name: continuous-integration
'on':
push:
branches:
- master
jobs:
# Release via semantic-release
release:
if: github.event.head_commit.author.username != 'semantic-release-bot'
uses: ./.github/workflows/semantic-release.yml
secrets: inherit
# Build & deploy static assets + QA (Staging)
build-and-deploy-staging:
needs: build-and-deploy-qa
uses: ./.github/workflows/build-and-deploy.yml
secrets: inherit
ext_stg_smoke_tests:
needs: build-and-deploy-staging
uses: ./.github/workflows/extension_stg_smoke.yml
secrets: inherit
with:
extension_public_url: $
# Build & deploy static assets + QA (Production)
build-and-deploy-prod:
needs: ext_stg_smoke_tests
uses: ./.github/workflows/build-and-deploy.yml
secrets: inherit
with:
override-environment: production
ext_prod_smoke_tests:
needs: build-and-deploy-prod
uses: ./.github/workflows/extension_prod_smoke.yml
secrets: inherit
with:
branches_for_test_execution: |
["master"]
extension_public_url: $
# For BETA build and publish
build-and-deploy-beta:
needs: ext_prod_smoke_tests
uses: ./.github/workflows/build-and-deploy.yml
secrets: inherit
with:
override-environment: beta
publish-to-beta:
needs: build-and-deploy-beta
uses: ./.github/workflows/publish-or-update-rollout.yml
secrets: inherit
with:
target: beta
deployPercentage: 100
# For PUBLIC, publish with 1% deploy percentage
publish-to-public:
needs: publish-to-beta
uses: ./.github/workflows/publish-or-update-rollout.yml
secrets: inherit
with:
target: public
deployPercentage: 1 -
build-and-deploy.yml
— builds the extension and deploy to s3 (static assets and the extension zip)# build-and-deploy.yaml
# This is a simplified version
name: Build and Deploy
on:
workflow_call:
inputs:
override-environment:
required: false
type: string
outputs:
extension-url:
description: 'S3 url of the built artifact'
value: $
env:
TARGET_ENVIRONMENT: $preview
permissions:
# Reduced set of necessary permissions
contents: write
id-token: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
outputs:
extension-url: $0
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
# To allow S3 uploads
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: us-east-1
role-to-assume: $
# At this stage, semantic already ran so we can grab the version from package.json
- name: Set Environment Variables
run: |
echo "VERSION=$(cat package.json | jq -r '.version')" >> "$GITHUB_ENV"
echo "GIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV"
- name: Build Application
run: npm run build:$
# Upload to Github actions
- uses: actions/upload-artifact@v4
if: $false
with:
name: extension-artifact-$
path: $
retention-days: 10
overwrite: true
- name: Upload to S3
id: upload-artifact
run: |
BUCKET=mixmax-extension-$
BUNDLE_NAME=$(echo *.zip)
aws s3 cp $BUNDLE_NAME "s3://$BUCKET/versions/$VERSION/$BUNDLE_NAME/" --cache-control 'max-age=0, must-revalidate' --acl public-read
aws s3 cp $BUNDLE_NAME "s3://$BUCKET/versions/latest.zip" --cache-control 'max-age=0, must-revalidate' --acl public-read
BUNDLE_URL=https://$.s3.amazonaws.com/versions/$VERSION/$BUNDLE_NAME
echo "EXTENSION_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
# Prepare and upload HTML, CSS, assets with appropriate headers
- name: Upload Static Assets
run: |
BUCKET=mixmax-extension-$
aws s3 cp static_dist/ "s3://$BUCKET/static/" --recursive --cache-control 'max-age=0' --acl public-read -
publish-or-update-rollout.yml
— changes the extension in the Chrome Webstore# publish-or-update-rollout.yml
name: Publish Chrome Extension
on:
workflow_call:
inputs:
target:
required: true
type: string
deployPercentage:
required: true
type: number
workflow_dispatch:
inputs:
deployPercentage:
required: true
type: number
version:
description: 'Version to deploy (must match git tag) - ex v1.2.3'
required: true
type: string
permissions:
# ... relevant permissions ...
jobs:
publish:
runs-on: ubuntu-latest
env:
CHROME_CLIENT_ID: $
CHROME_CLIENT_SECRET: $
CHROME_REFRESH_TOKEN: $
BETA_CHANNEL_ID: your-beta-extension-id
PUBLIC_CHANNEL_ID: your-public-extension-id
TARGET: $
environment: $
steps:
- name: Validate version
if: $false
run: |
if [[ ! $ =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then
echo "Invalid version format"
exit 1
fi
- uses: actions/checkout@v2
with:
ref: $
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Download extension artifact
if: $false
uses: actions/download-artifact@v4
with:
name: extension-artifact-$
- name: Publish extension
run: |
COMMAND="npx chrome-webstore-upload-cli"
if [[ "$" == 'push' ]]; then
COMMAND+=" --source=path/to/extension.zip"
else
COMMAND+=" publish"
fi
# Add authentication parameters
COMMAND+=" --client-id=$"
COMMAND+=" --client-secret=$"
COMMAND+=" --refresh-token=$"
# Set extension ID based on target
COMMAND+=" --extension-id=$"
# Add deployment options
if [[ "$" == 'public' ]]; then
COMMAND+=" --deploy-percentage=$"
else
COMMAND+=" --trusted-testers=true"
fi
eval $COMMAND
Wait, but there is more
It is paramount that we test the extension before releasing it. We made that happen through a comment in the Pull request:
-
And for that, we have done a “preview” build and deployment:
# PIPELINE FOR PREVIEW BUILDS
name: continuous-integration
'on':
pull_request:
branches:
- master
jobs:
build-and-deploy-preview:
needs: checks
uses: ./.github/workflows/build-and-deploy.yml
secrets: inherit
preview_pr_comment:
needs: build-and-deploy-preview
runs-on: ubuntu-22.04-8-cores
steps:
- name: Find existing comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: $
comment-author: 'github-actions[bot]'
body-includes: '<!-- preview-deploy-comment -->'
- name: Comment link to extension build in PR
if: $true
uses: actions/github-script@v7
with:
github-token: $
script: |
const url = '$';
const message = `<!-- preview-deploy-comment -->
> [!NOTE]
> Subsequent updates in this Pull Request will be uploaded under the same URL below.
[Download extension build](${url}) for this Pull Request.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message
})
Managing the gate
After the automation runs, we can approve the pipeline for the different deployments:
And following our weekly cadence, devs with access to Github can create a new workflow to update the rollout percentage:
Shameless plug
After this whole saga, one feature is still missing:
Google's Approval Process: No visibility of whether the deployment is approved
And for that, we used the easiest tool: Mixmax! You can try it out for free. Here is how:
- Set up your Mixmax account at https://app.mixmax.com
- Go to the Rules page and create a rule as such:
- Trigger = “I receive an email”
- Filter = “From(email) is chromewebstore-noreply@google.com”
- Action = Slack > “Send channel message”
Conclusion
Our new pipeline has significantly improved the Chrome extension release process, reducing labor and increasing reliability. With GitHub Actions, AWS S3, and Chrome Web Store APIs, we've created a powerful and maintainable process. And, on top of that, Mixmax helps us track Google's approval process, addressing all previous pain points.
A good release pipeline balances control, speed, and reliability. We believe this solution achieves that balance effectively.
I hope you have learned at least one or two deployment best practices and/or caveats for your Chrome Extension deployment!
Happy deploying! 🚀
Interested in joining the team? Visit Mixmax Careers.