version-bump

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Version Bump — version a project at land time

Version Bump — 在 land 时为项目确定版本

This repository's own versioning procedure.
AGENTS.md
declares it as the pre-merge step, so the land command runs it before merging. Tagging and the GitHub release are not part of it;
release-on-merge.yml
does that on merge. When invoked directly, run this skill before the land command, against the version of
main
you intend to land onto. When a land command invokes this skill, return to it after step 8.
这是仓库自身的版本管理流程。
AGENTS.md
将其声明为合并前步骤,因此 land command 会在合并前运行它。打标签和创建 GitHub release 不属于此流程;
release-on-merge.yml
会在合并后完成这些操作。直接调用时,应在 land command 之前运行此 skill,并针对你准备合入的
main
版本执行。当 land command 调用此 skill 时,完成第 8 步后返回该命令。

Precondition — explicit land intent

前置条件——明确的 land 意图

Why: the bump is computed against the base branch's tip at this moment; a bump made before the land goes stale when another PR merges, and the pre-merge guard then denies the merge.
This skill fires only on explicit land intent, meaning one of:
  • The user asked to land: "ship it", "land the PR", "land this".
  • The user asked for the bump itself: "bump the version", "version this PR".
  • A land command is already in flight and reached the project's pre-merge steps.
Never infer land intent. None of the following is a cue to bump:
  • The work is finished, the review passed, or CI is green.
  • A draft PR is about to be opened, or was just opened. A drafted PR carries no version: the bullet goes under
    ## [Unreleased]
    and nothing else moves.
  • The invariant script exited 1. That exit states a precondition for merging, expected for a runtime PR's whole review lifetime, never a request to bump now.
With no land intent, stop and say so. Report that the branch will need a bump before it can merge, and wait for the user. Do not bump "to be helpful".
**原因:**版本递增是根据基础分支当前的 tip 计算的;如果在 land 之前完成版本递增,而此时另一个 PR 合并,那么之前的递增结果就会过时,随后合并前门禁会拒绝合并。
此 skill 仅在存在明确 land 意图时触发,即满足以下任一条件:
  • 用户要求 land:“发布吧”、“land 这个 PR”、“执行 land”。
  • 用户要求执行版本递增:“递增版本”、“为此 PR 确定版本”。
  • land command 已在执行,并已进入项目的合并前步骤。
绝不能推断 land 意图。 以下情况均不是执行版本递增的信号:
  • 工作已完成、评审已通过或 CI 全绿。
  • 即将创建或刚创建了 draft PR。draft PR 不携带版本:相关条目应放在
    ## [Unreleased]
    下,除此之外不移动任何内容。
  • invariant script 以退出码 1 结束。这表示一项合并前置条件,在 runtime PR 的整个评审周期内都属于预期情况,绝不表示现在要求执行版本递增。
若没有 land 意图,停止并说明情况。报告该分支在合并前仍需进行版本递增,然后等待用户。不要为了“顺手帮忙”而递增版本。

Steps

Steps

0. Runtime-vs-dev gate — does this PR warrant a bump at all?

0. runtime 与 dev 门禁——这个 PR 是否确实需要版本递增?

Run this before everything else. Most steps below only apply if it says yes.
The quick look is orientation only — it never decides the exit:
git diff "origin/<default>...HEAD" --name-only
. The decision comes from the invariant run, the same invocation contract the pre-merge guard enforces:
  1. Resolve the default branch by asking GitHub (
    gh repo view
    ), falling back to the local
    origin/HEAD
    ref. Never a guessed
    main
    . If both fail, stop: no verdict.
  2. git fetch origin <default>
    must succeed. Never degrade to a stale base for a verdict.
  3. The fetched
    origin/<default>
    tip must be an ancestor of the branch tip. If the branch is behind, stop: rebase onto
    origin/<default>
    and re-enter step 0.
  4. Run the script with
    HEAD_SHA
    = the local branch tip and
    BASE_SHA
    = the fetched
    origin/<default>
    tip — never a pre-computed merge-base, and never a hand-rolled two-dot diff.
