# 使用 AI 代理和人工批准实现 GitHub 发布自动化

发布自动化应该减少事务性工作，而不是把随时分发软件的权限交给一个觉得自己已经完成任务的编码代理。代理可以收集变更、计算版本、构建候选版本、起草说明并整理文件。发布和回滚则需要有人检查一份具体的发布请求，并批准这一个操作。

只有当你清理过包含错误二进制文件、已移动标签，或承诺了实际上并未发布的修复的版本后，这条边界听起来才不再保守。调用 API 很容易，通常很难消除它带来的后果。

## 给代理一个发布工作台，而不是整个代码库的所有权

发布代理应该拥有一组小而明确的操作，正好覆盖你希望它完成的工作。广泛的代码库写权限看起来很方便，因为这样不必设计接口。但它也会让一次错误指令把发布任务变成删除标签、修改分支、编辑工作流或发布公告。

对于 GitHub 发布自动化，我会把准备和分发分开。代理可以读取代码库元数据，检查已批准范围内的提交，运行构建，计算构建文件哈希，创建或更新发布草稿，并将文件上传到草稿中。但它不能发布版本、删除版本、修改标签、修改代码库设置、创建令牌，也不能调用任意 GitHub 端点。

把允许的操作写成带固定输入的动词。这比告诉代理「小心一点」有用得多。

- `inspect_release_range(base_tag, head_sha)` 返回提交和拉取请求
- `build_candidate(head_sha)` 返回构建文件和 SHA-256 摘要
- `create_draft(version, target_sha, notes)` 返回草稿标识符
- `upload_asset(draft_id, filename, digest)` 上传一个经过验证的文件
- `request_publish(draft_id)` 创建批准请求

最后这个动词很重要。如果没有人回应，`request_publish` 不能悄悄完成发布。它应该返回待处理状态，让调用方显示并轮询。把超时当作同意的发布流程，实际上已经破坏了自己的控制措施。

不要提供类似 `github_api(method, path, body)` 的逃生通道。团队通常会为最初设计没有涵盖的偶发操作加上它。代理最终一定会使用这个通道，因为语言模型倾向于在接口中选择最短路径。每个不受限制的请求方法，都会把狭窄的权限重新变成通用代码库令牌。

Shell 访问也遵循同样的规则。不要暴露带任意参数的 `gh`，然后把它称为发布接口。将接受的具体操作封装起来，验证参数，并拒绝其他所有内容。如果下个月需要新操作，就有意识地添加、测试，并决定确认应该放在哪里。

## 发布草稿是审核对象，不是已发布版本

GitHub 发布草稿可以让你组装公开记录，却不会立即公开。这样，审核者看到的是一个真实的对象，包括版本、目标提交、生成的说明、附件、校验和以及预发布状态。仅仅显示「准备发布 v2.4.0」并不是足够的证据。

GitHub 的 REST 文档区分了使用 `draft: true` 创建发布和通过改变草稿状态来发布版本这两种操作。GitHub 还说明，`make_latest` 会影响 GitHub 将哪个版本展示为最新版本。这些字段看起来像普通元数据，却会影响用户和工具。应把它们当作发布选择，而不是让代理自行推断的默认值。

可靠的请求应包含不可变标识符。版本字符串方便人阅读，但提交 SHA 才能标识你真正审核过的源代码。文件摘要能标识你真正构建出的文件。如果请求只有「发布 v2.4.0」，审核者就必须在急于等待的代理面前重新拼出整个版本。

可以使用这样的结构化批准记录：

```json
{
  "operation": "publish_release",
  "repository": "acme/widgets",
  "draft_id": "123456",
  "version": "v2.4.0",
  "target_sha": "8b0c5f7c2d6c4e1a9f1a3b0e5e92d7a4c6f80c11",
  "prerelease": false,
  "make_latest": "true",
  "assets": [
    {"name": "widgets-v2.4.0.tar.gz", "sha256": "b3e1..."},
    {"name": "widgets-v2.4.0.tar.gz.sha256", "sha256": "f18a..."}
  ],
  "notes_digest": "c921..."
}
```

