Release Process
How an XChain Platform release is cut, signed and published, and how an urgent fix reaches users without waiting for the next one.
This is the runbook. It assumes you are the person driving the release.
The shape of it
XChain Platform ships as a release train: nine components that move together under one version number, so that “XChain 0.9.0” names an exact, reproducible set of software rather than a rough era.
| Train members | xchain-vm, xchain-decoder, xchain-indexer, xchain-hub, xchain-sync, xchain-node, xchain-encoder, xchain-utxo-tracker, xchain-explorer |
| Version scheme | one stream, MAJOR.MINOR.PATCH, shared by every member |
| Where work lands | the develop branch of each repo |
| Where releases live | the master branch, which only ever receives release merges |
Sparse lockstep
A component is tagged only for the trains it actually changes in. A gap in a component’s tags means “unchanged that release”, not “missed a release”.
Resolving a version: XChain X.Y.Z means each component at its highest tag less than or equal to X.Y.Z. You never have to work that out by hand, because every release records the resolved set explicitly (see the manifest below).
xchain-node is the exception: it ships in every train, because it carries
the manifest, so its content genuinely changes each time.
What a release publishes
Every train release publishes, on the xchain-node GitHub Release:
| Asset | What it is |
|---|---|
release-manifest.json |
every component’s exact tag and commit for this release |
| one source tarball per changed component | the release content itself |
SHA256SUMS |
digests of everything above |
SHA256SUMS.asc |
signature over SHA256SUMS |
Signing uses the XChain Platform release key. Its fingerprint and the commands to check it are on Release Signing.
Train releases are never marked as GitHub pre-releases, including the 0.9.x
testnet series. The “latest release” endpoint skips pre-releases, so flagging
them would break xchain-node install for everyone. Testnet status is
communicated by the version number and the release notes.
Cutting a release
Order matters here, and one part of it is easy to get backwards: the other
components merge and tag first, and xchain-node is cut last. The manifest
records the commit each tag points at, and those commits do not exist until the
merges have happened.
1. Freeze
Decide what is in the train. In each repo that changed, create a release branch
from the current develop:
git checkout develop && git pull
git checkout -b release/v0.9.0
git push -u origin release/v0.9.0
Cutting a branch, rather than releasing from develop directly, is what stops
ongoing work from changing the release underneath you after you have reviewed it.
Before going further, confirm on those exact commits:
- CI is green in every repo in the train.
- An end-to-end run against the frozen code is green. Use the E2E (regtest)
workflow in
xchain-nodeand give it the ref you are releasing.
Do not open a release PR while anything is red.
If you cut release branches only in the changed repos, the end-to-end run cannot install the whole stack from one ref. Either create the release branch in every train member for the duration of the freeze, or run the end-to-end check against the finished release afterwards and treat the pre-merge run as covering the changed components only.
2. Choose the version number
| Bump | When |
|---|---|
| MAJOR | anything that changes what the chain considers valid. Requires a coordinated network upgrade with an activation point. |
| MINOR | new features, behaviour changes that are not consensus-breaking. |
| PATCH | fixes. |
The one judgement call that matters. A fix that changes what state is derived from data already on the chain is a consensus change, even when it is obviously a bug fix and even when it is small. Two nodes running different versions of it compute different state. Such a change ships behind an activation point, never directly. If you are unsure whether a fix qualifies, treat it as though it does.
3. Prepare each changed component
On its release branch:
- bump
package.jsonto the train version - add a
CHANGELOG.mdentry headed## v0.9.0 (YYYY-MM-DD) - in
xchain-indexer, set the minimum hub version to the hub’s release tag
4. Open the release PRs
One PR per changed repo, release/v0.9.0 into master, titled Release v0.9.0.
The PR diff is the release.
Then check cross-component consistency. Some files are required to be byte-identical across repos, and no single repo’s CI can see its sibling, so this check only happens here.
Wait for all of them. Merge nothing until every PR in the set is green and reviewed at the same time. There is no transaction across repositories: merges are individual clicks, and the all-green checkpoint is the only thing that makes a half-merged train an anomaly rather than a routine occurrence.
5. Merge, wait for master to go green, then tag
Merge each PR (a merge commit, never squash, never rebase).
Wait for master’s CI to finish green on each merge commit before tagging it.
The checkpoint in step 4 measured the release branches. A merge commit is a
commit no CI has ever run on, and it is the one the tag names, the manifest
pins, and anyone verifying the release resolves. This is also the last point
where a problem is cheap: an untagged bad merge is a revert, while a tagged one
means burning the train (see below), because a pushed tag is the start of the
verification chain and moving it means deleting and re-pushing it.
git checkout master && git pull
git tag -s v0.9.0 -m "XChain Platform v0.9.0"
git push origin v0.9.0
-s signs the tag. An unsigned train tag is a defect, not a style preference:
the tag signature is what someone verifying the release starts from.
Then level develop with master in every repo you merged, before moving
on:
git fetch origin
git checkout develop && git merge --ff-only origin/master && git push
The release branch carries at least the version bump and the changelog entry,
and after the merge those commits exist only on master. Skip this and the
next release diff reopens work that already shipped. A fast-forward is the
normal case because the freeze window is short; if develop has moved, make it
a real merge.
6. Cut xchain-node last
On its release branch, write release-manifest.json from the tags you just
created, recording each component’s tag and the commit that tag points at.
Bump, changelog, PR, gate, merge, tag, push, exactly as above.
Then publish:
- the GitHub Release on
xchain-node, never flagged as a pre-release - the release notes: resolved component set, aggregated changelog, and for a MAJOR train the activation point
- the assets: manifest, source tarballs,
SHA256SUMS,SHA256SUMS.asc - a GitHub Release in every other repo that carries the train tag, each a short pointer at node’s Release rather than a copy of it
- the mirrored release page in the documentation
That per-repo bullet is not a formality. A repository page presents Releases as the newest published version, so a component whose tag never got one tells every visitor that the project last shipped whatever version did. Tags do not correct that impression, because most people never open the tag list. Publish one per repo and confirm it per repo: node’s Release is the one everyone checks, so checking it and assuming the rest followed is how a train ships with thirteen component repos still advertising an old version.
Build the tarballs during the ceremony rather than relying on GitHub’s auto-generated archives. Those are produced on demand and are not guaranteed to stay byte-identical, so a checksum recorded today can stop matching later with nothing having changed. Uploaded release assets are immutable.
7. Deploy
Follow dependency order: coin nodes (external), then xchain-utxo-tracker and
xchain-encoder, xchain-decoder, xchain-indexer, xchain-sync,
xchain-hub, xchain-node, xchain-explorer. See
Deployment and Upgrading.
If it goes wrong mid-train
If something turns red after you have already merged some PRs:
- Stop. Merge nothing further.
- Cut no tags for this train.
- Revert the merged PRs to restore each
master. - Burn the number. The next attempt uses the next patch version. Do not reuse the number, and never publish release notes for the burned one.
- Record the burn in the next release’s notes.
master cannot be force-pushed, so revert commits are the only way back. The
changelog noise is the cost of an honest history.
Hotfixes
For “the network is broken now and develop is not shippable”.
Classify before anything else
State-neutral (a crash, an RPC error, a display bug, anything that does not change what the chain derives): ship it as a patch train, no activation point needed.
State-affecting (it changes what is derived from data already on chain): this is a consensus change no matter how urgent it is or how small the diff looks. It ships behind an activation point, using the same machinery as a MAJOR train. A consensus fix rolled out without one re-creates, during the upgrade window, the exact split it was written to fix.
Numbering it as a patch is fine. Numbering does not decide gating; classification does.
Then
- Branch
hotfix/v0.9.1from thev0.9.0tag in each affected repo. - Fix it, with tests.
- PR into
master. Same gate as any release: CI green, reviewed, merged by a human. - Tag the patch train. Only the repos you touched get tags.
- Cut
xchain-node’s manifest release for the patch train, publish, deploy. - Merge
masterback intodevelopimmediately, in every repo you touched, so the fix is not missing from the next release.
Step 6 is the one people forget, and forgetting it silently reverts the hotfix at the next train.
Rolling back
xchain-node update can move backward within a major version.
Crossing a major boundary backward is not supported and must not be attempted. The rules changed at that boundary; going back beneath it means a node that disagrees with the network about history. If you need this, stop and treat it as an incident rather than an upgrade.
Quick reference
| Task | Command |
|---|---|
| Install the latest release | xchain-node install |
| Install an exact release | xchain-node install v0.12.3 |
| Track a branch (unreleased) | xchain-node install develop |
| Verify a tag | git tag -v v0.12.3 |
| Verify the assets | gpg --verify SHA256SUMS.asc SHA256SUMS && sha256sum -c SHA256SUMS |
install with no ref resolves the latest published release and installs every
component at the exact commit the manifest pins, verifying each one after it is
fetched. A version number always means the released code; anything else is read
as a branch name and installs an unreleased, tracking checkout.
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.