Documentation navigation
Alpha and Beta Pre-releases
Sometimes you want to build and test a change before it is merged to main — for example, to deploy a bugfix to dev and verify it against real traffic while the PR is still in review. That is what the alpha and beta branches are for. They are pre-release channels: pushing to them cuts a pre-release (e.g. v1.2.4-alpha.1), which the release workflow containerizes and can deploy to an environment like dev.
Each branch maps to a release channel:
| Branch | Channel | Tag format | Example |
|---|---|---|---|
main |
stable | vX.Y.Z |
v1.2.3 |
alpha |
pre-release | vX.Y.Z-alpha.N |
v1.2.4-alpha.1 |
beta |
pre-release | vX.Y.Z-beta.N |
v1.2.4-beta.1 |
next |
release candidate | vX.Y.Z-rc.N |
v1.3.0-rc.1 |
Versioning is driven by conventional commits via the create-release action (semantic-release under the hood). A fix: commit bumps the patch version, a feat: commit bumps the minor version. That distinction matters — see the rule below.
How to cut a pre-release
Cutting a pre-release is just a force-push of your current branch to the alpha (or beta) branch:
$ git push origin your-branch:alpha --force
The push triggers the repo's Release workflow, which:
- Runs semantic-release in dry-run mode to compute the next pre-release tag.
- Builds and pushes a container image tagged with that tag (e.g.
v1.2.4-alpha.1). - Creates the GitHub release and publishes the image to the configured environment(s).
Some repos wrap this push in a Makefile target so you don't have to remember the command:
$ make alpha-release
Force-pushing 'fix/wake-word-regression' -> origin/alpha
The exact target name varies by repo — some have alpha-pre-release (which rebases onto the remote alpha branch instead of force-pushing). Check the repo's Makefile (make help usually lists the targets).
The golden rule: only fix commits on alpha and beta
Every commit on alpha or beta must be a fix: commit. Never push feat: commits to a pre-release branch.
This isn't bureaucracy — it prevents version collisions.
First, how our create-release action picks versions (semantic-release under the hood): for the branch being released, it looks only at the tags merged into that branch (git tag --merged), takes the highest version among them, and bumps it according to the commit types since that tag. main therefore only ever counts stable tags merged into main — pre-release tags on alpha never affect what main releases. This is our policy, not a semantic-release limitation: stable releases stay correct either way.
The collision happens on the pre-release channel itself, because pre-release tags are permanent even though alpha is a throwaway branch:
- The latest stable release is
v1.2.3, and you pushfeat: faster wake-word detectiontoalpha. This cuts the tagv1.3.0-alpha.1— the1.3.0pre-release series now belongs to the alpha channel. - In review, the change gets descoped and merges to
mainsquashed into afix:commit.maincorrectly releasesv1.2.4— but nothing will ever release a stable1.3.0. The pre-release series is stranded: any further pre-release onalpha, even afix, continues it (v1.3.0-alpha.2,v1.3.0-alpha.3, …), promising a minor version that will never exist. - Sooner or later
alphagets reset to matchmain(that is whatmake alpha-releasedoes — a force-push), and the oldfeatcommit leaves the branch history. But the tagv1.3.0-alpha.1survives, because tags outlive force-pushes. - The next genuine
feat:pushed toalphacomputes its version against the stable tags now merged into the branch (v1.2.4), lands onv1.3.0-alpha.1again, and finds the tag already exists. The release fails, and you are stuck hand-deleting tags.
With fix: commits only on the channel, every pre-release is patch-level, and patch slots are cheap: when the PR eventually merges to main — with its real feat: title if it's a feature — the channel's baseline moves past any stranded pre-release tags, so no future recomputation lands on a tag that already exists. A stranded minor series, on the other hand, is exactly what the next feat: on the channel recomputes. That asymmetry is why the rule exists.
So: if your pre-release build needs a feat, don't push it as a feat to alpha. You have two options:
- Merge to
mainand release properly. The feat gets the minor-version bump it deserves (v1.3.0), and no pre-release is needed at all. This is the default. - Cut the pre-release from a
fixsquash — but keep thefeattitle on the PR. If the change genuinely must be alpha-tested before merge, squash it into a singlefix:commit and push that toalpha/beta. The PR itself keeps itsfeat:title, so when it finally merges intomain, it releases as the minor version it deserves (v1.3.0). The pre-release tag was only ever a test build —mainignores it.
What the two channels see doesn't have to match: the pre-release channel always sees fix: commits, and main sees the change's real type through the PR title. The only forbidden combination is the one above — a feat: commit on alpha or beta.
Keep the channels in sync
After anything merges to main and a stable release is cut, update alpha and beta to match main so their version history doesn't drift out of sync:
$ git push origin main:alpha --force-with-lease
$ git push origin main:beta --force-with-lease
The push needs force: alpha and beta are force-pushed throwaway branches, so their tips are usually not ancestors of main and a plain push would be rejected as non-fast-forward. --force-with-lease protects you from overwriting someone else's in-flight pre-release build — check with your team before resetting a channel that may be mid-test.
Key takeaways
alpha/betaare throwaway pre-release channels for testing fixes before they land onmain.- A pre-release is just a force-push to the channel branch (
git push origin your-branch:alpha --force); some repos wrap it inmake alpha-release. - Only
fix:commits on pre-release branches — afeatstrands a pre-release series thatmainwill never release, and its surviving tag later collides with the next recomputed version. - A
featcan still be pre-released: squash it into afix:commit for thealpha/betabuild, and keep thefeat:title on the PR so the final merge tomainreleases the minor version it deserves. - After merging to
main, pushmaintoalpha/betawith--force-with-leaseto keep the channels in sync.