批准者应该打开草稿，将这条记录与代码库状态进行比较。有人在审核后编辑草稿时，说明摘要听起来可能有些过度，但这时它很有用。你不必把每个逗号都视为不可变，但必须知道文件列表、目标或版本分类是否发生了变化。

草稿默认并非无害。拥有代码库访问权限的用户可能看得到它，内部工具也可能由此触发操作。只有在自己的环境中草稿确实被视为审核材料时，才可以把创建草稿放进低风险操作集合。如果草稿中的文件会流入包注册表或构建文件镜像，那么上传也应先获得批准。

## 将版本绑定到提交，再将构建文件绑定到版本

最常见的发布故障并不是恶意代理，而是版本、标签、源提交和二进制文件来自不断变化的代码库中的不同时间点。

干净的流程会先选择一个不可变的提交 SHA。构建从这个 SHA 开始，而不是从分支名称开始。构建完成后，代理记录文件摘要。随后才创建标签和草稿，而且两者都指向同一个 SHA。如果发布流程要求签名标签，签名身份应像发布凭据一样，在代理进程之外运行。

不要让代理使用 `main` 这样的可变引用作为公开版本的目标。构建可能从一个修订版开始，而代理创建版本前的另一次合并又推进了分支。如果代码要求 GitHub 在当时解析 `main`，GitHub 会正常地将发布附加到较晚的目标。结果可能是说明描述了一个修订版，文件却来自另一个修订版。

在出现批准提示前检查这种关系。简单的本地验证可以尽早失败：

```sh
git fetch --tags origin
git rev-parse "v2.4.0^{}"
git rev-parse HEAD
shasum -a 256 dist/widgets-v2.4.0.tar.gz
```

标签存在后，前两个对象 ID 必须一致。如果你在当前检出目录中构建，`HEAD` 也必须是审核过的 SHA。校验和命令会生成类似下面的一行：

```text
b3e1f2...  dist/widgets-v2.4.0.tar.gz
```

将这个精确摘要放进批准记录。不要让审核者只相信文件名。名为 `widgets-v2.4.0.tar.gz` 的文件可能是调试构建、旧构建，或来自另一种架构的二进制文件。

GitHub Actions 文档警告说，`pull_request_target` 会在基础代码库的上下文中运行。如果使用不当，它可能把写权限或机密暴露给不受信任的拉取请求代码。发布代理也要遵循同样的原则：不要在一个可以发布的任务中，从不受信任的代码构建可分发文件。将不受信任的验证与拥有发布权限的受控构建分开。

版本计算也需要固定规则。代理可以根据约定式提交或已合并标签提出版本建议，但建议不是证明。维护者应决定破坏性变更、回滚变更和发布分支如何影响版本。代码库的提交规范不一致时，自动语义化版本很容易静默出错。

## 生成的发布说明需要人工编辑

代理可以将已合并的拉取请求整理成一份像样的初稿。但它不知道某次迁移是否需要警告，不知道贡献者是否希望获得署名，也不知道某个行为变化是否会破坏提交标题中从未提及的使用场景。

让它按能暴露不确定性的章节生成说明。先写面向用户的变化，再写修复，最后写内部工作。加入拉取请求引用和所使用的提交范围。对于来自代码检查而非维护者描述的说法，明确标记其来源。这样，流畅的段落就不容易掩盖猜测。

我不会采用「根据 git log 写一份完善的发布说明」这个常见指令。它受欢迎，是因为能立即生成文档。但它并不正确，因为提交信息描述的是实现意图，不一定是已经发布的行为。名为「修复身份验证」的提交，可能修复测试夹具、改变错误字符串，或者关闭一个严重缺陷。发布说明需要描述实际的用户影响。

发布前让审核者回答几个简单问题：

