This document describes how Rocket.Chat Desktop moves code from a merged PR
to a published release, for every channel: alpha, stable, and patch. It
replaces the former docs/alpha-release-process.md and
docs/pre-release-process.md.
For the conceptual overview, see development-and-release-flow.md.
| Event | Where | Mechanics |
|---|---|---|
| Feature/fix PR | → dev (default branch) |
squash-merge |
Alpha X.Y.0-alpha.N |
on dev |
bump PR on dev → tag the dev tip via yarn release:tag |
Stable X.Y.0 |
dev → master |
bump PR on dev first, then a release PR dev→master merged with a true merge commit (gh pr merge --merge, never squash), tag the merge commit |
Patch X.Y.Z |
release/X.Y.x |
branch cut from the stable tag X.Y.0. Fixes land on dev first and are cherry-picked down. Bump and tag on the release branch. Then merge the release branch into master with a true merge commit |
| Back-merges | never (one exception below) | master and release/X.Y.x never merge into dev |
Do not use release/<version>-alpha.N (or any alpha) branch naming.
release/ is reserved exclusively for patch lines (release/4.16.x, etc.).
Alpha work stays on dev and has no long-lived branch of its own.
| Channel | Version Format | Who Receives It |
|---|---|---|
| Stable | 4.12.0 |
All users (default) |
| Beta | 4.12.0-beta.1 |
Beta opt-in users |
| Alpha | 4.12.0-alpha.1 |
Alpha opt-in users |
Every release (stable, beta, or alpha) uploads the same update metadata
(latest.yml, latest-mac.yml, latest-linux.yml). There is no
per-channel .yml filename. Two things separate the channels:
- Whether the GitHub release is marked Pre-release (true for alpha/beta, false for stable).
- Whether the client has opted into prereleases. electron-updater's
allowPrereleaseis enabled for the alpha/beta channel settings and disabled for stable.
The updater of a stable client ignores prerelease-flagged releases, even
though they share the same latest*.yml file names.
Channel hierarchy: Alpha users receive alpha, beta, AND stable updates. Beta users receive beta AND stable. Stable users receive only stable.
package.json's "version" on dev always equals the newest tag cut from
dev's line:
- Right after the promotion of
4.16.0tomaster,devstays at4.16.0. The bump PR changed it to4.16.0before the promotion PR. - The first alpha of the next cycle bumps
devstraight to4.17.0-alpha.1. Start numbering at.1and never use a phantom.0. There is no bare4.17.0tag until that cycle stabilizes.
Create every release tag with yarn release:tag. NEVER use a hand-rolled
git tag and git push. The script (scripts/release-tag.ts) does these
things:
- It reads the version from
package.json. - It fetches the refs that the version's channel allows.
- It fails closed (exit 1) on every unsafe case.
- It tags HEAD as the bare version and pushes the tag.
A tag that you create by hand skips all of these steps.
The script detects the channel (stable, alpha, beta, or candidate) from the version. It compares a version only with the tags of its own channel. An alpha never blocks a stable, and a stable never blocks an alpha.
The allowed remote refs depend on the channel:
- A prerelease tag (alpha, beta, or rc: any version with a prerelease id)
needs HEAD to be an ancestor of
origin/devor of anyorigin/release/*branch. - A stable tag (no prerelease id) needs HEAD to be an ancestor of
origin/masteror of anyorigin/release/*branch.
The script finds the origin/release/* branches at run time with
git ls-remote --heads origin 'release/*'.
| Guard | Override |
|---|---|
Invalid semver in package.json |
none |
| HEAD not an ancestor of an allowed ref for its channel | --allow-unverified-ref |
| Tag already exists | none — not even --force |
| Version not greater than latest tag in its channel | --force |
If a guard fires, treat it as a real finding. Report it and fix the cause.
Use --force or --allow-unverified-ref only when the release owner agrees
explicitly.
--yes/-yskips the confirmation prompt. The script also skips the prompt automatically whenCI=true.--helplists all flags.- Without
--yes, the prompt reads a real TTY. Pipingecho yis unreliable, so use the flag.
Tag the squashed merge or bump commit on the branch that the channel expects:
- Prereleases:
dev. - Stables:
master, or arelease/X.Y.xbranch for a patch release.
NEVER tag a pre-merge bump commit on an unmerged branch. That ships the wrong tree. The ref-ancestor guard enforces this rule. If the guard fires, fix the checkout and do not override the guard.
The tag name is the bare version (4.16.0, no v prefix).
build-release.yml triggers on any tag push. The auto-updater feed derives
from the package.json version, which MUST match the tag.
A fresh worktree has no node_modules. The script then fails with
Couldn't find the node_modules state file (findPackageLocation). Run
yarn install in the worktree. NEVER work around the error by tagging by
hand.
The pure tag and channel logic is in scripts/releaseTag.lib.ts. The unit
tests are in scripts/releaseTag.lib.spec.ts. release-tag.ts keeps the
I/O. Script specs run under their own Jest project (testEnvironment: 'node').
The ship-release skill drives the full end-to-end flow: notes, bump PR,
merge, tag, CI, asset matrix, and Jira release sync.
Cut alphas directly from dev. There is no dedicated branch.
-
Update the version on
dev:git checkout dev git pull origin dev git checkout -b chore/release-4.17.0-alpha.1
Edit
package.json:{ "version": "4.17.0-alpha.1" }Also increment
mac.bundleVersioninelectron-builder.json. It is independent frompackage.json'sversion. Apple requires each submission'sCFBundleVersionto strictly increase over the last one. The format isYYMMplus a single-digit build counter. The counter resets to0at the start of each month. For example, the first build shipped in August 2026 is26080, and the second same-month build is26081. Check the current value and the date of its last bump (git log -p --follow -- electron-builder.json) before you increment it. NEVER guess an arbitrary increment. -
Commit and open a PR to
dev:git add package.json electron-builder.json git commit -m "chore: bump version to 4.17.0-alpha.1" git push origin chore/release-4.17.0-alpha.1Squash-merge the PR into
dev. -
Tag the
devtip:git checkout dev && git pull origin dev yarn release:tagThe guard checks that the commit of an alpha tag is an ancestor of
origin/devor of arelease/*branch. See "Tagging withyarn release:tag" for all guards. -
CI builds automatically. A semver tag push is the only trigger for a publishable release build (
build-release.ymldoes not run on branch pushes). A manualworkflow_dispatchrun is a dry run from a branch and does not publish a release.- A
preparejob creates the draft GitHub release for the tag. - Then seven packaging jobs run in parallel and upload into the draft.
The jobs are
windows-nsis,windows-msi,windows-appx,macos-dmg(dmg + zip + pkg, universal, notarized),macos-mas,linux-appimage(AppImage + deb + rpm + tar.gz) andlinux-snap. Thelinux-snapjob also publishes to the Snapcraft channel for the version. - Exactly one job per platform uploads the electron-updater metadata:
latest.ymlfromwindows-nsis,latest-mac.ymlfrommacos-dmg, andlatest-linux.ymlfromlinux-appimage. The metadata lists the files that the job built, so a second uploader would replace it with a partial list. - The draft is marked Pre-release for alpha, beta, and rc tags. That flag, and not the metadata file name, keeps the build away from stable clients.
- Lint and the Jest suite do not run here.
yarn release:tagaccepts only a HEAD that is already ondev,masteror arelease/*branch.validate-pr.ymlgates all of these branches.
Dry run without a tag.
build-release.ymlalso acceptsworkflow_dispatch, which is always a dry run. Run it from any branch (gh workflow run build-release.yml --ref <branch>). Every packaging job builds and signs its targets exactly as a tag push would. It skips GitHub releases and Snapcraft, and uploads itsdist/as adry-run-<job>workflow artifact. Use it to exercise a workflow or release-action change before you trust the change with a real tag. - A
-
Publish the release. Open the draft on GitHub Releases, review the notes, and click "Publish release". No client sees the release until a human publishes it.
-
Bump PR on
devfirst. Drop the pre-release suffix inpackage.json(e.g.4.17.0-alpha.6→4.17.0). Open a PR that targetsdevand squash-merge it. This keeps the version invariant intact. It also ensures thatmasternever carries a version thatdevhas not already reached. -
Release PR
dev→master. Open a PR fromdevintomaster. Merge it withgh pr merge --merge, a true merge commit. NEVER squash it. A squash of a release PR forks history permanently.masterwould stop being a subset ofdev's commit graph, and every future diff between the branches becomes unreadable.If the release PR has a conflict, do NOT resolve it in the GitHub UI. The UI resolves a conflict by a merge of
masterintodev, which is a back-merge. After a patch release,masterhas a version bump thatdevdoes not have, sopackage.jsonconflicts. Resolve it on a promotion branch:git switch -c promote/4.17.0 origin/dev git merge origin/master git checkout --ours -- package.json # keep the version from dev git add package.json git commit git diff origin/dev # read every remaining difference git push origin promote/4.17.0
Every remaining difference from
devmust be a change that is onmasteronly on purpose. If a fix shows, someone wrote a hotfix on a release branch and did not forward-port it. Forward-port it todevfirst. Then open the release PR frompromote/X.Y.0intomaster, and merge it with a true merge commit. -
Tag the merge commit on
masterwithyarn release:tag. The channel-aware guard checks that a stable tag points at a commit that is an ancestor oforigin/masteror arelease/*branch. -
CI builds and drafts the release exactly as in the alpha flow. Publish after you check the assets.
-
Make sure that the patch line branch exists. Cut it from the stable tag that it patches:
git checkout -b release/4.16.x 4.16.0 git push origin release/4.16.x
If the branch already exists (an earlier patch), skip this step.
-
Write and merge the fixes on
devfirst. Then cherry-pick them onto the release branch:git checkout release/4.16.x git cherry-pick <fix-commit-sha>
-
The bump PR targets the release branch (
package.json→4.16.1). Review and merge it there. -
Tag on the release branch with
yarn release:tag. The guard accepts a stable or patch tag whose commit is an ancestor of anyorigin/release/*branch, in addition toorigin/master. -
CI builds and drafts the release the same way. Publish after you check the assets.
-
Merge the release branch into
master. After you publish, open a PR fromrelease/X.Y.xintomaster. Merge it withgh pr merge --merge, a true merge commit. NEVER squash it.masterthen always holds the latest stable release.Do this only for the patch line of the newest stable minor. A patch for an older line stays on its release branch, for example
4.16.2after4.17.0shipped.mastermust not go back to an older version.
master never merges back into dev. A release/X.Y.x branch never merges
back into dev either. Two practices make sure that dev already has every
change that master gets, so that nothing on master needs to flow back:
- Bump
devbefore the promotion tomaster. - Cherry-pick fixes down to patch lines from
dev, and never the reverse.
A patch release merges its release branch into master, so master gets
the patch version bump, which dev never has. This is the expected
difference between the branches. The next promotion resolves it on a
promotion branch (see "Promoting to a stable release").
Single exception: you may author a hotfix directly on a release/X.Y.x
branch, and not on dev with a cherry-pick down. You MUST forward-port such
a hotfix to dev immediately, with a small cherry-pick PR. Otherwise the
next promotion of dev to master silently loses the fix.
- Open the Help menu and enable the Developer Mode checkbox.
- Open App settings (
Ctrl+,, orCmd+,on macOS). - Go to the Advanced section. An Update channel dropdown appears when Developer Mode is on and updates are enabled for the build.
- Select the desired channel:
- Stable - Production releases only
- Beta - Beta and stable releases
- Alpha (Experimental) - Alpha, beta, and stable releases
- Go to the General section and click Check for updates now.
The app persists the setting automatically. The setting survives app restarts.
Create update.json in the user data directory:
| Platform | Location |
|---|---|
| Windows | %APPDATA%\Rocket.Chat\update.json |
| macOS | ~/Library/Application Support/Rocket.Chat/update.json |
| Linux | ~/.config/Rocket.Chat/update.json |
Content for alpha channel:
{
"channel": "alpha"
}Content for beta channel:
{
"channel": "beta"
}For enterprise deployments where you want to force the setting (users cannot change it):
{
"channel": "beta",
"forced": true
}- Enable Developer Mode in the Help menu.
- Open App settings and go to the Advanced section.
- Select the desired channel from the Update channel dropdown.
- Click "Check for updates now" in the General section. The app offers the next pre-release version.
- Open App settings and go to the Advanced section.
- Select "Stable" from the Update channel dropdown.
- Click "Check for updates now" in the General section.
Important: Switching to stable does NOT automatically downgrade the app. This is what happens:
- You are on
4.12.0-alpha.2and switch to the stable channel. You receive the next stable release (e.g.,4.12.0). Semver considers4.12.0greater than4.12.0-alpha.2, so the app offers it as an update. You do not receive further alpha/beta releases until you switch back. - To downgrade immediately, uninstall the current version. Then download and install the stable version from GitHub releases.
- Alpha:
4.12.0-alpha.1,4.12.0-alpha.2, etc. - Beta:
4.12.0-beta.1,4.12.0-beta.2, etc. - Stable:
4.12.0 - Patch:
4.12.1,4.12.2, etc., onrelease/4.12.x.
Typical release progression:
4.12.0-alpha.1 → 4.12.0-alpha.2 → 4.12.0-beta.1 → 4.12.0-beta.2 → 4.12.0 → 4.12.1
- Stable users never see alpha/beta releases. Their updater ignores prerelease-flagged releases.
- Users must enable Developer Mode explicitly and select the alpha/beta channel.
- Alpha and beta releases are marked as "Pre-release" on GitHub.
- A semver tag push (never a branch push) creates a draft release, and a
human reviews and publishes it explicitly. No client receives a release
before that. A manual
workflow_dispatchrun is a dry run and publishes nothing. - The app persists the channel selection, and it survives restarts.
- Users can switch channels at any time in App settings.
- Check that the release is published (not draft) on GitHub.
- Check that
latest.yml,latest-mac.yml, andlatest-linux.ymlexist in the release assets. Check that the Pre-release flag of the release matches the intended channel (checked for alpha/beta, unchecked for stable). - Click "Check for updates now" in the General section of App settings.
- Restart the app and try again.
- Make sure that Developer Mode is enabled in the Help menu.
- Close and reopen App settings.
- Restart the app completely.
With Developer Mode enabled, open App settings. The Advanced section shows the current channel in the Update channel dropdown.
The app stores the channel preference in its config file:
- Windows:
%APPDATA%\Rocket.Chat\config.json - macOS:
~/Library/Application Support/Rocket.Chat/config.json - Linux:
~/.config/Rocket.Chat/config.json
Look for the updateChannel key (values: latest, beta, or alpha).