Skip to content
BaseHub by wbnns Updated

Release Process

Shipping a base/base release means driving two workflows by hand and letting the rest fire on their own. You start a release, let release candidates accumulate on the branch until one looks right, then publish that version as the final tag.

The version number lives in Cargo.toml on the release branch, and almost every guard rail in the pipeline keys off it. A branch whose Cargo.toml still reads 0.0.0 has not been initialised yet, and the automation declines to build from it.

Go to Actions → Start Release → Run workflow and pick a bump type — major for breaking changes, minor for a feature release, patch for fixes.

You do not supply a version. The workflow derives the next one from the most recent final tag and cuts a releases/vX.Y.Z branch for it. Where that branch is cut from depends on the bump: a patch branches off the newest matching releases/vX.Y.* branch, while major and minor branch off main.

Creating the branch sets off Release Version Sync, which raises a PR writing the new version into Cargo.toml.

Nothing else can proceed until that PR lands. While Cargo.toml reads 0.0.0, RC builds keep declining to run. Review it and merge it into the release branch.

From here the branch builds itself. Create RC runs on every push to it and:

  • Backs out quietly when Cargo.toml is still 0.0.0, which means step 2 has not finished.
  • Works out the next number in the RC series and tags it — v0.6.0-rc.1, then v0.6.0-rc.2, and so on — against the commit that triggered the run.
  • Hands that tag to Build RC as a separate dispatch, then reports success without waiting to see what the build does.

Build RC is what actually produces the artifacts, delegating to Build Release for multi-arch Docker images and native binaries. The compatibility node image and each of the single-binary images all receive the RC tag and nothing else; latest stays where it is until a final release. Because each candidate builds inside its own run, a fresh merge no longer cancels or queues behind the artifacts of the candidate before it — runner capacity permitting, the two build side by side.

That split is worth internalising before you read a green checkmark. A passing Create RC tells you a tag was allocated and a build was requested, and nothing more. Artifact success lives in the Build RC run, so that is the one to open when you want to know whether a candidate is installable.

Need another candidate? Push another commit. Fixes and backports both re-trigger the workflow, so there is no separate command for cutting an RC.

When a candidate holds up, run Actions → Publish Release → Run workflow and type the bare version — 0.6.0, with no v and no releases/ in front of it.

Before doing anything, the workflow confirms the release branch is really there and that Cargo.toml has moved off 0.0.0. Then it:

  • Tags the release branch vX.Y.Z.
  • Builds seven images under PROFILE=maxperf, each for both amd64 and arm64: the compatibility node image plus the single-binary base, base-reth-node, base-consensus, base-builder, basectl, and base-snapshotter images.
  • Gives every one of them the same four tags: vX.Y.Z, X.Y, X, and latest.
  • Drafts a GitHub release, writes its changelog automatically, and attaches the binaries.

The draft is the last manual gate. Read it over on GitHub and publish it yourself.

Every image lands under the ghcr.io/base/ namespace, and the names are easy to confuse. The one to watch is base.

ImageBake targetWhat it holds
ghcr.io/base/nodebaseThe compatibility image: base, base-reth-node, base-consensus, and snapshotter, plus supervisord, which is the default command and runs the execution and consensus entrypoints together. The root docker-compose.yml pulls this one.
ghcr.io/base/baseunifiedOnly the base binary, with ENTRYPOINT ["./base"].
ghcr.io/base/base-reth-nodeexecutionOnly base-reth-node.
ghcr.io/base/base-consensusconsensusOnly base-consensus.
ghcr.io/base/base-builderbuilderOnly base-builder.
ghcr.io/base/basectlbasectlOnly basectl.
ghcr.io/base/base-snapshottersnapshotterOnly snapshotter.

The bake target called base publishes as node, while the image called base comes from a different target. So ghcr.io/base/base is not a renamed ghcr.io/base/node. Swap one for the other and the container starts a lone ./base binary, with no supervisord and none of the entrypoint scripts the operator default relies on.

The split arrived after v1.4.1-rc.11 was cut, so no tag published so far carries the single-binary images. Until a release built from it ships, ghcr.io/base/node is still the only image path you can pull.