- 目标提交是否包含这里声明的每项变更？
- 范围内是否仍有已删除或已回滚的拉取请求？
- 用户是否需要修改配置、数据、权限或客户端？
- 安全措辞是否需要由调查此事的人员审核？
- 这是预发布版本吗？GitHub 是否应该将它展示为最新版本？

不要把安全公告交给代理，让它改写成公开发布说明。披露时间、受影响版本、缓解办法和署名都必须由审核者决定。代理可以排版已经批准的文字，但不应决定哪些内容公开。

在审核时让发布说明和发布文件保持对应。当审核者看到「新增 ARM 支持」时，应该能在草稿中看到 ARM 文件，并在批准请求中看到它的校验和。这样可以发现文档审核和构建审核分开进行时容易漏掉的一类问题。

## 确认必须发生在不可逆边界之前

发布完成后才出现的确认对话框，只是在记录后悔。应把提示放在改变分发状态的操作之前，并展示将要授权的精确请求。

批准者需要足够的上下文，在几秒内发现不匹配，包括代码库、草稿标题、目标 SHA、文件名和摘要、预发布状态、最新版本选择，以及发起批准的进程。不要把代码库藏在折叠的详细信息面板中。发布到错误的代码库，可能比说明格式错误造成更大的损害。

以下操作应分别确认：

- 发布版本草稿
- 删除或取消发布版本
- 删除版本标签
- 替换现有发布文件
- 将版本从预发布改为稳定版，或将其标记为最新版本

列表有意保持简短。每次读取和每次计算文件摘要都要求许可，会让人习惯于不看内容就批准。要求在公开或破坏性边界做出决定，才能把注意力留在真正有意义的地方。

确认应绑定到请求摘要。如果代理在批准后修改草稿、添加文件或改变 `make_latest`，就应使批准失效并重新请求。不要因为版本字符串仍然匹配，就重复使用之前的点击。

Sallyport 的逐会话授权可以在代理执行任何操作前识别新的代理进程，逐次调用控制则可以要求对选定凭据的每次使用单独批准。这与发布工作非常匹配：允许经过审核的代理运行准备草稿，然后在它使用发布凭据时要求逐次确认。

不要把批准和身份验证混为一谈。代码签名身份或 CI 工作负载身份告诉你是哪一个进程发起了请求。确认则表示有人接受了这一个具体的对外操作。发布出错时，两类记录都需要保留。

## 不要让凭据进入代理提示词和环境

直接持有 GitHub 令牌的代理，可以用 curl 命令、修改后的工作流文件，或你忘记限制的工具调用绕过精心设计的批准接口。提示词中的指令无法弥补这个缺口。

将凭据保存在只执行允许操作的组件中。代理提交结构化请求。组件验证代码库、动词、目标引用和文件约束，获取所需批准，将凭据注入对外请求，然后返回结果。代理永远不会收到机密，即使是打码后的占位符也不会收到。

Sallyport 将 API 和 SSH 机密保存在加密保险库中，并自行执行 HTTP 或 SSH 操作，因此支持 MCP 的代理不必持有凭据。保险库锁定时，保险库网关会拒绝操作。在无人值守的机器状态下，这正是你想要的行为，而不是让令牌继续留在 Shell 环境中。

为发布使用单独的凭据。将它限制在实际需要处理的代码库中，并移除管理组织、管理 Webhook、编辑 Actions 设置或写入无关代码库的权限。GitHub Apps 通常比个人令牌更适合，因为安装范围和权限用途更明确。具体选择取决于你的代码库设置，但共享维护者令牌并不是一个好的发布身份。

不要把令牌粘贴到代理指令、`.env` 文件或 CI 构建文件中。打码并不能解决问题。只要进程能够读取机密，它通常就能把机密编码进输出字段、发布正文、文件名，或发往另一个服务的请求中。预防的起点，是永远不要把机密交给这个进程。

## 回滚是纠正发布的决定，不是删除按钮

