首页
/ LobeHub PR 技能详解:canary 分支策略与跨层功能拆分(Stacked PR)实战

LobeHub PR 技能详解:canary 分支策略与跨层功能拆分(Stacked PR)实战

2026-09-06 12:33:23作者:裴麒琰

本文基于 LobeHub 仓库中的 PR 技能文档 .agents/skills/pr/SKILL.md,系统讲解该项目「一个分支一个 PR」的标准提交流程与「跨层功能拆分为有序 Stack PR」的分层合并策略。读完你可以掌握:为什么 LobeHub 要求所有 PR 目标 canary 而非 main、如何用一组并行 git 命令安全地收集上下文并创建 PR,以及如何把一个横跨数据库、共享包、服务端 TRPC、桌面端/CLI 与 UI 的功能分支,用可复现的 git 操作步骤安全地拆成「底层先合、上层后合」的两个 PR。

技能定位与触发方式

该文档是 LobeHub 内置的一个 Agent Skill,用于自动化处理「为当前分支创建 PR」这类请求。从 frontmatter 可以看到:

  • 技能名pr
  • user-invocable: true:可由用户直接调用。其描述覆盖了 create prsubmit propen a PRpull requestsplit this PRstacked PRbackend should merge first提 PR拆 PR后端先合分层合并 等触发词——中英文关键词都纳入了触发范围。
  • 配套策略文件 .agents/skills/pr/agents/openai.yaml 声明了 policy: allow_implicit_invocation: false,即该技能只允许显式调用,不会被其他技能隐式触发——这在「改分支、强推远端」这类有副作用的操作上是一个重要的安全边界。

技能整体分为两大部分:前 6 步的标准单 PR 流程(适用于绝大多数场景),以及后半部分的 Stacked PRs 跨层拆分流程(适用于客户端依赖服务端新契约的跨层功能)。

分支策略:canary 是开发主干,main 是发布分支

文档在开头就确立了两条硬性规则:

  • 目标分支canary(开发分支,对应云生产环境)
  • main 是发布分支——永远不要直接对 main 提 PR

这一策略并非孤立存在,仓库的主 Agent 规范 AGENTS.md 中「Git Workflow」一节给出了同样的约定:

canary is the development branch (cloud production); main is the release branch (periodically cherry-picks from canary). New branches should be created from canary; PRs should target canary. Use rebase for git pull. Commit messages: prefix with gitmoji. Branch format: <type>/<feature-name>.

也就是说 LobeHub 的分支模型是:日常开发全部流入 canarymain 周期性从 canary 同步(cherry-pick)。仓库中还存在一条专门维护这一模型的 CI 工作流 .github/workflows/sync-main-to-canary.yaml:当 main 收到 push 时,机器人 lobehubbot 会自动以 sync/main-to-canary-* 为 head 分支、canary 为 base 创建同步 PR,并复用已存在的同名 open PR 避免重复。理解这一点很重要——它解释了为什么技能流程把 canary 当作「trunk」来处理所有比对(如 git log --oneline origin/canary..HEAD)与 PR 目标选择。

标准流程第一步:并行收集上下文

技能要求并行执行以下 6 条命令,一次性摸清当前分支状态:

git branch --show-current                                        # 当前分支名
git status --short                                               # 未提交改动
git rev-parse --abbrev-ref @{u} 2>/dev/null                      # 远端跟踪(upstream)状态
git log --oneline origin/canary..HEAD                            # 尚未进入 canary 的提交
gh pr list --head "$(git branch --show-current)" --json number,title,state,url  # 该分支是否已有 PR
git diff --stat --stat-count=20 origin/canary..HEAD              # 变更概览(限制 20 行防刷屏)

这几条命令覆盖了后续决策需要的全部信息:分支名决定推送方式,upstream 状态决定是否需要 git push -uorigin/canary..HEAD 的提交与 diff 决定「是否有东西可提 PR」以及 PR 描述怎么写,而 gh pr list 则是防重复创建的关键检查——文档在 Notes 中明确要求:如果该分支已存在 PR,应告知用户而不是创建重复 PR。

在默认分支上有未提交改动时:先建分支再提 PR

当当前分支是 canarymain 且存在未提交改动时,文档给出了一条标准处理链:

  1. git diff 分析改动内容;
  2. 从改动推断分支名,格式遵循 <type>/<short-description>(例如 fix/i18n-cjk-spacing,与 AGENTS.md<type>/<feature-name> 的分支格式一致);
  3. git checkout -b <branch-name> 建分支并切换;
  4. git add <files> 显式暂存相关文件——文档特意强调优先使用显式文件路径而非 git add .,避免把无关改动带进 PR;
  5. 用符合 gitmoji 规范的提交信息提交(对应 commitlint.config.mjs 基于 @lobehub/lint 的提交校验);
  6. 继续后续步骤。

边界情况也有明确约定:如果当前在 canary/main既无未提交改动、也没有未推送提交,直接中止——没有可创建 PR 的内容。

