This document describes the PR-based, label-based release process used by the automated GitHub Actions release workflow.
A release is created by merging a Pull Request from the release branch into main with release labels.
The workflow always reads release metadata from PR labels:
release— required; enables the release workflowx.y.z— required version label, for example0.4.36devnetN— optional network label, for exampledevnet4ordevnet5
The workflow resolves the release commit from the release PR head, not blindly from the top of main. If the PR head is an empty release commit, the workflow publishes its parent. This allows two release modes:
- Current-main release — create
releasefromorigin/main. - Pinned release — create
releasefrom a specific earlier commit, then add an empty release commit.
This keeps the process PR-reviewed and label-based while allowing releases from exact commits.
- Repository access with permission to create/push the
releasebranch and open PRs - Permission to add release labels and merge the release PR
- A unique semantic version label, for example
0.4.36
The workflow starts from the release PR head commit:
github.event.pull_request.head.sha
If the PR head is an empty single-parent release commit, the workflow publishes the parent commit instead. Git tags, Docker metadata, and the devnet branch point to the commit whose code is actually being released.
blockblaz/zeam:latest is only updated when the resolved release tree matches current main.
- Current-main release: updates
latest - Pinned older release: does not update
latest
This prevents an older pinned devnet release from accidentally overwriting latest.
When a devnet label is present, the corresponding lowercase branch is updated to the release commit:
devnet4, devnet5, ...
The branch is intentionally lowercase, while the GitHub Release tag is capitalized:
Docker/branch: devnet4
GitHub tag: Devnet4
Use this when the release should include the latest code on main.
git fetch origin --prune --tags
git checkout -B release origin/main
git commit --allow-empty -m "Release commit for devnet5 v0.5.0"
git push -f origin releaseOpen a PR:
release -> main
Add labels:
release
0.5.0
devnet5
After review, merge the PR. The workflow will publish the parent of the empty release commit and, because its tree matches current main, update latest.
Use this when the release should be cut from an exact older commit, not from top of main.
Common uses:
- final release for an old devnet
- release before a large breaking PR
- known-good historical build
- hotfix-style release from an earlier point in history
Find the target commit:
git fetch origin --prune --tags
git log --oneline origin/mainCreate the release branch from that commit:
git checkout -B release <target-commit>
git commit --allow-empty -m "Release commit for devnet4 v0.4.36 from <target-commit>"
git push -f origin releaseOpen a PR:
release -> main
Add labels:
release
0.4.36
devnet4
After review, merge the PR. The workflow will publish the parent of the empty release commit. Because the release tree does not match current main, the workflow will skip the latest Docker tag.
If 0009ea9 is the commit immediately before devnet5 landed:
git fetch origin --prune --tags
git checkout -B release 0009ea9
git commit --allow-empty -m "Release commit for devnet4 v0.4.36 from 0009ea9"
git push -f origin releasePR labels:
release
0.4.36
devnet4
Expected outputs:
v0.4.36
Devnet4
blockblaz/zeam:0.4.36
blockblaz/zeam:devnet4
blockblaz/zeam:latest is not updated for this pinned release.
When the PR is merged with a version label and optional devnet label, the workflow produces:
- Version tag:
v{VERSION}, for examplev0.4.36 - Devnet tag:
{GITHUB_TAG}, for exampleDevnet4- The devnet tag is recreated if it already exists.
- The version tag must not already exist.
- Versioned image:
blockblaz/zeam:{VERSION}blockblaz/zeam:{VERSION}-amd64blockblaz/zeam:{VERSION}-arm64
- Devnet image, when a devnet label is present:
blockblaz/zeam:{DOCKER_TAG}blockblaz/zeam:{DOCKER_TAG}-amd64blockblaz/zeam:{DOCKER_TAG}-arm64
- Latest image, only for current-main releases:
blockblaz/zeam:latestblockblaz/zeam:latest-amd64blockblaz/zeam:latest-arm64
Created only when a devnet label is present.
- Tag:
DevnetN, for exampleDevnet4 - Title:
Zeam DevnetN Release - Marked as prerelease
- Includes version, release commit, changelog, and Docker pull commands
- Changelog source:
- If a
devnetNlabel is present, changelog is generated fromDevnetN-1when that tag exists. - If no devnet label is present, or the previous devnet tag does not exist, changelog falls back to the nearest lower
vX.Y.Zversion tag.
- If a
Created or updated to the release commit:
{DOCKER_TAG}
For example:
devnet4
- Version label
0.4.36→VERSION=0.4.36→ tagv0.4.36 - Devnet label
devnet4→ Docker tag/branchdevnet4 - Devnet label
devnet4→ GitHub Release tagDevnet4
The workflow enforces these checks:
- PR must be merged into
main. - PR head branch must be
release. - PR must have the
releaselabel. - PR must have exactly a valid semantic version-style label, such as
0.4.36. - Version tag
v{VERSION}must not already exist. - Release PR parent must be reachable from
mainhistory. latestis only published when the resolved release tree matches currentmain.
This is expected for pinned releases.
Example:
A -- B -- C -- D main
\
R release
If release was created from B and R is an empty release commit, GitHub may show the PR as behind main. That is fine. The empty release commit normally merges without conflicts because it changes no files.
Usually no, as long as the release branch only contains the empty release commit. Since the commit changes no files, there is normally nothing to conflict with.
The workflow only updates latest when the resolved release tree matches current main. Pinned releases intentionally skip latest.
The empty commit exists only to make a reviewable PR from release to main. The workflow detects empty single-parent release commits and publishes the parent commit, so the recorded release SHA is the target commit being released.