Hive
feat(cli): backport patch-release workflow + semver-correct latest-version
GitHub issue · Closed
Addresses a customer support request: teams pinned to an older CLI line need a fix that only shipped in a newer minor (which broke something else for them), with no way to get a patch on their current line.
What changed
Two coupled pieces:
1. A CLI backport patch-release mechanism (release-branch model, Rails/Kubernetes-style)
cli-build-publish.yml— new reusable (workflow_call) workflow extracted fromcli-release.yml’srelease-cli/release-cli-linux/publish-clijobs. Both the normal release and the backport call it, so there is one source of truth and no drift.cli-backport.yml— newworkflow_dispatch(runs frommain) taking a release branch (e.g.releases/4.192.x) that a human/agent has already cherry-picked the fix(es) onto. It derives the next version by force-bumping the patch only (major.minor pinned to the branch’s line, never a git-cliff minor/major bump), resolves the branch HEAD to an immutable SHA, and publishes withmake_latest=false+update_homebrew=false.cli-release.yml— rewired to call the reusable workflow; normal-path behavior is unchanged.
2. Semver-correct “latest CLI version” on the server (hard prerequisite)
Tuist.GitHub.Releases.get_latest_cli_release/1andTuist.Docs.CLI.fetch_latest_cli_tag/0now select the highest-semver CLI release instead of the first one returned by GitHub.
Why
The only way to deliver a fix to a customer on an old version was to push them to a newer minor that might break something else. A patch release on the old line (e.g. 4.192.1) lets them stay on a known-good baseline and pick up only the fix.
Root cause of the server fix
GitHub’s /releases endpoint orders releases by publish date, and the server took the first CLI release in that list. A backport (4.192.1) is published after a newer minor (4.195.x) but is semantically lower, so it would be mistaken for “latest” — the dashboard banner, the deprecation-warning upgrade text, and the docs CLI spec would all regress to the backported version. Selecting by semver (bare-semver CLI tags parse as a Version; scoped component tags like server@1.2.3 don’t, so Version.parse/1 also filters them out) fixes all three call sites.
Design notes / alternatives
- Release-branch + dispatch, not in-CI cherry-pick. Conflict resolution lives on the branch (humans/agents own its contents); CI never cherry-picks. Matches how other projects backport.
- Dispatch from
mainwith abranchinput, not “on” the branch: GitHub resolves aworkflow_dispatch‘s file from the dispatched ref, and old release branches predate this feature, so they can’t host the workflow. workflow_callcorrectness: top-levelenvis redeclared in the reusable workflow (it doesn’t cross the boundary);inputs.refis threaded into every checkout; the tag’starget_commitishusesinputs.ref(a resolved SHA) rather thangithub.sha, which underworkflow_callresolves to the caller’s commit (main), not the backport branch.cli-release.ymlgains top-levelid-token: write(a called workflow’s job permissions are capped by the caller’s) and shares a ref-independentcli-publishconcurrency group with the backport so the two never tag/publish concurrently.- mise needs no change. mise resolves versions by semver from the tag list (scoped
@tags filtered), not by publish date, so a backport slots into semver order and only wins its own line (tuist@4.192), nevertuist@4or highest-semver latest. Proven live: the4.192line already carries4.192.0–4.192.4yettuist@4resolves to the true latest.make_latest=falsecovers the one chronological path (baremise latest tuist);update_homebrew=falsekeeps the Homebrew formula pointed at true latest.
Impact
- New capability: cut a patch on any older CLI line without touching
mainor moving “latest” pointers. - Customers can pin (
mise.toml: tuist = "4.192.5", ortuist@4.192) to get exactly the backport. - Server stops mis-advertising a backport as the latest version.
- Operational caveat: a backport builds with that line’s own scripts and pinned Xcode; if the line’s
.xcode-version-releasesXcode is not on themacos-26runner image, the build fails (signal that the line is too old for current infra). Documented in thecli-backport.ymlheader.
How to test locally
Server fix (fast):
cd server
mix test test/tuist/github/releases_test.exs # incl. new regression: 4.195.1 published before 4.192.1 -> 4.195.1 wins
mix test test/tuist/docs/cli_test.exs
Validation run in this PR: releases_test.exs 13/13, docs/cli_test.exs 8/8, mix format --check clean, mix credo no issues, actionlint 0 real findings on the three workflows (only pre-existing repo-wide tuist-linux label / shellcheck warnings, which CI does not gate).
Backport dry run (needs a pushed branch + dispatch): create a throwaway releases/X.Y.x branch off a recent tag with a trivial commit, dispatch cli-backport.yml with that branch, and confirm the derived version stays on the line’s major.minor and bumps only the patch, the tag points at the branch HEAD (not main), the release is not marked “Latest”, the Homebrew workflow is not dispatched, and main is untouched.