server/priv/docs/en/contributors/releases.md
Tuist publishes new versions automatically as changes are merged to the main branch. The server and app are released continuously, so improvements reach users immediately. The CLI builds on the same tooling but ships through release channels (canary, release candidate, stable) so the recommended install stays stable and slow-moving; cutting a release candidate and promoting it to stable are deliberate, maintainer-triggered steps.
We release these main components:
The server and app publish automatically on every push to the main branch. The CLI publishes a canary prerelease on every push, but reaches a stable release only when a maintainer cuts and promotes it (see CLI release channels).
We use Conventional Commits to structure our commit messages. This allows our tooling to understand the nature of changes, determine version bumps, and generate appropriate changelogs.
Format: type(scope): description
| Type | Description | Version Impact | Example |
|---|---|---|---|
feat | New feature or capability | Minor version bump (x.Y.z) | feat(cli): add support for Swift 6 |
fix | Bug fix | Patch version bump (x.y.Z) | fix(app): resolve crash when opening projects |
docs | Documentation changes | No release | docs: update installation guide |
style | Code style changes | No release | style: format code with swiftformat |
refactor | Code refactoring | No release | refactor(server): simplify auth logic |
perf | Performance improvements | Patch version bump | perf(cli): optimize dependency resolution |
test | Test additions/changes | No release | test: add unit tests for cache |
chore | Maintenance tasks | No release | chore: update dependencies |
ci | CI/CD changes | No release | ci: add workflow for releases |
For the CLI, these bumps determine the canary version and the changelog grouping. Stable CLI versions are minor lines (X.Y.0) cut by hand, and stable patches (X.Y.Z) come only from backports, so a feat or fix reaches CLI users through the channel flow below rather than as an immediate stable release.
Breaking changes trigger a major version bump (X.0.0) and should be indicated in the commit body:
feat(cli): change default cache location
BREAKING CHANGE: The cache is now stored in ~/.tuist/cache instead of .tuist-cache.
Users will need to clear their old cache directory.
Each component uses git cliff to:
When releasable changes are detected:
tuist.spec.json CLI specification generated from tuist --experimental-dump-helpFor the server and app this produces a stable release. For the CLI this same automatic pipeline produces a canary prerelease only; release candidates and stable releases are cut manually (see CLI release channels).
Each component only releases when it has relevant changes:
(cli) scope or no scope(app) scope(server) scopeUnlike the server and app, the CLI is not promoted to a stable release on every push to main. It ships through three channels so the recommended install stays stable and slow-moving while early adopters keep per-commit builds. The <.localized_link href="/cli/release-channels">Release channels</.localized_link> page documents this from a user's perspective; this section is the maintainer runbook.
| Channel | Version | Cut by |
|---|---|---|
| Canary | X.Y.0-canary.N | Automatically, on every CLI-touching push to main (cli-release.yml) |
| Release candidate | X.Y.0-rc.N | Manually, on a releases/<major>.<minor>.x branch (cli-rc.yml) |
| Stable | X.Y.Z | Manually, by promoting a soaked RC (cli-promote.yml); patches via cli-backport.yml |
Canary and RC builds are published as GitHub prereleases (never marked "Latest", never pushed to Homebrew), so package managers only resolve them on explicit opt-in. Canary always targets the next unreleased minor: once an RC line is cut, main's canary advances to the following minor.
You never hand-pick version numbers. Every channel's next version is derived from the existing git tags by mise/tasks/cli/release/channel-version.sh, which the workflows below invoke.
When the next minor is ready to soak, cut a new line:
cli-rc.yml) with an empty branch input.X.Y.0-rc.1 and, after that succeeds, creates the protected releases/X.Y.x branch at the built commit.To pull a critical fix or regression into a soaking line, cherry-pick it onto the release branch through a PR (the same flow as backports; CI never cherry-picks), then iterate the RC:
releases/X.Y.x, cherry-pick the fix, open a PR back into the release branch, resolve any conflicts there, and merge.branch=releases/X.Y.x. It publishes X.Y.0-rc.(N+1) from the branch HEAD.A soaking line is feature-frozen: only critical fixes and regressions go onto it.
After the RC has soaked cleanly (about a week):
cli-promote.yml) with branch=releases/X.Y.x.X.Y.0 tag with make_latest=true and updates the Homebrew formula.Promotion refuses to run unless the branch HEAD is exactly the commit the latest RC points at. So if any fix merged onto the line after the last RC, you must cut a new RC and let it soak before it can ship as stable.
Once a line is stable, ship patches with the CLI Backport Release workflow (cli-backport.yml): cherry-pick the fix onto releases/X.Y.x through a PR, then run the workflow with that branch to cut X.Y.(Z+1). Backports never move the "Latest" pointer or the Homebrew formula. Two lines are maintained at a time: the current line takes regressions and security fixes, the previous line takes critical and security fixes only. Other bug fixes are not backported and ship with the next minor.
Since commit messages directly influence release notes, it's important to write clear, descriptive messages:
fix(cli): resolve build cache issue (#1234)For breaking changes, include BREAKING CHANGE: in the commit body:
feat(cli): change cache directory structure
BREAKING CHANGE: Cache files are now stored in a new directory structure.
Users need to clear their cache after updating.
The server, app, cache, Gradle plugin, skills, Noora, and infrastructure components release through .github/workflows/release.yml. It runs on pushes to main, uses git cliff for change detection, and handles the full process including artifacts and GitHub releases.
The CLI has its own set of workflows (it is excluded from release.yml):
cli-release.yml - publishes a canary on every CLI-touching push to maincli-rc.yml - manually cuts or iterates a release candidatecli-promote.yml - manually promotes a soaked RC to stablecli-backport.yml - manually cuts a patch on a stable linecli-build-publish.yml - the shared build and publish pipeline the four above callThe CLI build (cli-build-publish.yml) also produces and publishes a tuist.spec.json artifact (generated from tuist --experimental-dump-help) so downstream tooling can consume the command interface. All CLI workflows serialize through one cli-publish concurrency group so version resolution and tagging never race.
You can monitor releases through:
This continuous release approach provides:
If a release fails:
For urgent fixes that need immediate release:
While the CLI and Server follow the continuous release process described above, the iOS app is an exception due to Apple's App Store review process:
The iOS app still follows the same commit conventions and uses git cliff for changelog generation, but the actual release to users happens on a less frequent, manual schedule.