关于 git-release-finish
git-release-finish 是用于 Git 版本发布收尾的工作流技能,面向需要将发布分支合并到主开发分支的团队。它按阶段检查远程状态、标签、合并约束、冲突和残留文件,再执行合并并生成报告。
主要功能
- 01检查冲突标记等残留内容
- 02分析标签命名并验证远程提交
- 03识别主开发分支及合并限制
- 04创建 MR 或 PR 并检查冲突
- 05按情况清理分支额外文件
- 06合并并输出发布报告
适用场景
- 01完成发布分支到主分支的合并
- 02在多仓库中协调版本发布
- 03检查合并冲突及分支残留后再发布
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ fudesign2008/ open-skills --skill git-release-finish安装后在 Claude Code 中输入/git-release-finish确认已加载。
附带文件references/cross-release-sync.mdreferences/ff-cherry-pick.md
详细介绍
Git Release Finish
前置技能检查
加载时,验证 report-output-discipline 是否可用。如果缺失,则中止并打印:
npx skills add FuDesign2008/open-skills -g --skill '*' --yes
不要默默使用替代方案。
角色:迭代结束时的发布工作流——为发布分支打标签,向主分支创建 MR/PR,解决冲突,清理多余文件并合并。Git 操作使用原生命令表示;MR/PR 操作使用意图表示,由正在运行的 Agent 根据其环境选择平台 CLI(
glab/gh/tea/ 等效工具)。配对技能:
git-release-start(迭代开始时创建发布分支)↔git-release-finish(本技能)。依赖:第 6 阶段的冲突解决工作委托给
git-conflict-resolve技能(语义分析、多轮聚合)。远程优先原则:在执行任何本地操作前确认远程状态。打标签前,运行
git fetch并验证远程提交 SHA。绝不要为未经验证的本地 HEAD 打标签——它可能落后于远程。若平台支持,优先通过远程 API 创建标签(避免本地状态漂移);否则,在已验证的远程 SHA 上本地创建标签。
多仓库规范
单仓库运行可以跳过本节。多仓库运行(≥2 个仓库)必须遵守以下三条规则——每条都源自真实的事后复盘。
M.1 — 明确隔离工作目录。 工具调用层不会持久保留仓库上下文。在仓库 A 中执行 cd 后,下一个不带 cd 的 Bash 调用仍会在 A 中运行。多仓库并行发布若未明确执行 cd,就可能在错误的仓库中创建 MR、对错误的分支执行 merge-tree,或合并错误的 MR。多仓库运行中的每个命令块都必须以 cd <REPO_PATH> && 开头,即使你确信当前已在该目录中。每个确认表(阶段 1/2/3/3.5 的输出)都必须包含 Repo 列。
M.2 — 跨仓库传播故障模式。 当某个仓库遇到环境限制(ff 合并模式、受保护分支规则、CI 流水线要求、CLI 身份验证失败)时,立即在所有尚未进入第 9 阶段的仓库中执行相同检查。这是补查,并非主流程的重复;阶段 3.5 已并行检查所有仓库。
| 仓库 A 发现的问题 | 在仓库 B(及所有尚未合并的仓库)中执行 |
|---|---|
merge_method = ff | 阶段 3.5 指纹检查;预期采用 cherry-pick 策略 |
| 发布分支受保护(push=No one) | 检查 B 的发布分支保护规则;提前规划受保护分支的回退方案 |
| CLI 身份验证失败 | 重新运行平台的 auth status |
| 流水线失败阻止合并 | 检查 B 的 CI 配置和流水线状态 |
M.3 — 并行/串行边界。 与仓库无关的操作并行执行(打标签、创建 MR/PR、冲突试运行)。冲突解决(阶段 6)必须严格串行执行——同一时间只能有一个仓库的 worktree 处于冲突状态。
适用 / 不适用场景
适用于(单仓库或多仓库):标签命名不明确或发生过演变;主分支名称不确定(各仓库可能使用 master / main / develop);release → main 合并可能存在代码冲突;MR/PR 分支可能包含杂散文件(AI 工具配置、临时脚本);需要生成结构化的发布报告。
不适用于:非 Git 托管服务(SVN、Mercurial 等)。
必须端到端执行整个流程。无论仓库数量或发布看起来有多“简单”,每个阶段都必须执行。“这次看起来很简单”是跳过预检、将残留内容带入 main 的最常见原因。
前置条件:已安装并完成身份验证的平台 CLI(开始前验证身份验证状态);每个仓库的远程地址均可访问;已与用户确认版本号(例如 8.2.60)。
阶段图
| 阶段 | 操作 | 关键工具 |
|---|---|---|
| 0 | 前置健康检查(残留标记扫描) | git grep + git log -S |
| 1 | 分析标签命名约定 | git tag |
| 2 | 创建并推送标签(优先远程) | git fetch / git tag / git push / 远程 API |
| 3 | 确定主要开发分支 | git remote show + git log |
| 3.5 | 环境指纹检查(merge_method、保护规则、ff 检测) | 平台 API + git merge-base --is-ancestor |
| 4 | 创建 MR/PR | 平台 CLI |
| 5 | 冲突演练 | git merge-tree |
| 5.5 | 验证 MR 是否可合并(merge-tree=0 后必须执行) | 平台 CLI/API |
| 6 | 解决冲突(如有) | git-conflict-resolve 技能 |
| 7 | 清理 MR/PR 分支中的额外文件(仅合并模式) | git ls-tree + comm |
| 8 | 残留标记检查关卡(无条件执行) | git grep |
| 9 | 合并 MR/PR | 平台 CLI |
| 10 | 输出报告 | — |
阶段 0、3.5 和 8 均为无条件执行——每条路径都会运行。阶段 0 是前置保障,阶段 3.5 是合并约束关卡,阶段 8 是最终合并前关卡。无论阶段 5 是否检测到冲突,这些阶段都会执行。
执行路径
| 情况 | 路径 |
|---|---|
| merge-tree=0 且 MR 可合并 | 0→1→2→3→3.5→4→5→5.5→8→9→10(跳过 6/7) |
| merge-tree=0,但 MR 不可合并 | 0→1→2→3→3.5→4→5→5.5→6(rebase)→8→9→10(跳过 7) |
| 冲突数 > 0,源分支 ≤ 3 个提交且 ≤ 1 个冲突文件 | 0→1→2→3→3.5→4→5→6(rebase)→8→9→10(跳过 7) |
| 冲突数 > 0,其他所有情况 | 0→1→2→3→3.5→4→5→6(merge)→7→8→9→10 |
| 阶段 3.5 检测到 ff 模式 + 非快进 | 0→1→2→3→3.5(cherry-pick)→8→9→10(跳过默认 MR 阶段 4 / 5 / 5.5 / 6 / 7;ff cherry-pick 会创建 sync-release MR——参见 references/ff-cherry-pick.md) |
阶段 3.5 是每条路径上的必经节点。其判断结果决定后续流程采用默认的合并/变基分支(路径 1–4),还是 ff cherry-pick 分支(路径 5)。
阶段 0:发布前健康检查(残留标记扫描)
无条件执行。 每个仓库都必须在阶段 1 之前运行。如果工作树或近期提交中已包含冲突标记,请在发布前清理,否则这些残留会通过合并进入 main。
为何需要此步骤:阶段 5 的 git merge-tree 只能检测未来合并产生的新冲突;它无法发现已提交到分支历史中的冲突标记(这是 AI 自动解决失败后仍被推送时常见的遗留问题)。如果没有阶段 0,这些标记就会随合并进入 main。
精确的正则表达式(本技能后续的每次扫描都会重复使用):
^<{7,} |^={7,}$|^>{7,} |^\|{7,}
可匹配标准的 7 字符标记、非标准的 8 个及以上字符标记,以及 diff3 的 ||||||| base 标记。git grep 会自动遵循 .gitignore,因此不会扫描未跟踪的构建产物。^={7,}$ 要求行尾之前只能是纯 =,因此不会匹配 /* ====== */ 这类 CSS 注释;^<{7,} 要求 7 个及以上 < 后跟一个空格,因此不会匹配普通的 < 运算符。
残留判定——先检查成对标记(权威规则;本技能和 git-conflict-resolve 中的每次扫描都采用相同语义):上述正则表达式用于匹配,而非判定。只有当同一文件还包含开头或结尾标记 ^<{7,} / ^>{7,} / ^\|{7,} 时,匹配才算真正的残留。单独出现的纯 = 行属于误报(例如用 =============== 作为下划线的第三方许可证标题 The MIT License,以及 Markdown 分隔线)。命中不自动意味着存在残留——判定依据是标记是否成对出现。
L4a——工作树扫描:
git grep -lE '^<{7,} |^={7,}$|^>{7,} |^\|{7,} ' 2>/dev/null
L4a 分流——区分真正的残留与纯 = 误报:
git grep -lE '^<{7,} |^={7,}$|^>{7,} |^\|{7,} ' 2>/dev/null | while read -r f; do
if git grep -qE '^<{7,} |^>{7,} |^\|{7,} ' -- "$f" 2>/dev/null; then
echo "TRUE RESIDUE: $f"
else
echo "false positive (pure '=' lines only): $f"
fi
done
L4b——历史合并提交 pickaxe 扫描:
git log --all --merges --since="30 days ago" \
-S'^<<<<<<< ' --pickaxe-regex \
--format="COMMIT: %h %s" --name-only 2>/dev/null
| 结果 | 操作 |
|---|---|
| L4a 和 L4b 均为空 | 状态正常;继续阶段 1 |
| L4a 分流发现真正的残留,或 L4b 非空 | 中止。清理工作树(解决冲突或检出干净副本)并重新运行 |
L4a 匹配项全是纯 = 行(没有 < / > / | 标记) | 误报。在扫描输出中记录;继续阶段 1 |
不要“先发布,之后再清理”。main 中的残留会传播到基于错误内容构建的下游 CI/CD 流程。
阶段 1:分析标签命名约定
切勿假定标签格式统一。请分别分析每个仓库。
git tag --sort=-version:refname | head -30
| 历史模式 | 约定 |
|---|---|
8.2.52、8.2.51 | 纯版本号:{VERSION} |
v8.2.40、v8.2.20 | v 前缀:v{VERSION} |
desktop-8.2.50、desktop-8.2.40 | 产品前缀:desktop-{VERSION} |
mobile-7.5.720 | 产品前缀:mobile-{VERSION} |
多产品仓库:检查完整标签列表(而不只是最新的 10 个),确认是否存在任何 product- 前缀。每条产品线都有自己的标签;本次发布只为当前产品线打标签。
标签约定可能会变化。同一仓库过去可能使用
v8.2.60,之后改为8.2.63。遵循最新的标签;不要因为旧标签带有前缀,就假定必须使用前缀。如果历史记录内部不一致,请询问用户本次发布应采用哪种约定。
输出(确认表):
| 仓库 | 发布分支 | 标签名称 | 最新历史示例 |
|---|---|---|---|
| repo-A | release/X.Y.Z | X.Y.Z | 8.2.52 |
| repo-B | release/X.Y.Z | vX.Y.Z | v8.2.40 |
在进入阶段 2 之前,与用户确认标签名称。
阶段 2:创建并推送标签(先远程)
2.0 预检查(所有平台,必须执行)
针对每个仓库并行运行:
git fetch origin <RELEASE_BRANCH>
REMOTE_SHA=$(git rev-parse origin/<RELEASE_BRANCH>)
echo "remote $RELEASE_BRANCH HEAD: $REMOTE_SHA"
# Fork detection: is local behind remote?
LOCAL_SHA=$(git rev-parse HEAD)
BEHIND=$(git rev-list --count HEAD..origin/<RELEASE_BRANCH> 2>/dev/null || echo 0)
[ "$BEHIND" -gt 0 ] && echo "Local is $BEHIND behind remote; tag will anchor to remote SHA, not local HEAD"
# Tag-already-exists check
git ls-remote --tags origin | grep -q "refs/tags/<TAG_NAME>$" \
&& { echo "Tag <TAG_NAME> already exists on remote; aborting"; exit 1; }
2.1 创建标签
创建标签时,优先使用远程 API 创建(如果平台支持),以避免本地状态偏差;无论采用哪种方式,都要以已验证的 $REMOTE_SHA 为基准。
- 远程 API 方式(可用时优先):调用平台的“创建标签”端点,传入
tag_name=<TAG_NAME>、ref=$REMOTE_SHA、message="Release <VERSION>"。无需执行git push——标签会直接在远程创建。(如果 CLI 的api子命令会拆分参数,请参阅“常见错误”。) - 本地创建方式(其他情况):
git tag <TAG_NAME> $REMOTE_SHA && git push origin <TAG_NAME>。标签对象以$REMOTE_SHA为基准,而不是本地 HEAD。
2.2 验证标签指向正确的提交
REMOTE_SHA=$(git rev-parse origin/<RELEASE_BRANCH>)
TAG_SHA=$(git ls-remote origin refs/tags/<TAG_NAME> | awk '{print $1}')
echo "tag <TAG_NAME> -> $TAG_SHA"
echo "expected -> $REMOTE_SHA"
# Annotated tags return the tag-object SHA from ls-remote; dereference with:
# git ls-remote origin "refs/tags/<TAG_NAME>^{}"
如果 TAG_SHA ≠ REMOTE_SHA(对附注标签解引用后),说明标签指向了错误的提交——删除后重新创建(请参阅“常见错误”)。
输出:每个仓库一张表,列出标签名称、远程 SHA、已验证的标签 SHA 和状态。
阶段 3:确定主开发分支
主分支可能是
master、main或develop,且不同仓库可能各不相同。
四层证据,按可信度排序:
- 远程合并历史(最有力):
git log --oneline --merges -20 | grep -E "into '(master|main|develop)'"
历史记录中的“Merge branch 'release/X.Y.Z' into 'master'”可揭示目标分支。
-
远程 HEAD(
git ls-remote --symref origin HEAD是实时且权威的查询;git remote show origin也是实时查询,但经常超时;git symbolic-ref refs/remotes/origin/HEAD是本地缓存,可能已过时)。如果结果不一致,优先采用实时查询。 -
分支命名行为和已合并 MR 的分布:
sync-release/*-to-<X>同步分支会在分支名称中标明目标(git branch -a | grep sync-release/);如果所有版本发布 MR 都合并到了同一个目标分支,这种分布就是行为证据(通过平台 API 筛选目标分支为target_branch的已合并 MR)。 -
用户确认(备用方案):当来源相互冲突或没有历史记录时,列出候选项并询问。
候选分支可能是仍在使用的并行分支,而非已弃用的主干分支。 名为
master的分支可能是仍在使用的并行 CI 分支——它有自己的 CI 配置,使用不同的镜像和安装步骤,由分支名称独立触发,并且有其他贡献者的近期提交,而主干分支则是main。在将候选分支视为已弃用分支或清理它之前,先验证这一点:独立的 CI 配置或其他贡献者的近期提交都意味着它是仍在使用的并行分支。不要清理仍在使用的并行分支;在阶段 10 报告中记录其实际用途。
远程 HEAD 可能指向已弃用的分支。迁移或分支策略变更后,远程 HEAD 可能仍指向旧主分支。如果远程 HEAD 指向的分支比另一个候选分支落后数百个提交,就应视为远程 HEAD 已过时,并依据合并历史、向用户确认:
echo "HEAD candidate ahead of main: $(git rev-list --count origin/main..origin/<HEAD_BRANCH> 2>/dev/null)"
echo "main ahead of HEAD candidate: $(git rev-list --count origin/<HEAD_BRANCH>..origin/main 2>/dev/null)"
输出:每个仓库一张表,列出源分支、目标主分支和证据来源。进入阶段 4 前与用户确认一致。
第 3.5 阶段:环境指纹(无条件)
无条件执行。 创建任何 MR/PR 之前,都要收集每个代码库的合并限制。这道关卡可以避免经典陷阱:“merge-tree=0,
can_be_merged=true,但mr merge返回 406”。
为什么需要这一步:第 5 阶段的 merge-tree 和第 5.5 阶段的 can_be_merged 检查,只能判断代码层面的可合并性,无法识别项目级合并限制。最常见的陷阱是 GitLab merge_method = ff(仅快进):release 不是 main 的后代(release 分支创建后 main 有了新提交)→ git merge-tree 报告 0 个冲突 → MR 的 can_be_merged=true(存在合并结果;但不会验证 merge_method 限制)→ mr merge 返回 406 "Branch cannot be merged",因为 ff 模式拒绝生成合并提交,而 release 无法快进到 main。第 5/5.5 阶段无法发现这个陷阱,直到第 9 阶段才会暴露。第 3.5 阶段将这道障碍提前处理。
检查项(每个代码库并行执行):
- merge_method / 合并提交许可 — 通过平台的 API 查询项目的
merge_method(GitLab),或allow_merge_commit/allow_squash_merge/allow_rebase_merge(GitHub),以检测是否为仅 ff 模式。 - release 分支保护规则 — 查询
release/*和*的protected_branches;记录push_access_levels和merge_access_levels,以检测 push=No one。当git-branch-protect可用时,也通过glab或gh运行其只读的check-trunk。若发现不匹配,报告 "push denied, maintainer merge allowed"。不要运行set-trunk。若该技能不可用,保留此指纹并继续。 - CI 门禁 — 查询
only_allow_merge_if_pipeline_succeeds(GitLab)或等效设置。 - 后代检查(ff 模式判定):
git merge-base --is-ancestor origin/<TARGET_BRANCH> origin/<RELEASE_BRANCH> \
&& echo "Descendant: yes (ff possible, default flow)" \
|| echo "Descendant: no (non-fast-forward; ff-mode repos must use cherry-pick)"
策略表:
| merge_method | release 是 target 的后代? | 策略 |
|---|---|---|
merge / rebase_merge | 任意 | 默认合并策略(第 6 阶段合并模式) |
squash | 任意 | 第 9 阶段移除 --squash=false(项目强制 squash)。release 上的 N 个提交会在 main 上变成 1 个 squash 提交;在第 10 阶段报告中记录这一点 |
ff | ✅ 是(可以快进) | 默认流程;mr merge 直接成功 |
ff | ❌ 否(不能快进) | Cherry-pick 策略 — 参见 references/ff-cherry-pick.md |
ff + 不能快进是最常见的陷阱。release 分支通常会与 main 分叉(两边都有新提交),因此 ff 模式的代码库默认采用 cherry-pick。不要等到第 9 阶段遇到 406。
GitHub 中的对应陷阱:GitHub 没有 merge_method=ff 开关,而是提供三个布尔值。当 allow_merge_commit=false && allow_squash_merge=false && allow_rebase_merge=true(仅 rebase)时,PR 合并会重写提交并改变其哈希值——效果等同于仅 ff。此配置下,将 release → main 的 PR 视同 ff 模式处理;跨 release 同步应遵循 references/cross-release-sync.md 中的 rebase/哈希变更路径。
输出:每个代码库的环境指纹表(merge_method、release 保护规则、CI 门禁、选定策略)。若 ff 模式或保护规则异常,应在进入第 4 阶段前与用户确认。
受保护分支 + 不能快进的组合:如果 release 受保护(push=No one),无论
merge_method为何,第 6 阶段 rebase 模式的强制推送都会失败。将策略列标记为“受保护分支回退”,以便第 6 阶段采用回退路径。
阶段 4:创建 MR/PR
按仓库并行执行。通过平台的 CLI 创建从 <RELEASE_BRANCH> 到 <MAIN_BRANCH> 的合并请求(Agent 根据其环境选择 glab / gh / tea)。标题:Release <VERSION>。保留源分支(--remove-source-branch=false / --delete-branch=false)。记录每个 MR/PR 的 ID 和 URL。
如果阶段 3.5 选择了 ff cherry-pick 策略,则跳过此阶段,改为遵循
references/ff-cherry-pick.md——MR 的源分支将是sync-release/<VERSION>-to-<TARGET>,而不是原始发布分支。
阶段 5:冲突检测(试运行,不修改文件)
按仓库并行执行:
BASE=$(git merge-base origin/<RELEASE_BRANCH> origin/<MAIN_BRANCH>)
# Content conflict count
git merge-tree $BASE origin/<RELEASE_BRANCH> origin/<MAIN_BRANCH> 2>&1 \
| grep -c "^changed in both"
# Branch divergence
echo "release ahead: $(git rev-list --count origin/<MAIN_BRANCH>..origin/<RELEASE_BRANCH>)"
echo "main ahead: $(git rev-list --count origin/<RELEASE_BRANCH>..origin/<MAIN_BRANCH>)"
git merge-tree只统计文本内容冲突,不检测重命名/重命名冲突(这在构建产物哈希变更时很常见)。即使冲突数为 0,如果仓库中有构建产物,也要运行阶段 5.5。
结果分类:
| 内容冲突 | 源提交数 | 策略 |
|---|---|---|
| 0 | 任意 | 创建 MR 后,验证是否可合并(阶段 5.5);仅当可合并时才继续执行阶段 9 |
| 0(但 MR 实际上不可合并) | 任意 | Rebase 策略(阶段 6) |
| > 0 | ≤ 3 且 ≤ 1 个冲突文件 | Rebase 可接受(逐提交解决冲突的成本较低) |
| > 0 | 其他所有情况 | Merge 策略(阶段 6) |
merge-tree=0 陷阱:文本合并干净,不代表 MR 可合并。结构性分歧仍可能导致平台拒绝合并。阶段 5.5 是强制步骤。
Rebase 陷阱:Rebase 会在每个有冲突的提交处停止。一个有 80 个提交的分支意味着要中断数十次。除非源分支很短(≤ 3 个提交)且冲突极少,否则优先选择 Merge。
阶段 5.5:验证 MR 是否可合并
只要 merge-tree 报告为 0,就必须执行。merge-tree=0 ≠ MR 可合并。
通过平台的 CLI/API 检查 MR 的 mergeable / can_be_merged 状态。然后从上到下查看表格——第一个匹配的行优先(各行互斥):
| 结果 | 操作 |
|---|---|
can_be_merged / MERGEABLE,且阶段 3.5 未标记 ff 陷阱 | 继续执行阶段 9 |
不可合并(conflict / UNMERGEABLE) | 切换到 Rebase 策略(阶段 6);不要继续执行阶段 9 |
can_be_merged,但阶段 3.5 标记了 ff 模式 + 非快进 | **阶段 9 的 mr merge 仍会返回 406。**切换到 references/ff-cherry-pick.md 中的 cherry-pick 策略;不要继续执行阶段 9 |
卡在 checking(见下方的解除卡住状态流程) | 平台仍在重新计算——运行解除卡住状态流程;不要将其判定为不可合并 |
can_be_merged仅表示“代码没有冲突,并且存在合并结果”。它不会绕过merge_method=ff或其他项目级限制。完整机制和权威判断以阶段 3.5 为准。
解除卡在 checking 状态的情况
多仓库运行中最常遇到这种情况:前一个 MR 刚更新了目标引用,平台的异步重新检查队列因此排在其后。当 merge_status 连续 N 次轮询(默认 N=6,间隔 4 秒)都保持 checking,同时 has_conflicts=false 且 state=opened 时:
- 调用平台的可合并性重新计算端点(GitLab:
GET /projects/:id/merge_requests/:iid/merge_ref),强制重新计算。 - 等待约 3 秒,然后再次检查
merge_status。 - 如果变为
can_be_merged→ 按上表继续处理。 - 如果仍为
checking→ 继续轮询;只有在多轮重新计算和等待后仍无变化,才将其视为平台异常并告知用户。
卡住的 checking 表示计算尚未完成,而不是合并判定——它绝不意味着“不可合并”。
阶段 6:冲突解决(委托给 git-conflict-resolve)
当阶段 5/5.5 检测到冲突时,委托给 git-conflict-resolve 技能——传入 source=<RELEASE_BRANCH>、target=<MAIN_BRANCH>、version=<VERSION>,以及 mode=merge(默认)或 mode=rebase(仅当阶段 5 将 source 判定为 ≤ 3 个提交且 ≤ 1 个冲突文件时)。
构建产物(
dist/、resources/<bundle>/、哈希分块)会由git-conflict-resolve的构建产物自动快捷处理机制自动跳过,无需额外参数。
如果 git-conflict-resolve 无法完成(用户中止、冲突无法解决或 rebase 中断),停止。保持工作树现状以供人工处理;不要继续后续阶段。
git-conflict-resolve 完成并输出全局审查清单后,待用户确认,再根据模式分支:
Merge 模式——推送 merge 分支并替换 MR:
git push origin merge-release/<VERSION>
# Close the conflict-stalled MR via your platform CLI.
# Create a new MR: source=merge-release/<VERSION>, target=<MAIN_BRANCH>.
Rebase 模式——将 rebase 结果推回 release 分支(原 MR 会自动更新):
git push origin rebase-release/<VERSION>:<RELEASE_BRANCH> --force-with-lease
如果远程分支已更新,
--force-with-lease会拒绝推送,以防意外覆盖他人的工作。强制推送前,请向用户确认:“是否有其他人正在基于此分支工作?请确认强制推送。”
受保护分支回退方案(强制推送因 ! [remote rejected] 或 pre-receive hook declined 被拒绝时):
- 将 rebase 结果推送到新分支(普通推送,不强制):
git push origin rebase-release/<VERSION> - 关闭原 MR
- 创建新的 MR,源分支为
rebase-release/<VERSION>,目标分支为<MAIN_BRANCH>
原 release 分支仍停留在 rebase 前的提交上。之后任何 release → main 同步都必须考虑哈希分叉(参见 references/cross-release-sync.md)。
Rebase 跳过审计——如果 rebase 输出了 warning: skipped previously applied commit <SHA>:
git show <SKIPPED_SHA> --stat --oneline
git log origin/<MAIN_BRANCH> --oneline --grep="<keyword>" | head -5
diff <(git show <SKIPPED_SHA> --format=) <(git show <TARGET_SHA> --format=)
| diff 结果 | 操作 |
|---|---|
| 相同 | 可安全跳过;无需操作 |
| 不同 | 被跳过的提交并未完全包含;手动 cherry-pick <SKIPPED_SHA> |
阶段 7:清理 MR/PR 中的多余文件(仅 merge 模式)
Rebase 模式下跳过——rebase 不会带入目标分支的多余文件。仅适用于 merge 模式,此时阶段 6 生成了
merge-release/<VERSION>。
存在于 merge-release 中、但 release 和 target 中均不存在的文件,是意外暂存的本地文件(通常是在解决冲突时运行 git add -A 所致):
comm -23 \
<(git ls-tree -r HEAD --name-only | sort) \
<(sort \
<(git ls-tree -r origin/<RELEASE_BRANCH> --name-only) \
<(git ls-tree -r origin/<TARGET_BRANCH> --name-only) \
| uniq)
常见问题文件:AI 工具配置(.claude/、.omc/、.codemap/、opencode.json)、临时调试文件(ci-build-error.txt、ci-fix-log.txt)。存在于 release 或 target 任一分支中的文件都是预期文件——不要删除。
删除并 amend:
git rm --cached -rf <EXTRA_PATH_1> <EXTRA_PATH_2> # if "pathspec not found": git add -A first, then retry
git commit --amend --no-edit --no-verify
git push origin merge-release/<VERSION> --force-with-lease
重新运行 comm -23 检查;计数必须为 0。
阶段 8:残留标记门禁(无条件执行)
无条件门禁。 无论阶段 5/6/7 执行了什么,都必须在阶段 9 之前运行。这是最后一道防线——即使
git-conflict-resolve之前的所有防护都失效,阶段 8 也能捕获带有冲突标记的提交。
8.1 残留标记扫描(使用阶段 0 的正则表达式)
范围为 git diff --name-only(不是整个仓库)——只有合并更改的文件可能带有新的残留标记。扫描整个仓库会被 CSS ========= 注释、ASCII 艺术字、许可证文件和 Markdown 分隔线产生的大量误报淹没。判定优先检查标记组合(阶段 0):没有 <、> 或 | 标记的纯 = 行是误报,不是残留标记。
BASE=$(git merge-base origin/<MAIN_BRANCH> HEAD)
git diff --name-only $BASE..HEAD | while read f; do
if git grep -qE '^<{7,} |^>{7,} |^\|{7,} ' -- "$f" 2>/dev/null; then
git grep -nE '^<{7,} |^={7,}$|^>{7,} |^\|{7,} ' -- "$f" 2>/dev/null
echo "Residue in $f; merge forbidden"; exit 1
elif git grep -qE '^={7,}$' -- "$f" 2>/dev/null; then
echo "pure '=' lines in $f (no <, >, | markers) — pair-first false positive, not residue"
fi
done
构建产物冲突通常会由
git-conflict-resolve的构建产物自动快捷处理机制自动跳过(采用 release 一侧,不会留下残留标记)。如果该机制意外将标记带入构建产物,阶段 8 会作为故障保护。
遇到 exit 1 时:不要继续阶段 9。 返回阶段 6,修复后重新运行阶段 8。
8.2 条件性增强(仅在运行 Phase 6 时)
- 冲突文件差异审查 — 对 Phase 5 记录为冲突的每个文件执行:
git diff origin/<MAIN_BRANCH>..HEAD -- <conflict_file>。检查:双方的导入/require 均已保留;双方新增的函数/类均已保留;没有重复的符号定义;没有整段逻辑被注释掉的代码块(用//或/* */包裹);没有意外删除。任何一项不通过 → ❌。 - 变基跳过等价性 — 如果 Phase 6 在变基时跳过了提交,重新确认差异等价性(使用 Phase 6 中的命令)。差异非空 → 被跳过的提交未被完全包含;需要手动 cherry-pick。
8.3 判定
| 判定 | 条件 | 后续操作 |
|---|---|---|
| 通过 | 8.1 无残留,且 8.2(若运行)全部通过 | 继续执行 Phase 9 |
| 未通过 | 8.1 检测到残留,或任意 8.2 检查为 ❌ | 禁止合并。 返回 Phase 6;修复后重新运行 Phase 8 |
输出:
[Residual Scan Report]
- 8.1 marker scan: 42 files in merge scope, no residue
- 8.2 diff review (if run):
- .gitignore: 8.2.70 .omc/.sisyphus changes layered on master
- src/bridge/api/MainProcessAPI.ts: both sides' new functions retained
- 8.2 rebase skip (if run):
- skipped 7437368df: diff-equal to master 7421ca91b
Verdict: Pass — proceed to Phase 9
Phase 9:合并 MR/PR
获得用户确认后,按仓库并行执行:
- CI 门禁检查:查询 MR 的合并状态;如果状态不是
mergeable/can_be_merged,则在继续之前提示“CI 可能尚未通过 — 确认合并吗?”。 - 合并:调用平台的合并命令。对于使用
merge_method=merge的 GitLab,传入--squash=false --remove-source-branch=false。对于merge_method=squash,省略--squash=false(项目强制使用 squash)。对于 GitHub,使用--merge(如果项目要求,则使用--squash)。保留源分支。 - 验证:确认合并成功输出(
✓ Merged!/✓ Pull request ... was merged)。然后获取并检查目标分支:
git fetch origin <MAIN_BRANCH>
git log origin/<MAIN_BRANCH> --oneline -5
确认最新提交包含此次发布合并。
- 冻结检查 — 本阶段成功合并且 Phase 2 中的标签已存在后,如果
git-branch-protect可用,则读取此发布分支是否已冻结。如果已冻结,记录报告。如果未冻结,则显示当前规则,并询问用户是否接受freeze;只有在用户接受后才运行。不要调用rollback。如果用户拒绝,或报告显示分支未冻结,则保留标签和主干合并,并将该报告复制到 Phase 10。如果技能不可用,则跳过此步骤。
Phase 10:输出报告
写入 $(git rev-parse --show-toplevel)/release-<VERSION>-report.md。注册表行:report-output-discipline/reference.md 中的 发布报告(human + 接入)。写入 Markdown 报告后,遵循 report-output-discipline 在同一目录生成 release-<VERSION>-report.html,并报告两个文件的路径。
报告包含:
- 标签摘要:仓库 / 标签名称 / 提交 SHA / 远程验证状态
- MR/PR 摘要:仓库 / 源 → 目标 / ID 和 URL / 合并状态
- 冲突详情(如有):分支分歧情况(各仓库超前的提交数)、仅存在于目标分支的提交(冲突来源)、冲突文件及解决方式 — 直接引用
git-conflict-resolve技能的全局审查清单,而不是重新生成该清单;已清理的额外文件列表 - 手动后续事项:剩余风险、待处理的 cherry-pick 等
- 跨发布同步(如有):源发布 / 目标发布 / 策略 / 冲突摘要(参见
references/cross-release-sync.md)
常见错误
| 错误 | 后果 | 修正方法 |
|---|---|---|
| 因为“看起来很干净”而跳过 Phase 0 | 失败的 AI 自动解决所遗留的历史问题会随合并进入 main | 对每个仓库都无条件运行 Phase 0 |
| 未验证远程状态就为本地 HEAD 打标签 | 本地落后了;标签会落在过时的提交上 | 始终获取并为 $REMOTE_SHA 打标签,绝不要为本地 HEAD 打标签 |
| 在 Phase 3.5 指纹检查前创建 MR | 在 ff 模式仓库的 Phase 9 遇到 406;回滚代价高 | 所有路径都必须执行 Phase 3.5 |
将 can_be_merged=true 当作“合并一定会成功” | ff 模式仓库仍会返回 406 | 交叉核对 Phase 3.5 的判定结果;Phase 5.5 中以首个匹配行的结果为准 |
| 未经确认就强制推送共享的发布分支 | 覆盖队友的工作 | 使用 --force-with-lease 前先向用户确认 |
| 在包含 50 个提交的分支上执行 rebase | 每个冲突提交都会中断 rebase;可能连续数天被打断 | 除非提交数 ≤ 3 且冲突文件数 ≤ 1,否则使用合并策略 |
执行多仓库命令时未添加 cd <REPO> && 前缀 | 在错误的仓库中创建 MR,合并了错误的 MR | M.1:多仓库操作中的每个命令块都必须以 cd <REPO_PATH> && 开头 |
| 遇到第一个仓库 Phase 9 失败就停止 | 其他仓库也会遇到相同问题 | M.2:立即将失败模式应用到所有尚未合并的仓库 |
| 标签落在了错误的提交上 | 发布指向错误的 SHA | 执行 git push origin --delete <TAG> + git tag -d <TAG>,然后基于 $REMOTE_SHA 重新执行 Phase 2;平台 API 也可能支持远程删除标签 |
平台 api 报告 Accepts 1 arg(s), received 2 | Shell 拆分了参数 | 为每个字段加引号:-f "key=value";如果该 CLI 版本不接受,则使用 --raw-field "key=value"。优先升级 CLI |
| Cherry-pick 产生空提交(“previous cherry-pick is now empty”) | 目标分支已有等效变更 | 执行 git cherry-pick --skip;仅在必须保留记录时才使用 --allow-empty |
git rm --cached 报告 “pathspec not found” | 文件在工作树中,但不在索引中 | 执行 git add -A 将工作树同步到索引,然后重试 |
| 发布过程中 CLI 认证过期 | 命令因认证错误而失败 | 重新运行平台 auth login;从失败的阶段重新开始 |
附录:pre-commit hook(可选的 L5 增强)
一个 Git 原生 hook,可对每项已暂存内容运行 Phase 0/8 标记正则表达式。安装一次后即可使用,且独立于任何技能而持续生效。
cat > .git/hooks/pre-commit << 'HOOK'
#!/bin/bash
# Reject staged content with git conflict markers (Phase 0/8 pair-first verdict)
# Pure '=' lines without <, >, or | markers are false positives (license underlines,
# Markdown separators) — they do not block.
# Scan the staged blobs directly: git diff output prefixes every line (+/-/space),
# which defeats the ^ anchors of the marker regex.
git diff --cached --name-only -z | while IFS= read -r -d '' f; do
if git grep --cached -qE '^<{7,} |^>{7,} |^\|{7,} ' -- "$f" 2>/dev/null; then
echo "pre-commit: staged content contains conflict markers ($f); commit blocked"
exit 1
fi
done || exit 1
exit 0
HOOK
chmod +x .git/hooks/pre-commit
- 可通过
git commit --no-verify绕过 — L5 是增强功能,并非门禁 - 如果项目已有 pre-commit hook,请将标记检查追加到现有 hook 中
- 使用 Phase 0 正则表达式,并采用相同的“配对优先”判定规则
参考资料
references/ff-cherry-pick.md— 当 Phase 3.5 检测到 ff 模式 + 非快进时的完整 cherry-pick 流程。还涵盖 ff 项目中的哈希一致性细节。references/cross-release-sync.md— 将变更从release/A(已合并到 main)同步到release/B的决策树和命令,包括 rebase/哈希变更陷阱。