Skip to main content

Releasing a browser extension like itʼs 1999

Bitcrowdʼs custom approach on semi-automatic releases of our browser extension for Chrome and Firefox.

· 14 min read
Max Mulatz

bitcrowd has been developing and maintaining an open source browser extension since 2015 - Tickety-Tick! Within our fleet of open source projects, the browser extension has always been a special breed. It is more of a niche technology, not something we develop every day, and generally an area of web development which feels a bit more wild west compared to others.

With the lack of best practises - even though Mozilla and Chrome are continuously working on better documentation of the topic - there wasnʼt an obvious way to “deploy” a browser extension and make use of automation for it. So hereʼs our journey on how to release browser extensions like itʼs 1999. Maybe people can draw inspiration from our approach for their own niche web technology situation.

Problem

Tickety-Tick is a browser extension for naming Git branches and commit messages, a classic “developersʼ little helper” kind of tool (check it out if you havenʼt already). Pretty much from its early days on, itʼs been available for Firefox, Chrome, Safari and Opera. Actually making it available and releasing a new version to its users however, used to be a tedious manual process. The developer assigned with the task had to:

  1. 🧑‍💻 Check out and run the latest version of the code on their machine.
  2. 🏷️ Bump the version number, add a Git tag and prepare a pull request for it.
  3. ✅ Merge the pull request and run automated checks on the soon to be released version.
  4. 👷‍♀️Build releases for Firefox and Chrome locally (one requires a directory, the other a zip file…).
  5. 🦊 Log into the Firefox Add-ons management backend and go through the form to upload a new version.
  6. 🌐 Log into the Chrome Web Store management backend and go through another form to upload a new version.
  7. 🐙 Draft a release with release notes on GitHub.
  8. 🙋 Wait for questions and/or approval on Firefox Add-Ons.
  9. 📬 Answer emails from the review processes on Firefox or Chrome and send/upload the source code to the reviewers.
  10. ⏳ Frequently come back to see whether things are done now.

tl;dr: it was a time consuming process which involved bundling the extension locally and logging into different accounts to manually upload it. We tried our best to document the procedure so that anyone with access to the accounts could do it. But it was hard to keep up with the frequent UI and process changes of both platforms, so releasing Tickety-Tick always required a bit of domain knowledge and definitely a lot of time and attention. Naturally, we all shied away from this task and released only very infrequently which made us even less confident about doing it. A classic decay spiral…

note

We donʼt do separate releases on platforms for Safari and Opera. Safari users can manually install the release and Opera uses Chrome extensions.

History of Attempts

While the pain points of releasing our browser extension were obvious, it wasnʼt until a train ride from Berlin to Rotterdam for Euroku in 2019, that we actually made an attempt to tackle them. Still on the train, we started to hack away some automations to make releasing require less attention and prior knowledge.

The idea was to offload as many tasks as possible from the developerʼs hands to CI and automate them there.

Testing and building on CI was straightforward, and even drafting GitHub releases with a changelog was doable with the tools at hand. But we could not find any solid solutions for the biggest pain points of the release process: logging into different accounts and going through way too detailed upload forms. This was the blocker for any quick automation solution we were coming up with.

We tinkered with the idea on and off over the years and ended up with a semi-automated release process based on a dedicated set of tools and scripts. As weʼre so used to fully automated releases these days, a patchwork “semi-automated” flow may seem counterintuitive, but it turned out to be the best fit for our needs here.

Hereʼs what we came up and why:

Solution

