Files
ModelRouter/docs/git-workflow-en.md
JianFeeeee ad54616bee docs: 对齐分支命名实际用法 + 修正示例配置的存储位置说明
三处都是"文档说的"与"仓库实际做的"不一致,读文档的人会被误导。

1. 分支命名:文档写 release/vX.Y.Z 且举例 release/v1.4.2,实际从
   release/v1.4.x 起一律用字面 x(release/v1.5.x / v1.7.x)。改成实际情况,
   并说明"一条 minor 分支跨多个 patch"是刻意的:patch 是同批功能的修订,
   hotfix 落同一条分支,回流 main 时不必处理多条 release 分支间的依赖。

2. 退役规则与实践不符:文档说"下个版本发布就删上一个 release 分支",但
   release/v1.4.x / v1.5.x 至今仍在。保留无害(hotfix 已回流),但与规则
   矛盾。两份文档都如实记下这个出入,并记录特性分支**确实**在清理——本次
   删除的四个分支都逐提交用 git patch-id 核对过,工作已全部进入 main
   (唯一 patch-id 不同的是 bf0657b 的发布前变体,diff 过 api.go 改动逐字节
   相同)。留着只是给下个版本制造 cherry-pick/merge 陷阱。

3. 存储位置:示例配置说"WebUI 新增/编辑的源会写入 runtime_file",且默认
   模型注释说 AUTO"按各源 priority 自动选最高可用源"。两处都与实现相反——
   上游源、网关密钥、AUTO 链都在 config.yaml(AddSource 走
   UpsertSourceInYAML,密钥与链走 cfg.Save);runtime.json 只剩源模板、
   删除标记和预置模板名单。已核实 store.Upsert(runtime 源)已无调用点,
   store 里的 Keys/Auto 只被加解密、从不写入,是遗留字段。

priority 本身不是完全没用,所以注释保留并说清它的两个真实用途:首次启动
seedAuto 的初值,以及 AUTO 生图链未配置时挑"最佳模型"(bestChatModel /
bestImageModel 按 priority 取最大)。
2026-10-01 20:41:57 +08:00

6.9 KiB

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

  1. main is always deployable: never leave half-done work on main. Unfinished work lives on a feature branch.
  2. Features start from main and merge back: git checkout -b feature/xxx main, then git merge --no-ff feature/xxx (or squash) when done.
  3. Release = cut a release branch from main + tag:
    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
    
    Build installers and upload the GitCode Release from this tag so the published state is exactly reproducible.
  4. Only version-specific hotfixes go into a release branch during its window: new features always go to main for the next version, never into an already-released branch (unless you deliberately ship a minor revision).
  5. Hotfixes MUST flow back to main:
    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
    
    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.

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.x and release/v1.5.x still 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-cache and feature/agentrouter-id-sanitize were deleted on 2026-10-01 after confirming with a per-commit git patch-id comparison 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 main so 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 feature
  • fix(...): bug fix
  • chore(...): build / version / deps / non-code changes
  • docs(...): documentation
  • refactor(...): 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:

  1. 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.
  2. 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。