December 6, 2024

How We Built a Chrome Extension Pipeline So Good, It Might Just Apply for a Patent

How We Built a Chrome Extension Pipeline So Good, It Might Just Apply for a Patent

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

extension-1

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):

extension-2

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:

  1. A release of static assets
  2. 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:

extension-3

  • 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:

extension-4

And following our weekly cadence, devs with access to Github can create a new workflow to update the rollout percentage:

extension-5

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:

  1. Set up your Mixmax account at https://app.mixmax.com
  2. Go to the Rules page and create a rule as such:
    1. Trigger = “I receive an email”
    2. Filter = “From(email) is chromewebstore-noreply@google.com
    3. Action = Slack > “Send channel message”

extension-6

extension-7

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.

You deserve a spike in replies, meetings booked, and deals won.

Try Mixmax free