Tinkering along we found that several rather diverse factors play into the equation for finding a suitable release process. Some come from us and are more organisational and political, some come from the fact that weʼre dealing with a browser extension here and are more technical:

  • 🕵️‍♂️ Find reliable APIs, tools and libraries for automation.

    Since we are dealing with multiple 3rd parties (Firefox, Chrome, GitHub, etc.), there no silver bullet tool to rule them all. Instead we have to find individual solutions and tools for all steps of the workflow and see how much of the manual process can be automated with them. The tools need to be specific for their task and general enough so that they can be combined to a bigger pipeline.

  • 🕊️ Users need to be free to install any version.

    Platforms like Firefox Add-ons or the Chrome Webstore are handy, but we still want people to manually install Tickety-Tick and have full control over which version they are running. By default, browsers auto-update themselves and their installed extensions without the ability to downgrade again. So in order to give people the options to freely up- and downgrade whenever they want, any release on GitHub should provide a running version for manual installation in all of the browers we support.

    Chrome/Opera and Safari are uncomplicated with that, but Firefox requires a special .xpi file signed by Mozilla…

  • 🔐 Shipping to browsers has security impacts.

    As browsers by default roll out updates to their extensions automatically without prompting or notifying the users, releasing browser extensions comes with big responsibilities. Browsers are peopleʼs main entry point to the web and they see a lot of very private information (contacts, payment data, messages, etc.). A previously trusted extension being “hacked”, taken over or shipped with hidden malicious code can do a lot of damage.

    In order to not take any risks here, we prefer to not share any credentials with other parties outside of the bitcrowd organisation. If GitHub or CircleCI cannot upload to Firefox Add-ons, their potential issues are not becoming our issues.

  • 🔂 Devs need the ability to monitor and replay steps.

    The release submission, especially with Mozillaʼs addons-API is very brittle: processing takes a lot of time, API schemas change, responses change, error codes change, things time out, etc. There are just too many unforeseen interruptions to have this process done by a machine alone without a human monitoring it.

    Uploading new extension versions on Chromeʼs Webstore and Firefox Add-ons is not idempotent and released versions cannot be overridden.

    The developer observing also needs to be able to quickly abort, adjust and replay certain steps of the release pipeline. It may be that submitting to Chrome and Firefox worked fine, but drafting the release on GitHub failed for some reason. In that scenario it may not be possible to roll back the submissions any more, but we want to fix and repeat the part about GitHub.

Considering all of this lead us to a semi-automated release pipeline. A developer with maintainer access triggers the release from their local machine, monitors what is happening and can interrupt and replay when necessary. It feels more like a Capistrano deploy from back in the days than a fully automated pipeline somewhere in the cloud, but itʼs working perfectly fine and addressed all pain points without overdoing it 🎉.

Scripts

We designed releasing as a two step process:

  1. Prepare the project for the new version
  2. Release the new version with its artifacts

Both are implemented as Bash scripts using a variety of tools underneath. Letʼs take a look at whatʼs happening 👀.

Prepare release

The process starts with a yarn prepare-release command which runs this Bash script:

# …

./script/check-release-dependencies

branch="chore/prepare-release-$(git rev-parse HEAD)"
git checkout -b "$branch"

yarn version

tag=$(git describe --abbrev=0 --tags)

git tag --delete "$tag"

gh pr create \
--title "Prepare $tag" \
--body "Bump version to $tag."

The script will set the new SemVer version number, create a Git tag for the upcoming release and open a pull request on GitHub for the change. Letʼs quickly walk through all steps:

./script/check-release-dependencies

This will run a small helper script to ensure the developer releasing has GitHubʼs CLI tool installed, a nifty little helper heavily utilized by our scripts.

Next weʼre checking out a Git branch to prepare the release:

branch="chore/prepare-release-$(git rev-parse HEAD)"
git checkout -b "$branch"

The branch name includes the hash of the current commit, just to make sure only one release is prepared for that current state of the project.

Then we run yarnʼs built in yarn version command to prompt the developer for the new SemVer version. This will update the version number in the projectʼs package.json file and create a Git tag with the version name.

Hereʼs where weʼre doing a bit of our own thing then:

tag=$(git describe --abbrev=0 --tags)

git tag --delete "$tag"

We delete the Git tag again because we want to wait for the version change to be merged to main first. For this weʼre creating a pull request next, utilizing the previously mentioned GitHub CLI tool gh:

