Skip to content

Git Release model: hotfix branches off tags so stable users get fixes fast #918

Description

@romanstingler

@MalpenZibo here my 2 cents after our discussion. Feel free to bug me :)

Trunk based releases with hotfix branches (no develop, no release branch) (I think this is simpler)

Right now main is our dev line and we cut big releases from it. When something upstream breaks (like Hyprland API changes) and we need a quick patch, we can't ship a 0.9.x without also shipping the unreleased 0.10.0 features. So ashell stable users wait around 3 months (current avg). This proposes keeping main as the trunk, tagging it directly for normal releases after a short test window, and using hotfix branches off tags for urgent patches so they never drag in half finished features.

The problem

  • Features merge straight into main and main is always ahead of the last tag.
  • A hotfix can't be tagged from main without pulling in the next feature release.
  • Result: we sit on fixes for months until the next feature release is ready.

Proposed model

  • main stays the trunk. Features land there like they do now.
  • Normal release: when main tip looks good, test it for a few days, then tag and release directly from main tip. No separate release branch.
  • Hotfix: cut hotfix/X.Y.Z from the previous release tag, fix it, tag the hotfix commit, merge back into main.

Key rule: a hotfix branches off the release tag, never off main's tip. That keeps the patch a clean island of just the fix.

Hotfix flow (concrete)

# current state: main = 0.9.0 + F1 + F2 (0.10.0 features in progress)
git checkout -b hotfix/0.9.1 v0.9.0     # branch OFF the tag, not main
# apply fix
git tag v0.9.1 hotfix/0.9.1            # tag the hotfix commit (island), NOT the merge
git checkout main && git merge hotfix/0.9.1
# later, another break:
git checkout -b hotfix/0.9.2 v0.9.1    # branch off the LATEST patch tag
# apply fix #2
git tag v0.9.2 hotfix/0.9.2
git checkout main && git merge hotfix/0.9.2

Each 0.9.x tree is 0.9.0 plus only the fix(es). main just absorbs the fixes through merge.

Visual

gitGraph
   commit id: "0.9.0" tag: "v0.9.0"
   branch hotfix/0.9.1
   checkout main
   commit id: "F1 feature"
   commit id: "F2 feature"
   checkout hotfix/0.9.1
   commit id: "fix hyprland break" tag: "v0.9.1"
   checkout main
   merge hotfix/0.9.1
   commit id: "F3 feature"
   checkout hotfix/0.9.1
   branch hotfix/0.9.2
   checkout hotfix/0.9.2
   commit id: "fix break #2" tag: "v0.9.2"
   checkout main
   merge hotfix/0.9.2
   commit id: "F4 feature"
   commit id: "test a few days"
   commit id: "0.10.0" tag: "v0.10.0"
Loading

Normal 0.10.0 is just main tip tagged after testing, no branch. Hotfixes hang off the tags.

Tooling blocker we have to solve for this to actually work

Our pre-release.yml (the update-after-publish-release job) does this:

git tag -f "${TAG}"
git push -f origin "${TAG}"
gh release edit $TAG --target $SHA

and then cargo-dist builds from that tag. For a normal release where the tag is already on main tip this is harmless. But for a hotfix it force relocates v0.9.1 onto a main commit that already has F1 and F2 in it, and cargo-dist rebuilds the patch with those features included. That defeats the whole point.

To make hotfixes ship clean patches, the automation must not force move patch tags onto main. Two options:

  1. Skip the git tag -f and target edit for hotfix derived releases, keep the tag on the hotfix commit.
  2. Build cargo-dist from the original hotfix SHA instead of the relocated tag.

This is the one real code change. The branching part is just convention.

AUR note (separate but related)

The ashell stable AUR still lags because update-arch-package.yml is a WIP no-op. Branching lets us cut 0.9.1 fast. Finishing that job lets stable users receive it fast. They are independent fixes, worth tracking together but not blocking each other.


Git Flow-lite alternative (main + develop, no release branch) (leaning against this for a two person project: the wiring cost outweighs the benefit)

Same goal, but we split main and a develop branch so main only ever holds released code.

  • main = always released (tagged). Only gets release merges and hotfix merges.
  • develop = default integration branch. Features land here via PRs.
  • Normal release: when develop tip looks good, test it for a few days, then merge develop into main and tag main. No separate release branch.
  • hotfix/X.Y.Z = cut from the main release tag, fix, merge to main, tag, then merge down into develop.

Key rule: emergency fixes go to main (via hotfix off the tag) first, then get merged down into develop. Never fix only on develop and wait for the next feature release.

Flow (concrete)

# features land on develop (PRs target develop)
git checkout develop
# ... F1, F2 land on develop ...

# urgent hotfix for the released 0.9.0 line
git checkout -b hotfix/0.9.1 v0.9.0    # branch OFF the tag, not develop
# apply fix
git checkout main && git merge hotfix/0.9.1 && git tag v0.9.1   # tag the main merge commit
git checkout develop && git merge hotfix/0.9.1   # don't lose the fix on develop

# normal release 0.10.0 (no release branch): test develop tip, then merge to main
git checkout main && git merge develop && git tag v0.10.0
git checkout develop   # develop keeps moving with new features

Visual (Git Flow-lite, no release branch)

gitGraph
   commit id: "0.9.0" tag: "v0.9.0"
   branch develop
   checkout develop
   commit id: "F1 feature"
   commit id: "F2 feature"
   checkout main
   branch hotfix/0.9.1
   checkout hotfix/0.9.1
   commit id: "fix hyprland break"
   checkout main
   merge hotfix/0.9.1 tag: "v0.9.1"
   checkout develop
   merge hotfix/0.9.1
   commit id: "F3 feature"
   checkout main
   merge develop tag: "v0.10.0"
   checkout develop
   commit id: "F4 feature"
Loading

main only moves on hotfix and release merges, so tagging the hotfix merge on main (v0.9.1) pulls in only the fix, not features. develop carries the day to day work and stays in sync by merging main back after each hotfix/release.

Tooling changes for Git Flow-lite

  • Flip the GitHub default branch to develop.
  • CI (ci.yml, nix-ci.yml) must run on main AND develop, not just main.
  • release-drafter must run on develop (it drafts the next release from develop commits), not main.
  • pre-release.yml checks out main to bump changelog and version, then must also re-sync develop (merge main back into develop) so develop does not drift.
  • ashell-git AUR PKGBUILD must be repointed to develop, otherwise git users stay on old stable and get neither fixes nor features.
  • update-arch-package.yml is still a WIP no-op and still needs finishing (same as the trunk option).
  • The force-tag issue from the trunk proposal does not apply here: main only holds released code, so relocating v0.9.1 onto main's hotfix merge commit pulls in only the fix. No release-automation surgery needed for correctness, only the branch/CI/AUR wiring above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions