Release Process
This document describes how to create and publish releases of HelixScreen.
Table of Contents
Section titled “Table of Contents”- Version Scheme
- Automated Release Pipeline
- Creating a Release
- Release Checklist
- Hotfix Releases
- Pre-release Versions
Version Scheme
Section titled “Version Scheme”HelixScreen uses Semantic Versioning:
MAJOR.MINOR.PATCH[-PRERELEASE]| Component | When to Increment |
|---|---|
| MAJOR | Breaking changes (config format, API, incompatible UI changes) |
| MINOR | New features, backwards-compatible |
| PATCH | Bug fixes, documentation, minor improvements |
| PRERELEASE | Trunk builds heading for the next MINOR: -beta.N |
A prerelease sorts below its release, and the whole scheme rests on that
Section titled “A prerelease sorts below its release, and the whole scheme rests on that”
helix::version::Versionimplements Semantic Versioning 2.0.0 precedence (src/util/version.cpp), so1.1.0-beta.1 < 1.1.0-beta.2 < 1.1.0-rc.1 < 1.1.0. That ordering is what lets the trunk ship1.1.0-beta.Nwhilerelease/1.0still ships1.0.x: a beta outranks every 1.0 hotfix and is still superseded by the real1.1.0when it publishes.The same question gets asked in three languages, so three implementations exist: the app (
src/util/version.cpp), CI’s pre-upload channel guard (scripts/version-compare.sh), and the Android versionCode (scripts/android-version-code.sh). They share one set of cases intests/fixtures/version_precedence.txt, and adding a version there fails every suite that disagrees with it. Change one comparator and you change all three.
sort -Vis not one of them and must not be used: GNU version sort orders1.1.0before1.1.0-beta.1, the opposite of semver.
Suffixes the tooling accepts
Section titled “Suffixes the tooling accepts”-alpha.N, -beta.N, -rc.N, with N from 1 to 29. The Android versionCode packs
the suffix into a 100-wide lane and errors on anything it does not recognise rather
than guess an ordinal, so a novel suffix fails the release build instead of
shipping an unpublishable APK. Build metadata (+sha) does not affect precedence
and is not used.
Examples
Section titled “Examples”v1.0.0- First stable releasev1.1.0-beta.1- First beta of the 1.1 line, frommainv1.1.0-rc.1- Release candidate, same line, higher precedence than any betav1.1.0- The 1.1 stable release, fromrelease/1.1v1.1.1- Bug fix on the 1.1 linev2.0.0- Breaking changes
The trunk’s beta line
Section titled “The trunk’s beta line”main carries RELEASE_CHANNEL=beta, so its tags reach the beta and dev channels
while release/1.0 keeps stable:
| Branch | Versions | Channel |
|---|---|---|
release/1.0 |
1.0.0, 1.0.1, 1.0.2 … |
stable |
main |
1.1.0-beta.1, 1.1.0-beta.2 … |
beta + dev |
release/1.1 (future) |
1.1.0, then 1.1.1 … |
stable |
The trunk names the release it is heading for, so a beta install says which minor
it belongs to and 1.1.0 stays free for the stable cut. A beta at 1.1.0-beta.7
is offered 1.1.0 the moment it publishes, and a 1.0.2 hotfix on the stable line
never looks like an upgrade to it.
When release/1.1 is cut, main moves to 1.2.0-beta.1 and the pattern repeats.
Automated Release Pipeline
Section titled “Automated Release Pipeline”The release process is fully automated via GitHub Actions (.github/workflows/release.yml).
Trigger
Section titled “Trigger”Pushing a tag matching v* triggers the release workflow:
git tag v1.2.0git push origin v1.2.0Dry run
Section titled “Dry run”Only a v* tag push publishes. workflow_dispatch is always a dry run of the whole pipeline, on any ref:
gh workflow run release.yml --ref <branch> -f platforms=x86 # platforms empty = full matrixA dry run creates no GitHub release and sends nothing to Play, Discord or the website. Its R2 uploads go under dry-run/<run_id>/ with version 0.0.0-dryrun.<run_id>, and the would-be release assets go to the dry-run-release-assets workflow artifact. The dry-run-verify job then runs scripts/verify-dry-run-release.sh against both and deletes the prefix. tests/shell/test_release_dry_run.bats fails any publishing step that is not gated on RELEASE_MODE == 'publish' and does not write through R2_PREFIX.
Pipeline Stages
Section titled “Pipeline Stages”┌─────────────────────────────────────────────────────────────┐│ Tag Push (v1.2.0) │└──────────────────────────────┬──────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Build Matrix │└──────────────────────────────┬──────────────────────────────┘ │ ▼ validate-shell gate → build-platforms matrix (9): pi · pi32 · ad5m · cc1 · k1 · ad5x · k2 · x86 · snapmaker-u1 plus build-android → publish-android `release` needs build-android too, not just build-platforms — otherwise it could publish before Android finished and ship with no APKs, silently. build-android FAILS the whole release if ANDROID_KEYSTORE_BASE64 is unset; it no longer falls back to the debug keystore. See ANDROID_PLAY_STORE.md.
Each platform job: Docker cross-toolchain → target arch → package .zip │ ▼ ┌──────────────────┐ │ release │ │ │ │ • Download all │ │ artifacts │ │ • Extract version│ │ • Generate notes │ │ • Create release │ └──────────────────┘Build Artifacts
Section titled “Build Artifacts”| Platform | Artifact Name | Contents |
|---|---|---|
| Raspberry Pi (64-bit) | helixscreen-pi.zip |
aarch64 binary, assets, configs |
| Raspberry Pi (32-bit) | helixscreen-pi32.zip |
armhf binary, assets, configs |
| AD5M | helixscreen-ad5m.zip |
armv7l binary (static), assets, configs |
| CC1 | helixscreen-cc1.zip |
ARM binary, assets, configs |
| K1 | helixscreen-k1.zip |
MIPS32 binary (static, musl), assets, configs |
| AD5X | helixscreen-ad5x.zip |
MIPS binary (ZMOD), assets, configs |
| K2 | helixscreen-k2.zip |
ARM binary (static, musl), assets, configs |
| x86 | helixscreen-x86.zip |
x86 binary, assets, configs |
| Snapmaker U1 | helixscreen-snapmaker-u1.zip |
ARM binary, assets, configs |
| Android | Play Store bundle | via build-android / publish-android |
Bridge release note: Starting with v1.0.0 (the version you’re currently preparing), the primary release asset is
helixscreen-{platform}.zip(unversioned filename). The legacyhelixscreen-{platform}-v{version}.tar.gzis still published during this bridge release for backwards compatibility with older installed versions; it will be removed in the following release.
Creating a Release
Section titled “Creating a Release”Step 1: Prepare the Release
Section titled “Step 1: Prepare the Release”-
Ensure main branch is stable:
Terminal window git checkout maingit pull origin mainmake test-run # Run tests -
Bump
VERSION.txtand add the CHANGELOG entry, in onechore(release):commit.VERSION.txtis the source of truth for the built binary (Makefile:195,mk/cross.mk) - the version does not come from the git tag. Tagging without bumping the file ships a binary that reports the previous version.echo "X.Y.Z" > VERSION.txt- Add the release section to
CHANGELOG.mdabove the previous one git commit -m "chore(release): vX.Y.Z" CHANGELOG.md VERSION.txt- Also check
CLAUDE.md/README.mdfor any hardcoded version strings
-
Test on actual hardware:
- MainsailOS / Raspberry Pi
- AD5M with ForgeX (if applicable)
- Run through verification checklist in
docs/user/TESTING_INSTALLATION.md
Step 2: Create the Tag
Section titled “Step 2: Create the Tag”With release notes (recommended):
# Create annotated tag with release notesgit tag -a v1.2.0 -m "$(cat <<'EOF'## What's New
### Features- Added input shaper visualization- Improved AMS panel responsiveness
### Bug Fixes- Fixed crash when Moonraker disconnects during print- Fixed touch calibration on rotated displays
### Other- Updated documentation- Performance improvementsEOF)"
# Push the taggit push origin v1.2.0Without release notes:
git tag v1.2.0git push origin v1.2.0The workflow auto-generates basic release notes if no annotation is provided.
Step 3: Monitor the Build
Section titled “Step 3: Monitor the Build”- Go to Actions tab on GitHub
- Watch the “Release” workflow
- Build takes ~15-20 minutes typically
Step 4: Verify the Release
Section titled “Step 4: Verify the Release”- Go to Releases on GitHub
- Check both platform archives are attached
- Verify checksums in release notes
- Test installation:
Terminal window curl -sSL https://releases.helixscreen.org/install.sh | sh
Release Checklist
Section titled “Release Checklist”Before Tagging
Section titled “Before Tagging”- All tests pass (
make test-run) - No critical bugs in issue tracker
- Documentation updated for new features
- Tested on real hardware (Pi and/or AD5M)
After Release
Section titled “After Release”- Release workflow completed successfully
- Both platform artifacts attached
- Release notes accurate
- Installation tested via curl|sh
- Update any external references (Discord, documentation sites)
Hotfix Releases
Section titled “Hotfix Releases”For urgent bug fixes:
-
Create hotfix branch from the release tag:
Terminal window git checkout -b hotfix/v1.2.1 v1.2.0 -
Apply minimal fix - only the necessary changes
-
Test thoroughly - verify the fix, check for regressions
-
Merge to main (via PR if time permits):
Terminal window git checkout maingit merge hotfix/v1.2.1 -
Tag and release:
Terminal window git tag -a v1.2.1 -m "Fix: [description of fix]"git push origin v1.2.1
Pre-release Versions
Section titled “Pre-release Versions”For testing new features before stable release:
Creating a Pre-release
Section titled “Creating a Pre-release”Tag the branch whose RELEASE_CHANNEL already routes where you want it to go. The
suffix expresses precedence; the branch chooses the audience:
git tag -a v1.1.0-beta.2 -m "Beta: new AMS features"git push origin v1.1.0-beta.2Pre-release Behavior
Section titled “Pre-release Behavior”The GitHub prerelease flag and the R2 upload channels both come from the
RELEASE_CHANNEL file at the repo root, on the branch being tagged — not from
the tag string. See docs/devel/UPDATE_SYSTEM.md § “How CI Determines Upload
Channels” for the full table and the reason.
RELEASE_CHANNEL=stable-> full GitHub release, shown as “latest”RELEASE_CHANNEL=betaordev-> marked prerelease, not shown as “latest”- Users must explicitly choose to install a prerelease:
Terminal window curl -sSL .../install.sh | sh -s -- --version v1.1.0-beta.2
The suffix is not the routing. A -beta.N suffix says where the version sits
in the ordering, not who receives it: the dev channel cannot be spelled in a
version string at all, and a plain v1.1.0 tag pushed from the trunk would reach
the stable fleet if the tag decided. RELEASE_CHANNEL keeps that decision a
property of the branch, which is why scripts/release-channel.sh rejects a
suffixed tag on a stable branch: a prerelease has no business on the stable
channel regardless of which branch it was cut from.
Graduating the beta line
Section titled “Graduating the beta line”A stable release is not a re-tag of the last beta. Cut the maintenance branch,
edit its RELEASE_CHANNEL to stable, drop the suffix from VERSION.txt, and tag
there:
git checkout -b release/1.1 main$EDITOR RELEASE_CHANNEL # beta -> stableprintf '1.1.0\n' > VERSION.txt # was 1.1.0-beta.Ngit commit -m "chore(release): v1.1.0" RELEASE_CHANNEL VERSION.txt CHANGELOG.mdgit tag -a v1.1.0 -m "1.1.0"git push origin release/1.1 v1.1.0Dropping the suffix is itself the promotion: 1.1.0 outranks every
1.1.0-beta.N, so the beta fleet is offered it as an ordinary forward step.
main then moves to 1.2.0-beta.1 and keeps publishing to beta.
Manual Release (Emergency)
Section titled “Manual Release (Emergency)”If GitHub Actions fails, you can build and release manually:
Build Locally
Section titled “Build Locally”# Build for Pimake PLATFORM_TARGET=pi clean release-pi
# Build for AD5Mmake PLATFORM_TARGET=ad5m clean release-ad5mCreate Release Manually
Section titled “Create Release Manually”- Go to GitHub Releases → Draft a new release
- Choose the tag
- Write release notes
- Upload the
.zipfiles fromreleases/(plus the legacy.tar.gzfiles if the bridge release still ships them) - Publish
Changelog Generation
Section titled “Changelog Generation”CHANGELOG.md is the changelog. It is written by hand in the release commit (Step 1);
nothing generates it. The annotated tag’s message becomes the GitHub release body
(Step 2), and scripts/generate-whatsnew.sh distills the finished CHANGELOG section
into the Play Store “What’s New” - both consume what was already written.
Prose style - user-facing voice, separator, issue-link form, and the daily vs milestone entry shapes - is specified in CHANGELOG_STYLE.md. Read it before drafting the release’s section.
Troubleshooting
Section titled “Troubleshooting”Build Fails
Section titled “Build Fails”- Check Actions logs for the specific error
- Common issues:
- Submodule not checked out
- Docker cache issues (try re-running)
- Toolchain image problems
Wrong Version Released
Section titled “Wrong Version Released”- Delete the release on GitHub
- Delete the tag:
Terminal window git tag -d v1.2.0git push origin :refs/tags/v1.2.0 - Fix the issue
- Re-tag and push
Missing Artifact
Section titled “Missing Artifact”If one platform’s build fails:
- Fix the issue
- Delete the partial release
- Delete and re-push the tag to trigger fresh build
Related: CI/CD Guide | Testing Installation