Skip to content

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

Written by: Raphael Brandão

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

Try Mixmax free

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