Hive Hive
Sign in

feat(cli): backport patch-release workflow + semver-correct latest-version

GitHub issue · Closed

Metadata
Source
tuist/tuist #11206
Updated
Jun 24, 2026
Domains
Distribution
Details

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 from cli-release.yml’s release-cli / release-cli-linux / publish-cli jobs. Both the normal release and the backport call it, so there is one source of truth and no drift.
  • cli-backport.yml — new workflow_dispatch (runs from main) 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 with make_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/1 and Tuist.Docs.CLI.fetch_latest_cli_tag/0 now 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 main with a branch input, not “on” the branch: GitHub resolves a workflow_dispatch‘s file from the dispatched ref, and old release branches predate this feature, so they can’t host the workflow.
  • workflow_call correctness: top-level env is redeclared in the reusable workflow (it doesn’t cross the boundary); inputs.ref is threaded into every checkout; the tag’s target_commitish uses inputs.ref (a resolved SHA) rather than github.sha, which under workflow_call resolves to the caller’s commit (main), not the backport branch. cli-release.yml gains top-level id-token: write (a called workflow’s job permissions are capped by the caller’s) and shares a ref-independent cli-publish concurrency 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), never tuist@4 or highest-semver latest. Proven live: the 4.192 line already carries 4.192.04.192.4 yet tuist@4 resolves to the true latest. make_latest=false covers the one chronological path (bare mise latest tuist); update_homebrew=false keeps the Homebrew formula pointed at true latest.

Impact

  • New capability: cut a patch on any older CLI line without touching main or moving “latest” pointers.
  • Customers can pin (mise.toml: tuist = "4.192.5", or tuist@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-releases Xcode is not on the macos-26 runner image, the build fails (signal that the line is too old for current infra). Documented in the cli-backport.yml header.

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.

Comments
TA
tuist-atlas[bot] Jun 11, 2026

The backport patch-release workflow and semver-correct latest-version feature is now available in version xcresult-processor-image@0.16.0. Update to the Tart image ghcr.io/tuist/tuist-xcresult-processor:0.16.0 to use these capabilities.