删除 GitHub 版本可能会移除一个页面，却无法召回已下载的压缩包、缓存的元数据、已克隆的标签、镜像中的源代码，或已经触发的下游自动部署。把删除当作彻底撤销的发布代理，会让糟糕的事件更难调查。

先为回滚请求确定故障类型。如果文件有问题但没有危害，可以发布带有清晰说明的修正版，并决定是否将早期版本标为预发布，或将它移出最新版本位置。如果文件包含机密、恶意代码或严重漏洞，应先停止分发，撤销受影响的凭据，通过你运营的渠道联系用户，并保留调查所需的证据。

回滚操作应与发布分开由人工批准，因为两者的后果不同。提示应说明会改变什么，以及什么仍会对外可见。「删除版本 v2.4.0」表达得很弱。「删除 v2.4.0 的公开版本记录和文件；v2.4.0 标签仍指向 SHA X；现有下载无法召回」才让批准者清楚自己真正授权了什么。

不要让代理删除标签，只为了让发布说明看起来整齐。其他构建和用户可能依赖这些标签。如果必须替换标签，请在事件记录中保留旧 SHA，并说明这次变化。移动已经发布的版本标签，会让之后的验证困难得多。

更安全的默认选择是发布纠正版本。它能建立清晰的审计路径，让下游工具区分修正后的文件，也不会假装第一次发布从未发生。对于在任何人合理消费前意外发布的版本，删除有时有其用处，但这应是基于运营判断的决定，不是自动清理任务。

## 审计记录必须重建操作，而不是记录操作周围的故事

有人问是谁发布了一个版本时，「代理发布的」并不是答案。你需要知道代理进程、批准操作的人、执行操作的凭据机构、请求值，以及 GitHub 的响应。

为代理会话记录一个事件，为每个发布操作分别记录一个事件。会话记录回答谁启动了运行，以及它在何时失去权限。操作记录回答哪个等价于端点的操作实际发生了，以及最终使用了哪些值。两类记录都不要包含机密。

对于发布事件，至少记录代码库、草稿标识符、版本标识符、版本号、目标 SHA、代理提供的源引用、文件名和摘要、发布标志、说明摘要、决策时间、批准身份、进程身份、结果代码和响应标识符。失败也要记录。被拒绝的请求说明边界确实发挥了作用。

Sallyport 会从一个写入盲、加密、哈希链式的审计日志中生成会话和活动日志。它的 `sp audit verify` 命令可以在离线状态下对密文验证链条，无需保险库机密。对于发布证据，这项能力很有用，因为审计者可以检查记录是否被改动，而不必获得凭据材料。

日志不能替代控制措施。一份完美记录了代理发布错误软件包的日志，只能带来一份完整的复盘和一个糟糕的版本。只有当日志与狭窄操作、固定标识符以及不能在请求变化后继续有效的批准结合起来时，它才真正有价值。

## 发布门禁应在事实发生漂移时默认拒绝

发布门禁应该拒绝模糊状态，而不是让批准者替你弥补。如果草稿目标不同于审核过的 SHA，如果文件摘要不同于构建清单，如果草稿已经不存在，或者发布请求在批准后发生了变化，就停止并创建新的请求。

发布赶时间时，这种行为可能让人觉得过于严格。但它比事后查清标签是否在构建和发布之间移动，或代理是否在审核者检查旧文件后重新构建并上传了新文件，成本低得多。发布工作恰恰需要在这一点上保留一点摩擦。

在将门禁交给稳定版本使用前，用故意制造的失败案例测试它。让代理发布 SHA 不匹配的草稿。在计算校验和后修改文件内容。批准后改变草稿标题。尝试通过准备接口删除版本。每次尝试都应失败，并生成一条具体记录，告诉操作人员原因。

然后制定一条大家记得住的运营规则：代理准备一个命名的发布候选版本，人类批准这项精确的公开变更。如果当前自动化无法在那一刻明确代码库、提交、构建文件和回滚影响，就还没有准备好无人值守地发布软件。
