关于 ship
ship 是用于将已完成分支提交、推送并创建 PR 的交付技能,适合需要代码评审流程的团队。它自动识别 Azure Repos 或 GitHub,并可关联工作项,合并后清理分支和 worktree。
主要功能
- 01自动识别 Azure Repos 或 GitHub
- 02预检待交付内容与工作区范围
- 03提交并推送当前分支
- 04创建 PR 并关联工作项
- 05设置 PR 标签及评审人
- 06合并后清理分支和 worktree
适用场景
- 01通过 PR 评审交付分支变更
- 02确保 PR 关联 Azure Boards 工作项
- 03为 PR 添加标签和评审人
- 04PR 合并后清理本地分支与 worktree
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ julianobarbosa/ claude-code-skills --skill ship安装后在 Claude Code 中输入/ship确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignorepackage.jsontsconfig.jsonassets/pr-template.mdreferences/azure-devops.mdgithub.mdscripts/ship-detect.tsship-lib.test.tsship-lib.tsship-open.tsship-pr.tsship-preflight.tsship-push.tsship-snapshot.tsship-tag.ts详细介绍
Ship
ship 负责完成一个已完成分支的最后一步:干净地提交、推送(必要时处理 Azure
DevOps 身份验证问题)、创建拉取请求、将其关联到工作项,并在合并后清理分支和工作树。它会自动检测你使用的是 Azure Repos 还是 GitHub,并遵循对应流程,因此同一条命令既适用于你的组织,也适用于 github.com 仓库。
它的功能刻意保持精简。它不会在本地合并、运行流水线或管理待办事项,而是将可供审查的 PR 交给平台处理,并在合并后进行清理。
何时使用它,而不是其他工具
ship— 基于 PR 的交付流程:推送 → PR →(通过平台合并)→ 清理。commit— 仅暂存、提交并推送当前分支,不创建 PR。merge— 在本地将分支合并到main(快进合并)并清理。没有 PR 审批门槛时使用它;更改必须经过审查/策略检查时使用ship。azure-devops— 深入的 REST/MCP 工具箱(WIQL、批量更新、流水线、评论线程)。ship只会调用所需的精简功能;需要更丰富的功能时使用azure-devops。
流程
先运行 bun scripts/ship-detect.ts——它会输出平台、Azure 组织/项目/仓库(如有)、分支和推断出的工作项 ID。以下所有步骤都会据此选择流程。
**一次性设置:**这些脚本依赖
azure-devops-node-api和@octokit/rest。首次使用前,在技能目录(~/.claude/skills/ship/)中运行一次bun install。
只说“ship”时——直接执行
简短地说 ship / ship it 意味着“完成交付”,而不是“给我解释”。运行流程时尽量少作说明:仅报告预检范围结果(仅当结果不干净时)、pr_url、任何已创建的工作项 ID,以及清理确认。不要回顾会话状态、不要大声重新推导计划,也不要在执行前逐项列出所有步骤——对简短命令过度解说本身就是一种失误模式。唯一值得暂停的情况是预检(步骤 0)返回 scope_clean=false。
0. 预检
bun scripts/ship-preflight.ts # anything to deliver? + working-tree scope audit
- 它会输出
commits_ahead_of_upstream、staged/unstaged/untracked计数,以及scope_clean=true|false。 - 如果
scope_clean=false,请停止并向用户列出相关文件,然后再提交——确认这些文件确实有意排除在外。这是 PR #192 的防护检查:另一个目录中已更改但未暂存的文件会被悄悄漏出 PR。此检查仅提供提示(绝不会阻止操作),但未经确认的false正是此检查要避免的范围遗漏。 - 注意
ship-detect.ts检测到的平台。如果它显示unknown(例如自定义 SSH 主机别名),请在当前会话设置SHIP_PLATFORM=azure或SHIP_PLATFORM=github。
1. 提交——以作者身份,而不是以工具身份
仅暂存此任务所需的文件并提交。绝不添加 AI 署名——不要添加 Generated with Claude,也不要添加 Co-Authored-By: Claude。提交由人类署名;工具来源信息不应写入 Git 历史记录(此仓库的 commit-msg 钩子会移除这些尾注作为兜底,但不要依赖它——一开始就不要写入)。如果提交前钩子因无关问题失败,可以使用 --no-verify;如果它指出的是你所做的更改,请修复问题。
2. 分支状态
- 在功能分支上 → 正常,继续。
- 在
main/master上 → 无法从 main 向自身创建 PR。请创建分支,并在推送前将提交移到该分支上:
(仅当这些提交尚未推送到 main 时才这样做。如果不确定,请停止并询问。)git branch feature/<slug> && git reset --hard @{u} && git checkout feature/<slug> - 直接推送到 main 被阻止(分支策略)意味着应采用 PR 流程,而不是强制推送或绕过策略。
3. 推送
bun scripts/ship-push.ts # pushes current branch, sets upstream
在 Azure DevOps 上,如果常规推送因身份验证失败,此操作会自动生成 az OAuth 令牌,并使用 Bearer 标头重试——当 SSH 和 HTTPS 凭据助手都不可用时的备用方案。具体实现请参阅 references/azure-devops.md。
4. 创建 PR(+ 确保工作项存在/关联工作项,+ 转换状态)
不变量 — 永远不要向用户询问工作项 ID。 查找 → 验证 →(若不存在) 创建 + 分配 + 关联工作项,这一过程完全自动。PR 在关联 Board 工作项之前,绝不能显示为已就绪。如果你正准备询问“工作项 ID 是什么?”, 请停下 — 这个问题正是此技能要避免的错误。ID 来自
--work-item、分支名称或刚创建的工作项,绝不会通过提问获得。代码审查不负责管理工作项。 由于 ship 保证 PR 创建时已关联工作项, 审查步骤 — 无论是人工审查者,还是
code-review/bmad-code-review技能 — 都不得再次询问工作项,也不得因此阻止审查。上游已解决此问题。
起草一份真实的描述 — 将 assets/pr-template.md 复制到临时文件,填写 Summary / Changes /
Verification,并保留 AB#<id> 这一行,以便关联工作项。
bun scripts/ship-pr.ts --title "<title>" --body-file /tmp/pr-body.md \
--work-item <id> --transition Resolved \
--reviewer [email protected] --required-reviewer [email protected] --tag needs-review
- 自动检测平台;脚本使用 azure-devops-node-api SDK(
createPullRequest)或 Octokit(pulls.create)。 - 先决条件会随创建操作一并提交,而不是通过后续往返调用处理。 在 Azure 上,工作项、标签和审阅者都会打包在单次
createPullRequest调用中。在 GitHub 上,创建 API 不接受这些内容,因此标签和审阅者会通过后续调用添加——结果相同,只是在 GitHub 上无法避免后续调用。 - 审阅者 —
--reviewer <upn|username>用于添加可选(不会阻塞流程)的审阅者;--required-reviewer会在 Azure 上将其标记为必需(阻止完成)。两者都支持逗号分隔和/或重复指定。在 Azure 上,UPN/电子邮件地址会被解析为身份;无法解析的名称会发出警告并跳过(reviewer_skipped=…),绝不会阻塞 PR。GitHub 没有针对单个 PR 的“必需”审阅者(这是分支保护功能),因此必需审阅者只会作为普通请求发送。脚本会为每位审阅者打印reviewer_added=<name> required=<bool>。 在 Azure 上,如果未指定这两个标志,且设置了$SHIP_ADO_DEFAULT_REVIEWER,则会将其添加为可选审阅者(在 GitHub 上会忽略该变量,因为 ADO 身份毫无意义)——没有审阅者记录的 PR 不会留下任何已邀请审阅者的痕迹。未设置 = 不添加审阅者,沿用此前行为。 - Azure 绝不会在没有关联 Board 工作项的情况下创建 PR。 脚本会从
--work-item(或分支名称)解析 ID,然后通过 SDK(getWorkItem)验证该工作项确实存在。从分支名称解析只是启发式方法——例如1234-foo这样的分支可能包含一个并非真实工作项的编号,因此要检查其是否存在,而不是直接假定它存在。 - 如果找不到真实工作项,则会为每项任务创建一个工作项并关联到 PR,每个工作项都会分配给
$SHIP_ADO_ASSIGNEE(默认为[email protected];可通过--assignee覆盖)。 每项任务传入一次--task "<title>"(可重复指定),或传入--tasks-file <file>(每行一个标题),即可为每项任务创建一个工作项。若未指定--task,则根据 PR 的--title创建一个工作项。类型默认为Task(--work-item-type "User Story"|Bug)。使用--no-create-work-item可禁用创建,仅关联现有 ID。- 脚本会为每个已创建的工作项打印
work_item_created=<id>,并为整组工作项打印work_items=<ids>— 将新 ID 呈现给用户(不要编造;它们来自 SDK 响应)。
- 脚本会为每个已创建的工作项打印
--transition(仅限 Azure)会在 PR 创建后将每个已关联/已创建的工作项转换到指定状态。状态名称因流程而异(Agile:Resolved;Scrum:Committed;Basic:Doing)— 请先验证有效的下一状态;参阅references/azure-devops.md。省略--transition可让看板保持不变。(新建的工作项会从对应类型的初始状态开始,例如New/To Do。)- Complete 对话框中的“合并后完成关联的工作项”复选框默认已勾选。 Ship 会在创建调用后立即通过 PATCH 设置
completionOptions.transitionWorkItems=true,这样用户在 PR 上点击 Complete 时,关联的工作项就会完成,而不会在 PR 合并后仍保持未完成状态。这与--transition相互独立:后者在 PR 创建时触发;这一项则在合并时触发。它能闭合整个流程,因为没人会记得在合并时勾选复选框。传入--no-complete-transition可取消勾选。脚本会打印从 PATCH 响应中读取的complete_transitions_work_items=true|false。GitHub 上会忽略此选项,因为正文中的Closes #<id>可实现相同效果。 - 脚本会打印
pr_url=…;将其呈现给用户。
4b. 标签 + 审阅者(默认应用)
Azure DevOps 将它们称为标签;GitHub 称为标签——本质上是同一回事:一种简洁醒目的信号, 有助于审阅者分流处理,并帮助团队整理 PR。Microsoft 的指导意见是,标签可以“向审阅者传达额外信息,例如 PR 仍在进行中,或是针对即将发布版本的热修复”(向拉取请求添加标签)。
PR 绝不会不带标签就创建。 如果没有 --tag,ship-pr.ts 会按以下顺序推导标签:
- 根据 Conventional Commit 标题——
docs(finops): …→docs、finops。 - 根据分支前缀——
feat/…→feat,fix/|hotfix/|bugfix/→fix,docs/→docs,chore/→chore,refactor/→refactor。 - 否则使用
needs-review,并发出WARNING。
此处不会失败。没有哪个平台有“必须添加标签”的策略,因此这是唯一的强制执行点;硬性报错会导致采用其他约定的仓库无法使用。传入
--no-tag 可创建不带标签的 PR。如果没有 --reviewer/--required-reviewer,则会将
$SHIP_ADO_DEFAULT_REVIEWER(如果已设置)添加为可选审阅者;--no-reviewer
则会跳过此操作。
使用 --tag 在创建 PR 时添加标签(可用逗号分隔,也可重复传入):
bun scripts/ship-pr.ts --title "<title>" --body-file /tmp/pr-body.md \
--work-item <id> --transition Resolved --tag "hotfix,do-not-merge"
……或者使用 ship-tag.ts 为已创建的 PR 管理标签(Azure 上的 ID 为 pr_id=,GitHub 上为 pr_number=,
两者都会由 ship-pr.ts 打印):
bun scripts/ship-tag.ts <pr-id> --add "needs-review" # add
bun scripts/ship-tag.ts <pr-id> --remove "do-not-merge" # remove
bun scripts/ship-tag.ts <pr-id> --list # show current tags
两个平台上的标签都支持自由填写(Azure 会在首次使用时创建标签定义;GitHub
会自动创建缺失的标签)。关于推荐且统一的标签集——do-not-merge、work-in-progress、
hotfix,以及类型/领域约定——请参阅 references/azure-devops.md / references/github.md 中的
推荐标签部分。
4c. 在浏览器中打开 PR(可选)
实际审阅是在 PR 页面进行的;当你需要在多个账号(工作、个人或云端)之间切换时,使用哪个已登录账号打开页面很重要——错误的个人资料会显示错误的
权限,或者完全没有访问权限。因此,PR 创建后,始终显示 pr_url,然后询问是否要在浏览器中打开。打开操作须由用户选择,绝不自动执行(浏览器窗口属于可见副作用;用户要求先询问)。
ship-open.ts 通过读取浏览器自身的 Local State(与其个人资料切换器显示的账号映射相同),解析电子邮件 → 浏览器个人资料,然后使用该个人资料打开 URL。它适用于 WSL(→ Windows Edge/Chrome)和 macOS(→ Chrome/Edge)。所有信息都不会硬编码——每次运行都会实时发现映射,电子邮件作为参数传入,且只会记住你的默认电子邮件,保存在
~/.config/ship/open.json(此技能之外)。
发现账号后,使用 AskUserQuestion 询问用户选择哪个账号(只有多个账号时才值得询问;列出每个账号,并提供“不打开”选项):
bun scripts/ship-open.ts --list-profiles # prints browser= + one `profile=<dir>\t<email>\t<name>` per account
使用选定的账号打开 PR(如果想在不打开窗口的情况下确认解析出的个人资料,可先添加 --dry-run):
bun scripts/ship-open.ts "<pr_url>" --profile <email> # opens; prints opened_url= / profile_dir=
bun scripts/ship-open.ts "<pr_url>" --profile <email> --dry-run # resolve only, no window
将所选账号记为默认账号,以便今后执行 ship 时只需按一次键即可选择:
bun scripts/ship-open.ts --set-default <email> [--browser edge|chrome]
电子邮件未知或未提供时,会回退到浏览器的 Default 个人资料(并在 stderr 上提示),而不会冒险猜错。此步骤独立于下面的合并/清理操作——打开 PR 进行审阅不会推进或阻碍任何流程。
5. 合并后——清理
仅在 PR 确实已合并后执行(不要删除仍有未关闭 PR 的分支)。按照 merge 技能的清理流程操作,并且删除前先询问:
- 如果分支是在工作树中开发的,请先
cd到主仓库目录,然后执行git worktree remove <path>——不能在分支自身的工作树内删除该分支。 - 执行
git branch -d <branch>(小写-d会拒绝删除未合并的分支——这是安全保护;只有在用户明确确认后,才升级为-D)。 - 执行
git push origin --delete <branch>(如果该分支仅存在于本地,则忽略失败)。
6. 快照当前状态(可选)
与 PR 流程无关——任何时候想要一个可回退的状态点,都可以使用此功能:在干净地合并到 main 后、进行高风险迁移前,或者只是标记“一切正常”。它会在 HEAD 上创建一个带注释的 git 标签(工作树状态无关紧要——标签指向的是一次提交)。
bun scripts/ship-snapshot.ts \
-m "what changed / why you're saving here" \
-m "verification status"
脚本会自动填充那些容易手动填错的内容:
- 名称默认为
v<YYYY.MM.DD-HHMM>(精确到分钟,因此一天内重复创建的快照不会冲突)。传入--daily可使用每天一个的v<YYYY.MM.DD>,或传入--name <tag>指定确切名称。 - 第一段消息会自动生成——
Snapshot <tag> — <branch> @ <shorthash>——因此即使没有-m,标签也能自我说明。每个添加的-m都会成为单独一段(通过参数数组传入,因此实际换行符和标点都能保留——不像在单个 shell 字符串中使用\n)。 - 遇到冲突时会拒绝操作,而不会覆盖(退出码 3)——绝不能悄悄移动他人依赖的快照标签。
标签在推送前仅保存在本地(共享/可见——请先确认,就像打开 PR 一样)。添加 --push 可推送到 origin;在 Azure 上,如果身份验证失败,它会像 ship-push.ts 一样回退到 az OAuth Bearer 标头。脚本会打印 tag=、commit=、branch=、pushed=。
内置工具
| 脚本 | 功能 |
|---|---|
bun scripts/ship-preflight.ts [remote] | 检查待交付内容,并审计工作树范围(scope_clean=…);仅提供建议,从不阻止操作。先运行 |
bun scripts/ship-detect.ts [remote] | 打印平台、Azure 坐标、分支和推断出的工作项 |
bun scripts/ship-push.ts [-r remote] [-b branch] | 推送并设置上游;身份验证失败时回退到 Azure OAuth Bearer |
bun scripts/ship-pr.ts --title … [opts] | 在检测到的平台上创建 PR;确保工作项存在/创建工作项并关联(分配给已配置的用户);可选进行 Board 状态转换;根据标题/分支自动添加标签(可通过 --tag 覆盖,或通过 --no-tag 禁用);可选传入 --reviewer/--required-reviewer(默认使用 $SHIP_ADO_DEFAULT_REVIEWER,通过 --no-reviewer 禁用);预先检查“合并后完成已关联的工作项”(通过 --no-complete-transition 禁用)——在 Azure 上,工作项、标签和审阅者会合并到一次创建调用中 |
bun scripts/ship-tag.ts <pr-id> [--add|--remove "t1,t2"] [--list] | 在现有 PR 上添加/移除/列出标签(Azure)或标签(GitHub) |
bun scripts/ship-open.ts <url> [--profile <email>] [--dry-run] | 在指定浏览器账户配置文件中打开 PR/URL(通过浏览器的 Local State 将电子邮件映射到配置文件);WSL→Edge/Chrome,macOS→Chrome/Edge;还支持 --list-profiles、--set-default |
bun scripts/ship-snapshot.ts [-m "para"]… [--daily] [--name <tag>] [--push] | 将当前状态保存为带注释的git 标签(以日期命名,主题能自我说明);可选推送,并支持 Azure OAuth 回退 |
脚本是通过 bun 运行的 TypeScript;共用辅助函数位于 scripts/ship-lib.ts。在技能目录中运行一次 bun install
(安装 azure-devops-node-api + @octokit/rest)。Git/推送仍通过子进程调用(这是 git 操作);只有 ADO/GitHub REST 接口使用 SDK。平台相关说明位于 references/azure-devops.md 和 references/github.md——请阅读与检测到的平台相匹配的文件。PR 描述以 assets/pr-template.md 为模板。
安全
- 执行任何破坏性操作或会公开共享的操作前都要先询问——删除分支/工作树,以及打开 PR(团队可见)。推送功能分支和创建草稿风险较低;创建可供审阅的 PR 和删除任何分支都应先确认。
- 绝不要为了绕过被拒绝的推送而强制推送。 推送被拒绝意味着历史发生分歧或存在策略限制——应先调查,不要覆盖。
- 令牌应放在标头中,使用期限应短,并且绝不打印或放入 URL。
- 任何地方都不要注明 AI 署名——包括提交消息、PR 标题或 PR 正文。
注意事项
- 两条创建 PR 的路径(Octokit
pulls.create、ADOcreatePullRequest)都要求先推送分支——步骤 3 必须在步骤 4 之前。 - 如果
ship-detect.ts返回unknown,几乎总是因为使用了自定义 SSH 主机别名——请设置SHIP_PLATFORM。 (dev.azure.com-<alias>SSH 主机会被检测为 azure,但无法解析其组织/项目/仓库——这与 shell 版本的限制相同;必要时请显式传入坐标/--work-item。) - 脚本需要依赖项:如果看到
Cannot find module 'azure-devops-node-api',请在 skill 目录中运行bun install。 - 关联的工作项不会自行更改状态。有两个独立的开关:
--transition会在 PR 打开时更改其状态;completionOptions.transitionWorkItems(默认启用,见步骤 4)会在 PR 完成时更改其状态。此默认设置生效前创建的 PR 不包含任何完成选项,因此对应复选框不会勾选——该设置在创建时就已确定,无法添加到已合并的 PR。 - 如果设置了
AZURE_DEVOPS_EXT_PAT,但权限范围不足(例如只有 Code 权限,缺少 Work Items / Pull Request 写入权限),ship-pr.ts会自动检测 401/403,并使用azOAuth bearer token 重试——无需手动取消设置 PAT。回退方式要求你以组织成员身份执行az login。 - 创建时遇到 GitHub
404 Not Found,几乎总是因为账号错误,而不是仓库不存在。gh auth token会以当前活动账号响应,而不考虑仓库所有者;对于你无权查看的私有仓库,GitHub 会隐藏仓库,而不是返回 403。现在,ship-pr.ts/ship-tag.ts会调用githubToken(owner),优先使用gh auth token -u <owner>;对于所有者不是已登录账号的组织仓库,则回退到活动账号;GH_TOKEN/GITHUB_TOKEN仍具有最高优先级。如果仍然收到 404,请检查gh auth status是否列出了仓库所属账号。 - 不要用原始
az repos pr create/gh pr create(或repo_create_pull_requestMCP 工具)手动创建 PR,再事后附加工作项——这种做法会绕过自动创建并关联的约束,并迫使用户手动来回确认“ID 是多少?”。此外,它还会将标题中的&双重转义为字面量&(API 不会解码该实体),导致标题显示错误。ship-pr.ts通过 SDK 和参数数组直接传递标题,因此&会保留为&——这也是应该使用脚本而非原始路径的另一个原因。 createPullRequest会静默丢弃completionOptions。 载荷会被接受,但返回的 PR 中completionOptions: null,因此创建时传入的设置不会生效(在 ADO PR 1050 中观察到)。必须在 PR 创建后使用updatePullRequest设置完成选项。切勿根据传入的标志报告此类设置;请从 API 响应中读取实际值。- 从目标仓库运行脚本,并使用绝对路径——绝不要
cd进入 skill 目录。 平台检测会读取当前工作目录的 git remote,而 skill 目录本身是另一个 GitHub 仓库,因此运行cd ~/.claude/skills/ship && bun scripts/ship-pr.ts会针对那个仓库创建 PR(2026-09-23:只有 422 错误“No commits between main and main”阻止了操作)。请在要发布的仓库中运行bun ~/.claude/skills/ship/scripts/ship-pr.ts。 - PR 仍处于打开状态时删除分支会导致 PR 被放弃——请仅在合并后清理(步骤 5)。
ship-open.ts会将--profile <email>与浏览器中已登录账号的邮箱进行匹配,因此没有关联账号的配置文件只能通过 Default 回退选中,无法通过邮箱选中。它会自动选择已安装的浏览器(如果 Chrome 和其他浏览器都存在,则优先选择 Chrome),不需要依赖项或网络——只需浏览器本地的Local State文件。如果找不到该文件,说明浏览器未安装在预期位置;请传入--browser,或检查它打印的路径。- 三种不同的“标签”——不要混淆。 (1) PR 标签/分类标签(
--tag、ship-tag.ts)——PR 上用于分类处理的信号。(2) 工作项(步骤 4)——用于追溯的关联。(3) Git 标签(ship-snapshot.ts)——历史记录中的提交检查点。ship-tag.ts接收 PR ID;ship-snapshot.ts不接收位置参数,并会为HEAD添加标签。同一个词,三种概念。 - 重复添加标签是安全的——Azure 的
createPullRequestLabel会返回已有定义,而不会报错;GitHub 会自动去重;parseTags也会从 CSV 中移除空名称和重复名称。 ship-tag.ts需要 PR ID——Azure 使用数字pr_id,GitHub 使用pr_number(两者都由ship-pr.ts输出)。在 GitHub 上,如果标签名称对应的标签不存在,系统会自动创建。