推送、关联 Issue 与创建 PR

  • 推送:无 upstream 时用 git push -u origin $(git branch --show-current);有 upstream 时用 git push origin $(git branch --show-current)。两条命令都带显式分支名——这一点在后文「Gotchas」中会被再次强调,是防止误推 canary 的第一道防线。
  • 搜索关联 Issuegh issue list --search "<keywords>" --state all --limit 10。文档提醒只链接 scope 匹配的 issue,避免挂到大而全的 umbrella issue 上;没有匹配则跳过。
  • 创建 PRgh pr create --base canary,要求:
    • 标题格式:<gitmoji> <type>(<scope>): <description>
    • 正文基于 PR 模板 ​.github/PULL_REQUEST_TEMPLATE.md 填写并勾选相应复选框;
    • 用 magic keywords(Fixes #123Closes #123)关联 GitHub issue;适用时同时关联 Linear issue(Fixes LOBE-xxx);
    • 正文使用 HEREDOC 传入以保留 Markdown 格式。

仓库中的 ​.github/PULL_REQUEST_TEMPLATE.md 实际结构印证了模板要求:UI 改动需要 Before/After 截图表格、「Test」小节含三个复选框(Tested locally / Added/updated tests / No tests needed),以及「Related Issue」小节(Fixes #xxx, Closes #xxx, Related to #xxx)。技能文档要求填写的四大要素——Change Type、Related Issue、Description of Change、How to Test——正是对这个模板各小节的映射。

最后一步 gh pr view --web 在浏览器中打开 PR 页面供人工确认。另有一条全局约定:所有 PR 内容必须使用英文

Stacked PRs:当功能横跨多个层时如何拆分

这是文档最具价值的部分。文档描述的典型跨层链路是:packages/database 的 schema/model → 某个共享 packages/* 库 → 服务端 TRPC 路由 → apps/desktopapps/cli 的调用方 → src/features 下的 UI。仓库结构可以佐证这条链路的真实性:packages/database/src 下包含 schemasmodelsrepositoriescore 等目录,packages/trpc 提供 TRPC 路由基础设施,apps/desktopapps/cli 则是两个主要客户端。

为什么一个 PR 合不安全

文档给出的核心论据是:客户端调用的端点在同一个 PR 合入之前不存在于 trunk 上。此时单 PR 会出问题:

  • 部分合入、回滚、独立 review 都会破坏一致性;
  • 评审者无法单独验证任何一层。

因此要把功能拆成有序 PR,底层先合。

排序规则:调用方必须等被调用方先上 trunk

A PR may only merge after every layer it calls is already on the trunk.

即:服务端契约(新 TRPC procedure、返回结构变化、新表/新模型)先合;调用方(desktop、CLI、UI)后合。当难以判断时,用一个问题打破平局:「如果这个 PR 现在单独合入 canary,它能构建并正常工作吗?」 若不能,它就该排在后面的 PR 里。

文件归属判断:三个不直觉的规则

  1. 适配契约变化的前端代码要跟着服务端 PR 走。 例如放宽 TRPC 返回结构(listDevices 返回值变为 platform: string | null),消费该结构的组件必须在同一个 PR 里同步修改,否则服务端 PR 单独合入就会让构建挂掉。原则是:契约与其在仓库内的消费方一起交付。
  2. 新的共享包跟着它的消费方走,而不是默认归服务端——除非服务端也 import 它。一个只被 desktop/CLI 引用的包,应该放在客户端 PR 里,不要在下层 PR 中拖着无用的包。(注意:技能文档以 @lobechat/* 指代共享包;从当前仓库的 apps/cli/package.json 等文件看,工作区包实际使用 @lobehub/* 作用域,例如 @lobehub/cli。)
  3. 工作区依赖声明package.json 中的 workspace:*pnpm-workspace.yaml 条目)随 import 它的代码走。当前仓库的 pnpm-workspace.yaml 声明了 packages/**e2eapps/serverapps/shareapps/workbenchapps/desktop/src/main 等工作区包,拆分时这些声明应随对应包代码一起进入正确的 PR。

git 操作配方:把一个完整分支拆成 Stack

起点假设:一个分支 feat/x 上有单个包含全部改动的提交 <FULL>,且已推送到远端(远端这份副本本身就是安全网之一)。

# 1. 安全网 —— 改写任何东西之前,确保完整工作不可丢失
git branch backup/x-full <FULL>          # 指向完整提交的本地 ref
git branch feat/x-clients <FULL>         # 上层分支从完整提交起步

# 2. 把下层分支重写为只含下层文件
git checkout feat/x                      # 这个分支将成为 SERVER PR
git reset --hard origin/canary
git checkout <FULL> -- <server/db files…>   # 只暂存这些路径
git commit -m "✨ feat(...): <server half>"
git push --force-with-lease origin feat/x   # 永远不用 --force;永远不推 canary

# 3. 在刚重写的下层 HEAD 之上构建上层分支
git checkout feat/x-clients
git reset --hard feat/x                  # base = 刚重写的 server HEAD
git checkout backup/x-full -- <client/ui files…>   # 只取剩余路径
git commit -m "✨ feat(...): <client half>"
git push -u origin feat/x-clients

核心技巧是 git checkout <FULL> -- <paths>:从完整提交中按路径挑选文件到暂存区,从而在干净的 origin/canary 基线上只重建下层改动;上层分支则 reset --hard 到下层的重写结果之上,形成物理上的依赖。

随后创建上层 PR 时,base 指向下层分支而非 trunk

gh pr create --base feat/x --head feat/x-clients --title "…" --body "…"

--base feat/x 带来两个效果:diff 只包含客户端文件(不会泄漏服务端文件),并且从机制上杜绝了客户端先于服务端合并的可能。服务端 PR 合入 canary 后,把客户端 PR 的 base 重新指向 canary——GitHub 通常在 base 分支合并时会自动 retarget,文档建议同时在 PR 正文中说明这一点,由人工确认。

验证依赖关系确实成立

拆分的意义在于上层确实需要下层。文档要求证明这一点:在叠加上层分支上对调用方做类型检查,确认下层引入的符号能解析:

cd apps/cli && bun run type-check 2>&1 | grep -iE "connect\.ts|device\.register"
# 与你的改动相关的输出为空 = 栈式 base 提供了 device.register ✓

其中 apps/cli/package.json 定义的 type-check 脚本为 tsc --noEmit。文档还提示:过滤到你实际改动的文件——这个仓库的独立类型检查会输出一些与你无关的环境噪音(如 __ELECTRON__@/types/llm、未构建的 @lobechat/types),不要把这些当成拆分失败。

PR 与 Linear 的记账规则

  • 每个 PR 只关闭自己所属层的 issue:服务端 PR 写 Closes LOBE-<server>,客户端 PR 写 Closes LOBE-<pkg> / <desktop> / <cli>;不要让一个 PR 的正文去认领另一层的 issue。
  • 两个 PR 都标记为 Part of LOBE-<parent>
  • 创建 PR 时把各自关闭的子 issue 移到 In Review(而非 Done),并补 completion 评论。这一点与配套技能 .agents/skills/linear/SKILL.md 的约定一致:该技能明确「In Review 是 PR 创建后的状态,Done 留给 PR 合并之后」,且要求 Fixes/Closes/Resolves LOBE-xxx magic keywords 与 issue 上的完成评论成对完成——只写 magic keyword 而不留评论,会让 Linear 上的人看不到任何上下文。

Gotchas:六个踩坑点

  1. 永远不要推 canarygit checkout -b feat/x origin/canary 切出的分支会跟踪 origin/canary,此时裸 git push 会推往 canary。务必始终使用显式分支名:git push origin feat/x
  2. 重写下层分支用 --force-with-lease,不用 --force——如果远端在你脚下移动了(他人推送),lease 检查会中止操作。
  3. reset --hard 之前先备份。 配方第 1 步的 backup/x-full 加上已推送的远端分支,意味着完整提交在被改写前至少被 ≥3 个 ref 引用。可用 git branch --contains <FULL> 验证。
  4. 锁文件问题:文档指出这个 monorepo 不提交根级 pnpm-lock.yaml,因此新增 workspace:* 依赖不产生 lockfile 变更。当前仓库确实没有根级 lockfile,且 pnpm-workspace.yaml 显式声明了 lockfile: false。反过来,如果在一个确实提交 lockfile 的仓库中使用这套配方,拆分后需要在每个分支上分别重新生成 lockfile。
  5. 不要过度拆分。 两个 PR(契约 / 调用方)通常已经足够;只读取现有端点的 UI 页面可以成为后续的独立 PR,但不要为了拆分而拆分,把同一层拆散到多个 PR 里。

小结

LobeHub 的 PR 技能把「提 PR」这件事工程化成了两个层级:单 PR 场景下,以 canary 为 trunk、以显式分支名推送、以模板与 magic keywords 收尾的六步标准流程;跨层场景下,以「单独合入能否构建并正常工作」为判据的排序规则、以 git checkout <FULL> -- <paths> 为核心的无损拆分配方,以及用类型检查证明依赖关系真实存在的验证手段。整套流程的安全设计——三重 ref 备份、--force-with-lease--base 指向下层分支、显式分支名——都围绕同一个目标:让分层合并既保持正确的依赖顺序,又不丢失任何一行已经写完的代码。

相关延伸阅读:AGENTS.md(完整 Git 工作流与质量检查约定)、.agents/skills/linear/SKILL.md(Linear issue 状态与完成评论规范)、​.github/PULL_REQUEST_TEMPLATE.md(PR 模板原文)。

登录后查看全文
热门项目推荐
相关项目推荐