Git Branching Workflow
ModelRouter uses a GitHub Flow + release-branch model: main is the only
long-lived branch and is always deployable; new work branches off it as a
feature branch; each version gets its own release branch with a tag; hotfixes
land on the release branch and MUST flow back to main.
Goal: make "patching an already-released version during its support window" a
first-class operation, while main always contains every fix and stays
deployable.
Branch roles
| Branch | Name | Lifetime | Purpose |
|---|---|---|---|
| Main | main |
permanent | Only long-lived branch. Always deployable. Accumulates the next version. |
| Feature | feature/<desc> |
short (dev → merge → delete) | New features / ordinary fixes. Born from main, merged back into main. |
| Release | release/vX.Y.x |
one version cycle | Cut from main, tagged for release. Version-specific hotfixes land here. |
Change flow (important)
feature/foo ───────┐
▼
main ──────────────── next version ──────►
│ │
│ cut │ cut
▼ ▼
release/v1.6.x release/v1.7.x
│ │
tag: v1.6.0 tag: v1.7.6
│ │
hotfix ◄─────┘ hotfix ◄─────┘
│ │
└── cherry-pick back ──────────┘
The patch position in the branch name is a literal x, while the tag carries
the concrete version: one release/v1.7.x can hold tags v1.7.0 … v1.7.6.
Spanning several patches on a single minor branch is deliberate — patches are
revisions of the same feature batch, hotfixes land on one branch, and back-port
to main never has to resolve dependencies between several release branches.
Key rules
- main is always deployable: never leave half-done work on
main. Unfinished work lives on a feature branch. - Features start from main and merge back:
git checkout -b feature/xxx main, thengit merge --no-ff feature/xxx(or squash) when done. - Release = cut a release branch from main + tag:
Build installers and upload the GitCode Release from this tag so the published state is exactly reproducible.git checkout -b release/v1.7.x main git tag -a v1.7.0 -m "ModelRouter v1.7.0" git push origin release/v1.7.x v1.7.0 - Only version-specific hotfixes go into a release branch during its
window: new features always go to
mainfor the next version, never into an already-released branch (unless you deliberately ship a minor revision). - Hotfixes MUST flow back to main:
Most important rule: main only moves forward and never loses fixes. If a hotfix never reaches main, the next release ships with the old bug.git checkout release/v1.7.x # fix in the release branch git commit -m "fix: ..." git checkout main git cherry-pick <hotfix-commit> # and into main
End of a version lifecycle
When the next version ships, the previous release branch retires:
- Default: delete the remote release branch
(
git push origin :release/v1.7.x). All hotfixes were already cherry-picked into main, so main contains everything; no merge needed. - Long-term maintenance (e.g. an enterprise client pinned to an old version): keep the branch, accept only security fixes, keep the commit-then-cherry-pick loop.
Where practice diverged from this section (checked 2026-10-01):
release/v1.4.xandrelease/v1.5.xstill exist locally and on the remote, so "retire the previous branch when the next version ships" was never carried out. Keeping them is harmless (hotfixes were back-ported), but it contradicts the rule above and makes a reader wonder whether they should be there at all. Feature branches, by contrast, are cleaned up:feature/key-quota-control,feature/toolcall-id-sanitize,feature/anthropic-usage-cacheandfeature/agentrouter-id-sanitizewere deleted on 2026-10-01 after confirming with a per-commitgit patch-idcomparison that their work had already landed in main.
Explicit non-goals
- Never rebase main: main's history stays append-only; anyone pulling gets directly usable history.
- Release branches are not merged back wholesale: hotfixes were already cherry-picked; a whole-branch merge only creates conflicts with no benefit.
- No "quick edit" directly on main, even single-person: at minimum use
feature/xxx → merge mainso history keeps its why-boundaries.
Single vs multi person
- Single (this repo today): skip PRs —
git checkout -b feature/xxx→ finish → merge back to main. Release branch + tag flow unchanged. - Multi / open source: feature branches go through PR + Code Review, maintainers merge; resolve conflicts on the feature branch, never on main.
Commit message conventions
feat(...): new featurefix(...): bug fixchore(...): build / version / deps / non-code changesdocs(...): documentationrefactor(...): refactoring- Scope in parens, e.g.
fix(adapters),feat(webui),chore(version) - One-line summary; expand with problem/root-cause/fix/verification when needed
Release checklist (companion)
# 1. main is ready
git checkout main && git pull
# 2. cut release branch + tag
git checkout -b release/vX.Y.Z main
git tag -a vX.Y.Z -m "ModelRouter vX.Y.Z"
# 3. push branch + tag
git push origin release/vX.Y.Z
git push origin vX.Y.Z
# 4. build installers (core + GUI platforms)
make core-dist VERSION=X.Y.Z
# 5. create GitCode Release + upload
# https://gitcode.com/JianFeeeee/ModelRouter/releases
# upload MUST use PUT --http1.1, otherwise the OBS callback fails and the
# file never lands in the release; replacing artifacts = delete the tag
# (release disappears with it) then recreate.
Why this model (background)
Before 2026-08 everything went straight to main plus a tag, which caused two
pain points:
- After 1.4.2 shipped, a scheduler-scoring fix had to be added to the same version — the only option was deleting the remote tag to clear the release and re-uploading all installers. The published state was detached from code history; "what exactly shipped" could not be traced.
- No feature branches meant two independent efforts could not proceed in parallel without colliding.
With release branches: the published state = release/vX.Y.x branch +
the concrete vX.Y.Z tag, exactly reproducible; hotfixes have a clear landing
spot; main stays "latest + all fixes + deployable".
中文版见 docs/git-workflow.md。