gh pr create \
--title "Prepare $tag" \
--body "Bump version to $tag."

Our CI will run a set of checks on the pull request and once itʼs merged, we can continue with the second step of the release process.

Release

With the pull request merged, the developer can pull the latest changes of the main branch and continue with the actual release step with yarn release, running our release script:

Click here to expand the full script
# …

./script/check-release-dependencies

branch=$(git branch --show-current)

if [[ "$branch" != "main" ]]; then
abort "Please only release from the main branch."
fi

yarn checks

version=$(script/version)
tag="v$version"

git tag "$tag"
git push origin "$tag"

# Build artifacts.
yarn bundle:chrome
yarn bundle:firefox

# Create a GitHub release, upload build artifacts.
gh release create "$tag" \
--generate-notes \
--verify-tag \
./dist/*.zip

gh release download "$tag"\
--archive=zip \
--clobber \
--output="release-artifacts/tickety_tick-${tag##v}.zip"

if has op; then
apikey=$(op item get "bgi26drb3naqhipfmt5wth6634" --field key)
apisecret=$(op item get "bgi26drb3naqhipfmt5wth6634" --reveal --field secret)
client_id=$(op item get "yjtvyfa4wcygxlps6smdeybdu4" --field "Client ID")
client_secret=$(op item get "yjtvyfa4wcygxlps6smdeybdu4" --reveal --field "Client Secret")
refresh_token=$(op item get "yjtvyfa4wcygxlps6smdeybdu4" --reveal --field "OAuth Refresh Token")
else
# Mozilla Add-Ons
read -rp "Please provide your Mozilla API Key: " apikey
read -srp "Please provide the corresponding API Secret: " apisecret
printf "\n\n"

# Chrome Web Store
read -rp "Please provide your Chrome Web Store Publish API client ID: " client_id
read -srp "Please provide your Chrome Web Store Publish API client secret: " client_secret
printf "\n"
read -srp "Please provide your Chrome Web Store Publish API refresh token: " refresh_token
printf "\n\n"
fi

yarn web-ext sign \
--api-key "$apikey" \
--api-secret "$apisecret" \
--source-dir ./dist/firefox \
--channel=listed \
--upload-source-code="release-artifacts/tickety_tick-${tag##v}.zip" \
--artifacts-dir="./release-artifacts"

printf "\n\n"

gh release upload \
"$tag" \
"release-artifacts/tickety_tick-${tag##v}.xpi"

printf "\n\n"

yarn chrome-webstore-upload upload \
--client-id "$client_id" \
--client-secret "$client_secret" \
--refresh-token "$refresh_token" \
--extension-id "ciakolhgmfijpjbpcofoalfjiladihbg" \
--auto-publish \
--source ./dist/chrome.zip

cat <<EOS
Edit the auto-generated release notes on GitHub as necessary:
https://github.com/bitcrowd/tickety-tick/releases/tag/$tag
EOS

First weʼre again making some pre-checks:

./script/check-release-dependencies

branch=$(git branch --show-current)

if [[ "$branch" != "main" ]]; then
abort "Please only release from the main branch."
fi

yarn checks

This ensures all required dependencies are installed, that the developer running the script is on the main branch and that all of our static code checks in yarn checks (unit tests, linters, etc.) pass.

Now weʼre manually creating a Git tag for the version we previously set:

version=$(script/version)
tag="v$version"

git tag "$tag"
git push origin "$tag"

Weʼre using a small helper script to read out the version number from our package.json file with NodeJS:

#!/usr/bin/env node

const { version } = require("../package.json");

console.log(version);

And then we create the Git tag prefixed with a “v” (just our internal convention) and push it.

Next weʼre building the releases for Firefox and Chrome locally on our machine:

# Build artifacts.
yarn bundle:chrome
yarn bundle:firefox

The results are written to the projectʼs /dist directory.

Then weʼre ready to create the release on GitHub:

# Create a GitHub release, upload build artifacts.
gh release create "$tag" \
--generate-notes \
--verify-tag \
./dist/*.zip

Weʼre uploading the previously created builds as artifacts to the release. Note the --generate-notes flag which instructs GitHub to automatically draft release notes from the commit history and the --verify-tag flag which will abort if the matching Git tag for the version is not present on the remote.

GitHub automatically creates a Zip archive of the current state of the repo and this is what weʼre downloading next:

gh release download "$tag"\
--archive=zip \
--clobber \
--output="release-artifacts/tickety_tick-${tag##v}.zip"
Submit new versions

With that we have everything together to submit our new release to the Chrome Webstore and Firefox Add-ons. These steps require private API credentials which we store in the teamʼs own 1Password password manager. Another tool which comes in super handy here is 1Passwordsʼs CLI op. It allows save access to items in the password manager with just the main password or fingerprint, without requiring the developers to copy and paste to their terminal. For scenarios where developers are using different tooling, our release script still allows for manual input of API credentials, but the preferred and more straightforward way to collect all secrets is op:

if has op; then
apikey=$(op item get "bgi26drb3naqhipfmt5wth6634" --field key)
apisecret=$(op item get "bgi26drb3naqhipfmt5wth6634" --reveal --field secret)
# …
else
# Mozilla Add-Ons
read -rp "Please provide your Mozilla API Key: " apikey
read -srp "Please provide the corresponding API Secret: " apisecret
printf "\n\n"
# …
fi

Then we start with uploading to Firefox Add-ons via Mozillaʼs own web-ext tool. It includes a sign command to interact with the Add-ons submission API:

yarn web-ext sign \
--api-key "$apikey" \
--api-secret "$apisecret" \
--source-dir ./dist/firefox \
--channel=listed \
--upload-source-code="release-artifacts/tickety_tick-${tag##v}.zip" \
--artifacts-dir="./release-artifacts"

Weʼre uploading the previously bundled extension from the /dist directory and provide the source code archive we downloaded from the GitHub release. This speeds up the review process and spares extra emails and questions from Mozillaʼs reviewers. The other important flags here are --channel=listed, which makes sure the new version is publicly available on Add-ons and --artifacts-dir which specifies where the signed web extension from Mozilla should be downloaded to. This signed extension file is what people can use to manually install the extension in Firefox without the Add-ons platform.

The signing process may take up to a couple of minutes (it has some nice ASCII progress animations…) and gives us a .xpi file with the signed extension when itʼs done. We add the file to our release artifacts on GitHub:

gh release upload \
"$tag" \
"release-artifacts/tickety_tick-${tag##v}.xpi"

Then we upload to the Chrome Web Store, using chrome-webstore-upload-cli, a community package API wrapper:

yarn chrome-webstore-upload upload \
--client-id "$client_id" \
--client-secret "$client_secret" \
--refresh-token "$refresh_token" \
--extension-id "ciao-some-id" \
--auto-publish \
--source ./dist/chrome.zip

With that, all thatʼs left to do is double checking the release notes on GitHub and weʼre done 🎉.

Benefits & Outlook

This modular set of scripts has proved itself useful for us, for both streamlining the release process as well as documenting and maintaining it on the long run. When something was not working as expected with one of the steps, the developers could comment out the non-relevant parts of the very descriptive script debug the failing steps.

Altering or replacing individual tools or steps also worked quite well, as the script is written in a descriptive, well documented and iterative way.

Sure, there are more sophisticated ways to release a browser extension and maybe some day weʼll also see the need for something else for Tickety-Tick. But for now, with us only doing a couple of releases a year, this set of scripts, deploying like itʼs 1999 is just the right balance of automation and human interaction for us. Sometimes a “low-tech” solution is just good enough™ for a certain team and project.

Max Mulatz

Max Mulatz

Whitespace wrestler on a functional fieldtrip

We’re hiring

Work with our great team, apply for one of the open positions at bitcrowd