bash
DEFAULT=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null)
DEFAULT=${DEFAULT:-$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')}
[ -n "$DEFAULT" ] || { echo "cannot resolve the default branch — no verdict"; exit 1; }
git fetch origin "$DEFAULT" || { echo "fetch failed — no verdict"; exit 1; }
git merge-base --is-ancestor "refs/remotes/origin/$DEFAULT" HEAD \
  || { echo "behind base — rebase onto origin/$DEFAULT, re-enter step 0"; exit 1; }
HEAD_SHA=$(git rev-parse HEAD) BASE_SHA=$(git rev-parse "refs/remotes/origin/$DEFAULT") \
  .claude/scripts/version-bump-required.sh
Read the outcome by exact output match — the signal rule, default-deny:
  • Exit 0, stdout starting
    OK: runtime_changed=false bumped=false
    → dev-only and final. DO NOT BUMP. Skip every step below. Leave the version untouched, do not cut the changelog, and land with the plain conventional title (
    <type>: <subject>
    ). On a re-entry whose PR title still carries a stale
    vX.Y.Z
    prefix from an earlier bump, strip it now (
    gh pr edit --title
    ) — the title backstop never strips a stale prefix, and this is the one step-8 action a no-bump exit still owes. Then go straight to the land command. This exit requires that OK line — the quick look alone never authorizes it.
  • Exit 0, stdout starting
    OK: runtime_changed=true bumped=true
    → already bumped (a recovery re-entry). Never re-bump — proceed to the land command.
  • Exit 1, verdict containing
    cannot merge until version-bump runs at land time
    → bump warranted. It is actionable here only because the land-intent precondition already passed; read outside a land it states a merge precondition and nothing more.
    • On a branch with no
      chore(version)
      commit: continue to step 1. Every other exit-1 verdict in this list stops.
    • On a branch already carrying a
      chore(version)
      commit: the bump went stale (a rebase moved the fork point). Stop — drop the bump commit, undo the changelog cut, reset the title, and re-enter step 0.
  • Exit 1, verdict containing
    must land with no bump
    → wrongful bump. Stop — drop the commit, undo the cut, and re-run step 0.
  • Anything else — a non-semver version, a merge-base or diff failure, or unrecognized output — stops in both directions. Surface the message verbatim. A hard script error never means "keep going".
This is a hard gate, not a judgment call.
在执行其他所有操作之前运行此步骤。下方大多数步骤仅在结果为 yes 时适用。
快速查看仅用于初步定位——绝不能据此决定退出结果:
git diff "origin/<default>...HEAD" --name-only
。最终结论来自 invariant run,其调用契约与合并前门禁强制执行的契约完全相同:
  1. 通过 GitHub 查询默认分支(
    gh repo view
    ),失败时回退到本地
    origin/HEAD
    ref。绝不能猜测默认分支为
    main
    。如果两者都失败,则停止:无法给出结论。
  2. git fetch origin <default>
    必须成功。绝不能基于陈旧的 base 给出结论。
  3. 获取到的
    origin/<default>
    tip 必须是分支 tip 的祖先。如果分支落后,则停止:将分支 rebase 到
    origin/<default>
    上,然后重新进入步骤 0。
  4. 运行脚本时,
    HEAD_SHA
    使用本地分支 tip,
    BASE_SHA
    使用获取到的
    origin/<default>
    tip——绝不能使用预先计算的 merge-base,也绝不能使用自行构造的 two-dot diff。
bash
DEFAULT=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null)
DEFAULT=${DEFAULT:-$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')}
[ -n "$DEFAULT" ] || { echo "cannot resolve the default branch — no verdict"; exit 1; }
git fetch origin "$DEFAULT" || { echo "fetch failed — no verdict"; exit 1; }
git merge-base --is-ancestor "refs/remotes/origin/$DEFAULT" HEAD \
  || { echo "behind base — rebase onto origin/$DEFAULT, re-enter step 0"; exit 1; }
HEAD_SHA=$(git rev-parse HEAD) BASE_SHA=$(git rev-parse "refs/remotes/origin/$DEFAULT") \
  .claude/scripts/version-bump-required.sh
根据精确输出匹配读取结果——信号规则,默认拒绝:
  • 退出码 0,stdout 以
    OK: runtime_changed=false bumped=false
    开头 → 仅涉及开发内容,且流程至此结束。不要递增版本。 跳过下方每一步。保持版本不变,不要切割 changelog,并使用普通 conventional title(
    <type>: <subject>
    )执行 land。如果重新进入此流程时,PR 标题仍带有早先递增遗留的过时
    vX.Y.Z
    前缀,此时应将其移除(
    gh pr edit --title
    )——title backstop 从不移除过时前缀,而这是无需递增的退出路径仍需完成的唯一一项第 8 步操作。随后直接进入 land command。该退出结果必须包含那行 OK——仅凭快速查看绝不能授权跳过递增。
  • 退出码 0,stdout 以
    OK: runtime_changed=true bumped=true
    开头 → 已经完成过递增(属于恢复流程的重新进入)。绝不能再次递增——直接进入 land command。
  • 退出码 1,结论中包含
    cannot merge until version-bump runs at land time
    → 需要执行版本递增。只有在 land 意图前置条件已经通过后,该结论才在此处可执行;如果不在 land 流程中阅读,它仅表示一项合并前置条件,不应触发其他操作。
    • 如果分支上没有
      chore(version)
      提交:继续进入步骤 1。此列表中的其他任何 exit 1 结论都应停止。
    • 如果分支上已经包含
      chore(version)
      提交:之前的递增结果已过时(rebase 移动了 fork point)。停止——丢弃递增提交,撤销 changelog 切割,重置标题,然后重新进入步骤 0。
  • 退出码 1,结论中包含
    must land with no bump
    → 错误地执行了递增。停止——丢弃提交,撤销切割,然后重新运行步骤 0。
  • 其他任何情况——非 SemVer 版本、merge-base 或 diff 失败、无法识别的输出——无论结论朝哪个方向都应停止。逐字呈现其消息。脚本硬错误绝不表示“继续执行”。
这是硬门禁,不是主观判断。

1. Decide the bump level

1. 确定递增级别

Reached only when step 0 said a bump is warranted; the only question is how big the bump is.
bash
undefined
仅在步骤 0 判定需要递增时进入此步骤;唯一需要回答的问题是递增幅度有多大。
bash
undefined

Re-derive the base: shell variables do not persist from step 0's call.

Re-derive the base: shell variables do not persist from step 0's call.

DEFAULT=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null) DEFAULT=${DEFAULT:-$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')} : "${DEFAULT:?cannot resolve the default branch}" git log "origin/$DEFAULT..HEAD" --oneline git diff "origin/$DEFAULT...HEAD" --stat

[SemVer 2.0.0](https://semver.org/spec/v2.0.0.html), quoted verbatim. Items 6,
7, and 8 each carry an `x > 0` precondition:

| Item | Normative rule | Scope |
|------|----------------|-------|
| 6 | PATCH "MUST be incremented if only backward compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior." | `x.y.Z \| x > 0` |
| 7 | MINOR "MUST be incremented if new, backward compatible functionality is introduced to the public API." | `x.Y.z \| x > 0` |
| 8 | MAJOR "MUST be incremented if any backward incompatible changes are introduced to the public API." | `X.y.z \| X > 0` |

If the version starts `0.`, none of those three binds. Item 4 governs: "Major
version zero (`0.y.z`) is for initial development. Anything MAY change at any
time. The public API SHOULD NOT be considered stable." Item 5: "Version 1.0.0
defines the public API." The spec assigns no level pre-1.0, so the decision
below uses the same observable-change rule before and after 1.0.0.

Ask these in order. **The first yes wins.** Judge the *change*, never the
commit subject:

1. **Can a plugin user observe the difference?** → **minor**. Anything that
   changes what they type, what they get back, or what the project does on
   their behalf is observable: a command's name or arguments
   (`argument-hint`), documented behavior, whether a step prompts them, an
   artifact's format or frontmatter schema, an agent's model, or tool access.
   New and changed capability both land here.
2. **Otherwise** → **patch**: internal-only *and* backward compatible — prose
   that clarifies without changing an instruction, a comment, restructuring
   that preserves behavior. Item 6 requires both qualifiers, not just a `fix:`
   subject. Expect patch to be rare for projects that ship instructions a
   model reads. Do not widen patch to make the cadence feel familiar.

**`major` is unreachable while the version starts `0.`** A breaking change
pre-1.0 is a **minor**, not a major. If a change looks like it warrants major,
that is a signal to **ask whether it is time to declare 1.0.0** — a deliberate
decision, never a side effect of this skill.

State the chosen level and which question decided it. A level that needed a
judgment call means the observability question was not actually answered —
answer it rather than asking the user.
DEFAULT=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null) DEFAULT=${DEFAULT:-$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')} : "${DEFAULT:?cannot resolve the default branch}" git log "origin/$DEFAULT..HEAD" --oneline git diff "origin/$DEFAULT...HEAD" --stat

以下引用 [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) 的规范内容。第 6、7、8 项分别带有 `x > 0` 前置条件:

| 项目 | 规范规则 | 适用范围 |
|------|----------------|-------|
| 6 | PATCH“如果只引入向后兼容的 bug 修复,MUST 递增。bug 修复定义为修复错误行为的内部变更。” | `x.y.Z \| x > 0` |
| 7 | MINOR“如果向公共 API 引入新的向后兼容功能,MUST 递增。” | `x.Y.z \| x > 0` |
| 8 | MAJOR“如果对公共 API 引入任何向后不兼容的变更,MUST 递增。” | `X.y.z \| X > 0` |

如果版本以 `0.` 开头,上述三项规则均不适用。第 4 项规定:“主版本零(`0.y.z`)用于初始开发。任何内容都 MAY 随时更改。公共 API SHOULD NOT 被视为稳定。”第 5 项规定:“`1.0.0` 定义公共 API。”该规范未为 1.0.0 之前指定级别,因此下方判定在 1.0.0 前后采用相同的可观察变更规则。

按顺序询问以下问题。**第一个 yes 决定结果。** 判断依据始终是*变更本身*,绝不是提交主题:

1. **插件用户能否观察到差异?** → **minor**。任何改变用户输入内容、返回内容或项目代表其执行行为的变化,都是可观察的:命令的名称或参数(`argument-hint`)、文档所描述的行为、某个步骤是否会提示用户、artifact 的格式或 frontmatter schema、Agent 的模型或工具访问权限。新增能力和变更能力均归入此处。
2. **否则** → **patch**:仅涉及内部且向后兼容——澄清文案但不改变指令、添加注释,或进行保持行为的重构。第 6 项要求同时满足两个条件,而不能仅凭 `fix:` 主题作出判断。对于发布供模型读取的指令的项目,patch 应当很少使用。不要为了让发布节奏显得熟悉而扩大 patch 的适用范围。

**版本以 `0.` 开头时,`major` 不可达。** 1.0.0 之前的破坏性变更是 **minor**,而不是 major。如果某项变更看起来需要 major,这应是一个信号:询问是否已到宣布 `1.0.0` 的时候——这必须是一项有意决策,绝不能成为此 skill 的副作用。

说明所选级别,以及由哪个问题决定。如果某个级别仍需要主观判断,说明可观察性问题实际上没有得到回答;应回答该问题,而不是询问用户。

2. Compute the next version

2. 计算下一个版本

bash
bash .claude/scripts/next-version.sh <level>
bash
bash .claude/scripts/next-version.sh <level>

3. Bump every version string

3. 递增所有版本字符串

The version lives in every file below:
FileOccurrences
.claude-plugin/plugin.json
1 (
version
)
.claude-plugin/marketplace.json
2 (
metadata.version
and
plugins[0].version
)
.codex-plugin/plugin.json
1 (
version
)
.cursor-plugin/plugin.json
1 (
version
): what Cursor reports
package.json
1 (
version
)
plugin.json
(repo root)
1 (
version
): what Antigravity reports
Each host shows the version in its own manifest: Codex reads
.codex-plugin/plugin.json
in preference to the Claude manifest; Antigravity reads the root
plugin.json
, which sits at the root rather than in a directory of its own because that host resolves
skills/
and
agents/
as siblings of its manifest. A stale one makes the same release look like two different versions depending on the host.
Edit every file, then prove it:
bash
grep -rn '"version"' package.json plugin.json .claude-plugin/plugin.json \
  .claude-plugin/marketplace.json .codex-plugin/plugin.json .cursor-plugin/plugin.json
Every line must show the new version. None may still show the old one.
版本出现在下列每一个文件中:
文件出现次数
.claude-plugin/plugin.json
1(
version
)
.claude-plugin/marketplace.json
2(
metadata.version
和
plugins[0].version
)
.codex-plugin/plugin.json
1(
version
)
.cursor-plugin/plugin.json
1(
version
):Cursor 显示的版本
package.json
1(
version
)
仓库根目录的
plugin.json
1(
version
):Antigravity 显示的版本
每个 host 都在自己的 manifest 中显示版本:Codex 会优先读取
.codex-plugin/plugin.json
,而不是 Claude manifest;Antigravity 读取仓库根目录的
plugin.json
。后者之所以位于根目录而不是独立目录中,是因为该 host 会将
skills/
和
agents/
解析为其 manifest 的同级目录。任何一个版本未同步,都会让同一次 release 在不同 host 上看起来像是两个不同版本。
编辑每个文件,然后进行验证:
bash
grep -rn '"version"' package.json plugin.json .claude-plugin/plugin.json \
  .claude-plugin/marketplace.json .codex-plugin/plugin.json .cursor-plugin/plugin.json
每一行都必须显示新版本。不得有任何一行仍显示旧版本。

4. Cut the changelog section

4. 切割 changelog 章节

In
CHANGELOG.md
(Keep a Changelog format, entry style per
skills/team-pr/references/changelog.md
):
  • Move the entire
    [Unreleased]
    body into a new
    ## [X.Y.Z] - YYYY-MM-DD
    (today's date) section inserted directly below
    ## [Unreleased]
    . Leave
    ## [Unreleased]
    in place, now empty again.
  • Re-point the link-reference footer:
    • [Unreleased]
      compare base →
      vX.Y.Z...HEAD
    • Add
      [X.Y.Z]: https://github.com/<owner>/<repo>/compare/v<prev>...vX.Y.Z
This section becomes the GitHub release notes verbatim — write it for a reader deciding if the upgrade is worth it. Any links must be absolute URLs: relative paths (e.g.
docs/versioning.md
) render as dead links on the release page.
Empty
[Unreleased]
.
A PR that reached this step changed runtime files; an empty
[Unreleased]
on it means nobody wrote the user-facing bullet. Derive at least one bullet from the PR's runtime commits (
feat:
/
fix:
/
perf:
/
security:
, in that entry style). Never write an empty section (
release-on-merge.yml
errors on empty release notes).
Empty
[Unreleased]
and no runtime change is not this case: that PR must have stopped at step 0, with no bump and no changelog cut. Do not invent a bullet to justify a bump that step 0 already declined. Go back and land plain.
在
CHANGELOG.md
中(采用 Keep a Changelog 格式,条目样式遵循
skills/team-pr/references/changelog.md
):
  • 将整个
    [Unreleased]
    正文移入一个新的
    ## [X.Y.Z] - YYYY-MM-DD
    (今天的日期)章节,并直接插入在
    ## [Unreleased]
    下方。保留
    ## [Unreleased]
    ,但将其内容重新置空。
  • 重新指向链接引用页脚:
    • 将
      [Unreleased]
      的 compare base 指向
      vX.Y.Z...HEAD
    • 添加
      [X.Y.Z]: https://github.com/<owner>/<repo>/compare/v<prev>...vX.Y.Z
该章节将逐字成为 GitHub release notes——应面向正在判断升级是否值得的读者来编写。任何链接都必须是绝对 URL:相对路径(例如
docs/versioning.md
)在 release 页面中会呈现为失效链接。
空的
[Unreleased]
。
到达此步骤的 PR 改动了 runtime 文件;此时
[Unreleased]
为空,说明没有人编写面向用户的条目。至少根据该 PR 的 runtime 提交推导出一条条目(
feat:
/
fix:
/
perf:
/
security:
,并遵循相应条目样式)。绝不编写空章节(
release-on-merge.yml
会在 release notes 为空时报错)。
[Unreleased]
为空且没有 runtime 变更并不属于此情况:这种 PR 必须已经在步骤 0 停止,不递增版本,也不切割 changelog。不要发明条目来为步骤 0 已拒绝的递增寻找理由。返回并按普通方式 land。

5. Land-time consistency assertion

5. Land-time 一致性断言

Run it after the changelog cut (so the dated section exists) and before committing:
bash
bash .claude/scripts/check-version-consistency.sh
node -e "['.claude-plugin/plugin.json','.claude-plugin/marketplace.json','.codex-plugin/plugin.json','.cursor-plugin/plugin.json','.agents/plugins/marketplace.json','plugin.json','package.json'].forEach(f=>JSON.parse(require('fs').readFileSync(f)));console.log('JSON OK')"
The released-section and footer-compare-link invariants hold only after the cut, so assert them inline here:
bash
V=$(jq -r .version .claude-plugin/plugin.json)
OWNER_REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
ESC=$(sed 's/\./\\./g' <<<"$V")
grep -qE "^## \[$ESC\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md \
  || { echo "::error::no '## [$V] - YYYY-MM-DD' section — the cut did not land"; exit 1; }
grep -qE "^\[$ESC\]: https://" CHANGELOG.md \
  || { echo "::error::no footer compare link for $V"; exit 1; }
grep -q "\[Unreleased\]: https://github.com/$OWNER_REPO/compare/v$V...HEAD" CHANGELOG.md \
  || { echo "::error::[Unreleased] footer does not compare from v$V"; exit 1; }
echo "OK: land-time consistency holds"
If any check fails, stop before committing and fix the cut.
在切割 changelog 之后(这样带日期的章节才会存在)且提交之前运行:
bash
bash .claude/scripts/check-version-consistency.sh
node -e "['.claude-plugin/plugin.json','.claude-plugin/marketplace.json','.codex-plugin/plugin.json','.cursor-plugin/plugin.json','.agents/plugins/marketplace.json','plugin.json','package.json'].forEach(f=>JSON.parse(require('fs').readFileSync(f)));console.log('JSON OK')"
发布章节和页脚 compare 链接的不变量只有在切割完成后才成立,因此在此处内联断言:
bash
V=$(jq -r .version .claude-plugin/plugin.json)
OWNER_REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
ESC=$(sed 's/\./\\./g' <<<"$V")
grep -qE "^## \[$ESC\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md \
  || { echo "::error::no '## [$V] - YYYY-MM-DD' section — the cut did not land"; exit 1; }
grep -qE "^\[$ESC\]: https://" CHANGELOG.md \
  || { echo "::error::no footer compare link for $V"; exit 1; }
grep -q "\[Unreleased\]: https://github.com/$OWNER_REPO/compare/v$V...HEAD" CHANGELOG.md \
  || { echo "::error::[Unreleased] footer does not compare from v$V"; exit 1; }
echo "OK: land-time consistency holds"
如果任何检查失败,必须在提交前停止并修复切割结果。

6. Commit

6. 提交

Commit the bump as its own commit in the PR branch:
bash
git add .claude-plugin/plugin.json .claude-plugin/marketplace.json \
  .codex-plugin/plugin.json .cursor-plugin/plugin.json plugin.json package.json \
  CHANGELOG.md
git commit -m "chore(version): X.Y.Z"
在 PR 分支中将版本递增作为独立提交:
bash
git add .claude-plugin/plugin.json .claude-plugin/marketplace.json \
  .codex-plugin/plugin.json .cursor-plugin/plugin.json plugin.json package.json \
  CHANGELOG.md
git commit -m "chore(version): X.Y.Z"

7. Assert the bump invariant

7. 断言递增不变量

Re-run the invariant run from step 0 — the head is now the branch tip carrying the
chore(version)
commit. Require exit 0 with stdout starting
OK: runtime_changed=true bumped=true
.
This runs before any remote change (the title edit in step 8 is remote). On any other outcome, stop: drop the
chore(version)
commit, undo the changelog cut, and land plain — nothing has left the machine, so the recovery is purely local.
重新运行步骤 0 中的 invariant run——此时 HEAD 是包含
chore(version)
提交的分支 tip。要求退出码为 0,且 stdout 以
OK: runtime_changed=true bumped=true
开头。
此检查在任何远程变更之前运行(第 8 步中的标题编辑属于远程变更)。如果出现任何其他结果,则停止:丢弃
chore(version)
提交,撤销 changelog 切割,并按普通方式 land——此时尚未有任何内容离开本机,因此恢复操作完全可以在本地完成。

8. Title the PR

8. 设置 PR 标题

vX.Y.Z <type>: <subject>
— e.g.
v1.4.0 feat: add a design review gate
. Set it on the existing PR (
gh pr edit --title
). The
PR title sync
workflow is a backstop — do not rely on it.
A re-entry path that runs after a title already exists must reset it: the stale-bump recovery re-titles with the recomputed version, and a re-entry that ends at "no bump" strips the
vX.Y.Z
prefix explicitly — the title backstop never strips a stale prefix.
Return to the land command to push, wait for CI, and squash-merge.
vX.Y.Z <type>: <subject>
——例如
v1.4.0 feat: add a design review gate
。在现有 PR 上设置该标题(
gh pr edit --title
)。
PR title sync
workflow 只是兜底机制——不要依赖它。
如果重新进入流程时标题已经存在,则必须重置标题:递增结果过期的恢复流程应使用重新计算的版本重设标题;以“无需递增”结束的重新进入流程则应显式移除
vX.Y.Z
前缀——title backstop 从不移除过时前缀。
返回 land command,继续推送、等待 CI,并执行 squash-merge。