Each image is published on its own, so one failure does not stop the rest. If neither architecture builds for an image, the workflow skips that image with a warning. If only one builds, it publishes that one with a warning. A final release can therefore exist on one image path but not another, or exist as amd64 only. Before you pin a tag on arm64 hardware, check that the manifest actually lists your platform:

Terminal window
docker buildx imagetools inspect ghcr.io/base/node:vX.Y.Z

Create RC is wired to every push on any releases/v* branch, including pushes that arrive before the version sync PR merges. Those are harmless. Rather than failing the run, the workflow spots the 0.0.0 version and exits with a notice.

RC numbering needs no bookkeeping either. N in vX.Y.Z-rc.N is derived from the tags already present.

Only the tagging and dispatch half is serialised per release branch, and it is serialised deliberately rather than throttled. The tagging job runs under queue: max, which lets as many as 100 runs sit in line instead of displacing one another. Setting cancel-in-progress: false on its own does not achieve that — pending runs still get replaced. Two caveats come with the queue. GitHub releases waiting jobs in the order they began waiting, which is not guaranteed to be commit order, though each run still tags the commit that started it. And once the line is full, further runs are cancelled outright.

The dispatch is explicit for a reason that is easy to trip over when editing these workflows. A tag pushed under GITHUB_TOKEN will not set off another workflow, so Create RC cannot simply push the tag and expect a build to notice. It calls the build directly instead, which is why it carries actions: write. The permissions needed to publish artifacts are not granted there; they stay with Build RC.

Which half failed decides what you rerun.

If the tag was created but the dispatch did not go through, rerun Create RC. It recognises the tag it already made at that commit and reuses it rather than burning the next number in the series.

If the tag is fine and the artifacts failed, rerun the Build RC run itself, or start one against the existing tag:

Terminal window
gh workflow run build-rc.yml --ref v1.4.0-rc.1

Build RC only accepts an RC tag as its ref. Pointing it at a release branch or a final tag will not work. Note also that dispatching a tag that is already building starts a second build rather than replacing the first, so check before you fire one off.

Landing this split across branches has an ordering requirement that is easy to get backwards. GitHub will only dispatch a workflow that exists on the default branch, so Build RC has to merge to the default branch first. Only then do you backport the workflow and release-script changes to each release branch still in service.

Both halves matter because dispatch happens at the RC tag. The commit under that tag has to contain build-rc.yml and the reusable build workflow too, otherwise there is nothing there to run. Tags cut before the split therefore cannot be dispatched this way at all; recover those by rerunning the build jobs they originally had.

Two things this rollout does not do. Updating the default branch alone leaves push-triggered workflows on existing release branches exactly as they were. And runs already queued or in flight keep the configuration they started with, so the change will not retroactively rescue cancelled runs or free the concurrency locks they are holding.

There is one trap in older runs specifically. Before touching a legacy Create RC run, look at which ref it checks out. Earlier definitions checked out the release branch rather than the triggering commit, and since the branch keeps moving, a rerun can tag a newer commit than the one you were trying to recover.

WorkflowTriggerWhat it doesOutput
Start ReleaseManual — bump typeCuts the release branch from the computed next versionreleases/vX.Y.Z branch
Release Version SyncAutomatic — branch creationRaises the PR that writes the version into Cargo.tomlPR targeting the release branch
Create RCAutomatic — push to releases/v*Tags the next candidate and requests its buildRC tag + a dispatched build
Build RCDispatched by Create RC at the RC tagChecks the ref is an RC tag, then builds on its own scheduleDocker images + binaries
Build ReleaseCalled by Build RC and Publish ReleaseShared build step behind both pathsDocker images + binaries
Publish ReleaseManual — version numberCuts the final tag and assembles the releaseFinal tag + Docker images + draft GitHub release

Build Release is the only entry here you never invoke directly. Both the candidate path and the publish path delegate their image and binary builds to it, so a final release and the candidate it came from are produced by the same build steps.

Both paths build with maxperf, not the release default. Build Release compiles every native binary archive under it and passes it through to the image bake, so candidates and final releases are optimized identically. It layers fat LTO and a single codegen unit over release, which is why these builds take considerably longer than one you run yourself. See build profiles for the full settings.