Versioning and releasing larger Chrome extensions
An approach to plan and implement a robust versioning scheme and release schedule – without Semver
Abstract
I’ve been developing Chrome extensions for the last few years, and whilst most don’t need a lengthy development phase, if you want to develop alpha
or beta
versions, the limitations of Chrome’s manifest version (opens new window) (in comparison to Semver (opens new window)’s more flexible schema) can trip you up.
This article is somewhat of a retrospective of my journey through poorly-executed versioning, awkward naming, tagging and releases, to final robust versioning strategy whilst developing Control Space (opens new window).
Chrome extension version format
Intro
The version string (opens new window) in Chrome extensions may contain 1 to 4 integers. Most commonly this might be:
major
major.minor
major.minor.patch
major.minor.patch.build
It cannot contain text so indicating -alpha
or -beta
releases is problematic.
As a fallback, you can use a “version name” but that doesn’t fix the numerical conundrum:
{
"version": "1.0.4.0",
"version_name": "Control Space 1.0 (Beta 4)"
}
For those unfamiliar with the build
unit, it is intended to be a number that is incremented each time there is a build. Ideally it is set in CI and is used to reference a snapshot of a particular state of the code, no matter the version.
Prerelease numbering
Numbering pre-releases in Chrome can leave you high and dry if you plan badly, or misjudge your launch timeline.
If you make the mistake of setting 1.0
too early and have published on the Chrome Web Store, you can’t replace it with a revised, lower version because the Web Store won’t accept it.
As such, if your aim is a neat-and-tidy 1.0
release (more on this later) you’re going to have to stick to 0.x.y
versions. If you screw up and go to 1.0
too early you then may need to hack the patch
or build
units (like I did for too long):
Semver | Chrome | Chrome (hack) | Work |
---|---|---|---|
0.0.0 | 0.0.0.1 | Initial commit | |
0.1.0 | 0.1.0.2 | Prerelease work | |
1.0.0-alpha | 0.1.1.3 | 1.0.0.3 | Begin work on Version 1 |
1.0.0-beta | 0.1.2.4 | 1.0.1.4 | |
1.0.0-rc | 0.1.3.5 | 1.0.2.5 | |
1.0.0 | 1.0.0.6 | 1.0.3.6 | Release Version 1 |
The problems of thinking in Semver
If you’re thinking in terms of Semver, the lack of prerelease labels has drawbacks:
- you can’t identify “beta” versions i.e.
1.0.0-beta-05
- you can’t move from
alpha
tobeta
orrc
as the format doesn’t support it - you may have to abuse
minor
,patch
orbuild
units to achieve your goals
Additionally:
- version strings for new features may always be out of date:
- let’s say you’ve released with manifest version
1.0.0
- you start working and committing code for the
2.0.0
version - as there’s no way to specify
2.0.0-beta
the manifest states1.0.0
although the code is technically2.0
-ish
- let’s say you’ve released with manifest version
- Git tags are ambiguous:
- when I was thinking in terms of a protracted “beta” phase, I used a mix of Chrome
x.y.z.0
manifest versions and SemvervX.Y.Z.beta-N
Git tags, so up until some theoretical1.1.0
release, the two would never align
- when I was thinking in terms of a protracted “beta” phase, I used a mix of Chrome
- it’s confusing for everyone:
- tagging releases, generating release notes and creating zips was an ongoing headache as there was no clear system on how the incompatible formats were supposed to marry-up
Versioning for product releases
Where versioning matters
To give some context to the rest of the article, let’s review the all versioning touchpoints.
There are various terms and values to keep track of:
- manifest
version
, e.g.1.0.0.n
- potential manifest
version_name
, e.g.Control Space 1.0
- Git
tags
, e.g.v1.0.0
Which may be used in the following places:
- Chrome extension manifest
- local build scripts
- zipped extension files (sent to testers or uploaded to the store)
- one or more unzipped extension folders used for local browser testing
- changelog
- release notes
- GitHub issues
- marketing communications
- Chrome web store page
- extension options page
- support tickets
As you can see, it’s critical to settle on a straightforward and robust system that needs no additional interpretation.
Stepping outside the Semver box
I spent a good few days researching, strategizing and testing ways to move to a new versioning paradigm and build pipeline, and ultimately realised that I had boxed myself in thinking in Semver terms in a Chrome Versioned world.
The epiphany was realising that focusing on major
and beta
monikers (aka “breaking changes” and “pre-releases”) was missing the point as I was building a product not a library; fixating on how to satisfy these library-oriented constraints was the cause of my product-versioning woes:
there isn’t really a concept of a “breaking change" in most products:
- using a
major
version bump as a signal to “review before moving forwards” means nothing to users - factoring-out the use of
major
checkpoints misses an opportunity to communicate additional information
- using a
the idea of “alpha” and “beta” doesn’t make sense; builds are either:
- private, i.e. a zip file with testers
- public, i.e. the extension on the web store
juggling named versions and
.beta-nn
monikers is confusing:- for end users it’s the
major
version that matters, i.e. “do I have the newest features?” - for beta testers it’s the
build
, i.e. “is there something for me to review?”
- for end users it’s the
The other big issue was that being in (air-quotes) “beta” for so long, there was no incentive to publicly release until I felt the product was (air-quotes) “one-dot-oh” ready. What I should have done was concentrate on regular stable releases, and communicated properly what each release was about.
Bringing it all together
Versioning scheme
Given the above, I concluded:
- counting down to
1.0
(i.e.alpha
,beta
) serves no practical purpose - there is no such thing as a
beta-nn
, there are just “sprints” and releases major
versions would be better-used to represent “product goals”, rather than “breaking changes”- without
major
version constraints,minor
versions (i.e. features) have more meaning within the release - features will be released only when it makes sense, so an initial release could be
x.5.2
(vs an alignedx.0.0
) - there is no requirement to consider
beta
private andn.0.0
public; it’s either with testers or it’s published - the
build
unit identifies which release is the most recent, no matter what the other units - using a manifest
version_name
offers no real value – and worse – hides the realversion
string
So for a project like Control Space (opens new window) how does that translate?
Unit | Purpose | Example |
---|---|---|
major | Sprint | Improve window management |
minor | Feature | Add context menu to window header |
patch | Fix (on main ) | Fix Settings panel CSS bug |
build | PRs / test builds | Send build to testers to try out new context menu |
I decided I wanted to move to this new versioning scheme immediately:
- what had been
1.0.16.0
(1.0 Beta 16
) would jump to16.0.0
- individual features would now have proper
minor
homes in16.1
,16.2
, etc - fixes could now be properly recognised with
patch
releases, i.e.15.0.1
- the ever-incrementing
build
identifier would remove ambiguity from Web Store submissions
Branching strategy
The reality of moving away from a Beta-oriented (i.e. private) workflow to more regular public releases and product sprint cycles has made me decide to switch from trunk-based (opens new window) development to Gitflow (opens new window).
Though it might be seen as unfashionable in the era of CI, I think this should allow me to patch the currently-released major version more easily; main
is the released version (15.x
) whilst develop
is the current sprint (16.x
).
I’m no Git expert but as there is only me on the project, I don’t foresee this posing any problems.
Release strategy
Being in a never-ending Beta means that often, projects don’t end; it’s just a constant feeding of issues through the pipe, piling yet more tickets into a hungry GitHub Projects “Todo” column.
Moving forwards, I think it should be much easier to plan sprints and identify what goes in and what gets left.
This should hopefully result in clearer product releases where a single feature-set is executed on, and the communication and marketing around that should be simpler as well, for example:
August 2023
Control Space – Sprint 16
Polished setup, help and first-use experience ready for public release
Updating versions
Updating the major version
As part of trying to tally a version with a sprint, I am experimenting with updating the major
version at the start of the sprint rather than the release of the feature.
Taking the “Improve window management” project example above, I might update the manifest from 15.1.0
to 16.0.0
as soon as I create the branch. This means that features should at least show the correct major
version (aligning to the current sprint) rather than being linked to 15.1.0
as they might be otherwise.
Additionally, as there’s no longer a requirement to plot intercept courses to x.0.0
versions, features will be released as it makes sense to release them, so the first public release for a sprint branch may be something like 16.3.0
, with additional minor
releases / features following as they are completed.
Updating the build version
For clarity up until now I have omitted the build
version, but in this new paradigm, we might start with:
16.0.0.123 (number of closed issues)
I’ve created a script to bump version units, which I can run manually as I commit and push features, as well as hook it up to a GitHub Action which bumps the build
each time a PR is merged.
Additionally, I could run multiple local builds for testers, so the final versions for the 16.x
builds might look like:
16.0.0.123
16.1.0.124
16.2.0.125
16.3.0.126
16.3.0.127
16.3.0.128
Even though only the final 16.3.0.n
version might be merged.
Fixing bugs and updating the patch version
Fixing bugs should be fairly straightforward by branching off main
.
The main thing to check is that build numbers between branches are reconciled:
- when committing the fix in
main
, ensurebuild
matches the latest infeature
- when merging the fix to
feature
, ensure the latestbuild
is used
Though, if you forget it doesn’t matter as the overall version strings should always be different.
Note that the versioning for a theoretical CSS fix in the live 15.x
branch bug might go like this:
Branch | Version | Work | Change |
---|---|---|---|
main | 15.0.0.123 | Feature 15 is live | - |
feature | 16.0.0.123 | Start “Window Management” project | major |
feature | 16.1.0.124 | Add “Context menu” feature | minor build |
main | 15.0.1.125 | Fix some CSS issue (update build to latest) | patch build and release |
feature | 16.1.0.125 | Pull in the fix (ensure latest build ) | - |
feature | 16.2.0.126 | Add feature | minor build |
feature | 16.2.0.127 | Ready to release | build and release |
The final releases would be:
15.0.0.123
onmain
15.0.1.125
which adds the CSS fix16.2.1.127
which adds the the16.x
branch along with the fix
And the incrementing build
should ensure identical versions are never uploaded to the Chrome Web Store!
Changelogs vs release notes
In recent years I’ve switched from changelogs (opens new window) to release notes (opens new window).
Changelogs are good for developers but no good for customers or marketing as they are too technical and granular. A user doesn’t care that you “refactored some component” they care that you “added support for context menus”.
And creating release notes at the end of a sprint from changelog.md
is hard, as you’ve forgotten the context a month or so later. I prefer to just update the release notes for the sprint as I commit the feature:
Windows
- added support for context menus
- made shortcut keys combos clearer
- …
If and when the notes make it into your public-facing site, they are much easier to develop in this format. Many products, such as Chrome (opens new window) and Chrome Dev Tools (opens new window) use their blog to provide additional context.
Also, for developers, GitHub already provides a neat log of changes (opens new window) between releases by comparing commits (opens new window).
Summary
To summarise:
- Think
sprint.feature.fix.build
vsmajor.minor.patch.build
major
versions signify the start (rather than the end) of a block of product featuresminor
versions count from the currentmajor
versionminor
versions are released as and when they make sense- Work is beta by definition until published
- Use
build
versions to identify changes in code more granular than the feature level - Create
patch
releases as needed
Note that this approach may not suit you and your project (and you should do what suits you) but is something I shall be moving forwards with, for now.
I’ll be sure to update this post if anything changes or I notice any shortcomings!
Addendum
How do other extensions handle versioning?
Looking at my Extensions page, I was interested to see which extensions had larger major
build numbers.
Creating a new profile, I installed around 20 of the most popular extensions, and ordered by version
string:
Extension | Users | Size | Builder | Major | Minor | Patch | Build |
---|---|---|---|---|---|---|---|
Lighthouse (opens new window) | 1m | 27 KB | Bundler | 100 | 0 | 0 | 0 |
Honey (opens new window) | 10m+ | 4.5 MB | Compiled | 16 | 1 | 2 | |
Grammarly (opens new window) | 10m+ | 37 MB | Webpack | 14 | 1118 | 0 | |
Todoist (opens new window) | 700K | 85 KB | - | 11 | 1 | ||
Octotree (opens new window) | 300K | 3.4 MB | Bundler | 7 | 9 | 3 | |
Loom (opens new window) | 5m | 13 MB | Webpack | 5 | 5 | 26 | |
Google Keep (opens new window) | 6m | 4 MB | Closure | 4 | 23312 | 540 | 1 |
LastPass (opens new window) | 10m+ | 55 MB | Webpack | 4 | 119 | 0 | |
Tampermonkey (opens new window) | 10m+ | 1.5 MB | Bundler | 4 | 19 | 0 | |
Dark Reader (opens new window) | 5m | 600 KB | - | 4 | 9 | 64 | |
Save to Pocket (opens new window) | 2m | 356 KB | Bundler | 4 | 0 | 6 | |
Save to Google Drive (opens new window) | 3m | 650 KB | Closure | 3 | 0 | 4 | |
Momentum (opens new window) | 3m | 14MB | Bundler | 2 | 10 | 0 | |
Google Translate (opens new window) | 10m+ | 236 KB | Closure | 2 | 0 | 13 | |
OneTab (opens new window) | 2m | 1.2 MB | Bundler | 1 | 76 | ||
Google Docs (opens new window) | 10m+ | 88 KB | Closure | 1 | 65 | 0 | |
Zoom (opens new window) | 7m | 240 KB | Bundler | 1 | 8 | 20 |
I’m not sure how conclusive this is, but perhaps:
- some developers might reconsider their versioning scheme
- some projects use
major.build.minor
(Google Keep)
How do other developers handle versioning?
I posted this article on the Chromium Extensions (opens new window) channel and got some great input:
Oliver Dunk (opens new window) (Extensions DevRel at Google) notes leading zeros in versions are stripped; something to be aware of
Erek Speed (opens new window) implements
major.minor.patch
using Semantic Release (opens new window) to automate tagging and releases (opens new window)Gaurang Tandon (opens new window) suggests another modified versioning scheme:
minor
for public releases,patch
for beta releases andbuild
for internal releases
How does Chrome handle versioning?
Chrome is built upon the Chromium project, which has a very specific (opens new window) release and channel lifecycle.
Their versioning scheme works like this:
major.minor.build.patch
The web page (opens new window) explains it like this:
MAJOR and MINOR track updates to the Google Chrome stable channel. In this sense, they reflect a scheduling or marketing decision rather than anything about the code itself. These numbers are generally only significant for tracking milestones. In the event that we get a significant release vehicle for Chromium code other than Google Chrome, we can revisit the versioning scheme.
The BUILD and PATCH numbers together are the canonical representation of what code is in a given release. The BUILD number is always increasing as the source code trunk advances, so build 180 is always newer code than build 177. The PATCH number is always increasing for a given BUILD. Developers and testers generally refer to an instance of the product (Chromium or Google Chrome) as BUILD.PATCH. It is the shortest unambiguous name for a build.
For example, the 154 branch was originally released as 0.3.154.9, but now stands at 1.0.154.65. It’s the same basic code with a lot of bug fixes applied. The fact that it went from a Beta release to several 1.0 stable releases just reflects the decision to call some version (1.0.154.36) ‘out of Beta’.
Note that (as of this article) the public stable version Chrome (opens new window) is 115 and the latest Chromium branch (opens new window) is 117.
Resources
Finally, links to the resources which helped me reach a conclusion on my own versioning strategy:
- What exactly is the build number in MAJOR.MINOR.BUILDNUMBER.REVISION (opens new window)
- When do you change your major/minor/patch version number? (opens new window)
- How/When should I update the version number? (opens new window)
- When should I increment version number? (opens new window)
- When to increment build number? (opens new window)
- How often should you ship your product releases (opens new window)
- Semantic Versioning with CI/CD and semantic-release (opens new window)