Homebrew 维护者专项故障处理:bottle 发布失败后的回滚重放与陌生构建失败排查
本文面向拥有 homebrew/core(或任意 tap)写权限的 Homebrew 维护者,系统梳理 docs/Common-Issues-for-Maintainers.md 中记录的两类维护者专有恢复流程:一类是「formula 提交已生成、但 bottle 发布失败」时的止血与重放方案;另一类是面对"陌生构建失败"时应遵循的排查纪律。这两类流程不属于普通用户的日常排障范畴,通常需要操作规范化的 tap 仓库规范检出(canonical tap checkout)并拥有 GitHub 写权限。读完本文,你将能:在 brew pr-pull / brew pr-upload 发布链路半途失败时判断正确动作、在不破坏未发布提交的前提下安全回退并重放、以及在构建异常出现时避免"以一次错误字符串为据打补丁"的误诊。
该文档定位为"维护者专用",因此文中的命令默认会在
homebrew/core的规范检出上执行 git 历史改写(reset --hard、创建/删除备份分支等)。请先在临时 clone 或 Git worktree 中演练,任何既有本地成果务必先落盘或备份。
前置背景:从 PR 到 bottle 发布的完整链路
在深入恢复流程前,先理解这条链路为什么失败现场会以"部分提交 + 未发布 bottle"的形态出现。
对 homebrew/core 而言,一条 formula 改动 PR 的正常合入流程是:CI(tests.yml)在各支持平台构建成功后,产出以 bottles{,_*} 为默认模式的构建产物(artifact);随后由维护者(或 BrewTestBot)执行 brew pr-pull 把 PR 提交落到规范检出中,并为合并后的 formula 写回更新后的 bottle do 块,最后把构建出的二进制包发布到 GitHub Packages(或 GitHub Releases)。之后才会用 GitHub 的标准 Merge/Squash/Rebase 合入,具体协作约定见 docs/Homebrew-homebrew-core-Maintainer-Guide.md。
两个命令是整个链条的执行器:
brew pr-pull PULL_REQUEST:从 PR 抓取并 cherry-pick 提交、下载 workflow 产出的 bottle 产物、执行版本一致性检查,最后委托brew pr-upload完成"生成 commit + 上传"。它默认以 canonical tap checkout(即$(brew --repository homebrew/core),默认homebrew/core)为操作对象,无论你当前在哪个目录执行。源码见 dev-cmd/pr-pull.rb:默认 tap 取CoreTap.instance.name(第 84–87 行),下载完产物后拼接upload_args = ["pr-upload"]并同步透传--no-commit、--dry-run、--keep-old、--warn-on-upload-failure等开关(第 176–187 行)。brew pr-upload:面向当前工作目录内的*.bottle.json运行。读取全部 JSON、按需合并生成/重写 formula 的 bottle DSL、执行brew audit --skip-style自检,最后把 bottle 推送到 GitHub Releases 或 GitHub Packages。
换言之:失败点可能发生在 pr-pull(下载/提交阶段)也可能发生在 pr-upload(上传阶段)。下述恢复流程正是在"上传失败、而 formula 提交本身已正确"这一精确场景下设计的。
场景一:提交已生成、仅 bottle 发布失败
如果失败发生时,formula 的提交内容是正确的(bottle do 块、sha256 摘要、版本号等都没问题),只是上传动作失败了,那么恢复流程要尽量克制,不要重做整个 pr-pull。
恢复四步曲
按 Common-Issues-for-Maintainers.md 的指引,正确动作如下:
-
从失败 workflow 中下载并解压 bottle 产物(artifact)。产物就在失败的那次 CI 运行中,无需重新构建。
-
进入解压后的产物目录。这一点关键:
brew pr-upload的行为严重依赖当前工作目录。 -
带上合适的上传参数执行:
brew pr-upload --no-commit -
发布成功后收尾(回退规范检出、删除备份分支等见下文)。
为什么是 --no-commit
阅读 pr-upload 的源码可解释每个开关的取舍。入口 dev-cmd/pr-upload.rb 的 run 首先扫描当前目录:
json_files = Dir["*.bottle.json"]
odie "No bottle JSON files found in the current working directory" if json_files.blank?
随后,除非传入了 --upload-only,否则它会先执行 brew bottle --merge --write(第 62、93 行)——也就是说默认情况下,它会重新把 JSON 合并进 formula 并生成一个新的 commit。这正好是本次场景想避免的:既然失败提交的内容已正确,再生成/改写一次提交是多余的,甚至会污染现场。
--no-commit 开关在参数表中被定义为 "Do not generate a new commit before uploading"(第 23–24 行),它让 pr-upload 只做"上传/补传"这件事。注意它和 --upload-only 互斥(conflicts "--upload-only", "--no-commit",第 39 行),二者语义一个跳过合并、一个跳过提交,不可同用。
合并之后,只要不是 --no-commit,还会对本次涉及的 formula 执行 brew audit --skip-style(第 105–111 行),确保刚写回的 bottle commit 没有破坏 lint/audit,这也是判断"提交是否正确"的一道自动化闸门。
上传目标由 JSON 中 bottle.root_url 决定:匹配 GitHub Releases 的 URL 规则则走 GitHubReleases.upload_bottles(第 115–117 行),匹配 GitHub Packages 的 URL 规则则走 GitHubPackages.upload_bottles(第 118–123 行),后者会把 --warn-on-upload-failure 映射为 warn_on_error 参数传入。
关键前提:pr-pull 只认规范检出
文档特别强调:
brew pr-pullalways operates on the canonical tap checkout, regardless of the current directory.
这决定了后续所有 git 回退动作都必须在规范检出上进行——这也是下文所有命令都带 -C "$(brew --repository homebrew/core)" 的原因。如果你需要让 git 操作指向其他 tap,可参照 docs/How-to-Create-and-Maintain-a-Tap.md 中 brew pr-pull --tap=$YOUR_GITHUB_USERNAME/tap 的用法为命令指定 --tap。
场景一进阶:从更早的提交"完整重放" pr-pull
如果上传失败比"补传"更严重——例如需要从失败 pr-pull 创建提交之前的某个提交开始整体重做——那么仅靠 --no-commit 补传就不够了。此时要把规范检出重置到那个历史位置,重新跑一次 pr-pull。这属于对共享检出执行破坏性操作,因此文档给出了严格的"先备份、后回退、终恢复"流程。
第 0 步:确认无未提交工作 + 建立备份分支
在任何 reset --hard 之前,必须满足两个前提:规范检出中没有任何未提交的工作;并且远端已经被 fetch,以便后续可以回到 origin/HEAD。
git -C "$(brew --repository homebrew/core)" status --short
git -C "$(brew --repository homebrew/core)" fetch origin
git -C "$(brew --repository homebrew/core)" branch pr-pull-recovery-backup
三个命令依次完成:确认工作区干净、拉取远端引用、在当前提交处创建一个备份分支 pr-pull-recovery-backup。备份分支是这条恢复路径的安全网:即使下面的 reset --hard 出现偏差,你依然可以从该分支找回被丢弃的提交。
第 1 步:把规范检出重置到失败前的提交
COMMIT_SHA 取失败的那次 brew pr-pull 创建提交之前的那个提交(例如用 git log 找到失败前最后一次干净的远端提交):
git -C "$(brew --repository homebrew/core)" reset --hard COMMIT_SHA
第 2 步:以原始参数重放 pr-pull
brew pr-pull PULL_REQUEST
务必"with the original options"——即把初次执行时使用的所有选项原样带上(如 --keep-old、--no-upload、--autosquash、--head-sha 等)。如果只是想要"下载但不发布",可加 --no-upload 仅验证下载与提交阶段(见 dev-cmd/pr-pull.rb)。
为什么必须从"失败前的提交"重放而不是直接在失败现场补跑?从源码看,
pr-pull会先基于merge-base origin/HEAD current_branch_head计算基准(dev-cmd/pr-pull.rb),再 cherry-pick PR 提交(第 140–150 行)。如果现场已被部分提交污染,重新计算出的 merge-base 与期望不符,可能把 PR 提交重复应用或漏掉。从干净的基准状态重放,才能保证提交集合与首次执行一致。
第 3 步(按需):部分上传成功时使用 --warn-on-upload-failure
文档在此处给出了一条精确的边界:
Add
--warn-on-upload-failureonly when bottles were partially uploaded and their checksums are known to match the existingbottle doblock.
也就是说,只有当你确认瓶子的上传是"部分成功"(一部分已传上去、一部分失败)且这些产物当前的 checksum 与 formula 中既有的 bottle do 块完全一致时,才把该开关追加进重放命令:
brew pr-pull --warn-on-upload-failure PULL_REQUEST
该开关的语义在参数表中写明是 "Warn instead of raising an error if the bottle upload fails. Useful for repairing bottle uploads that previously failed."(dev-cmd/pr-pull.rb)。在实现上它被透传给 pr-upload(dev-cmd/pr-pull.rb),再作为 warn_on_error: 传入 github_packages.rb 的 upload_bottles/upload_bottle/preupload_check 系列方法——上传冲突/失败时把"直接 raise 中断"降级为告警继续。它天然是面向"半程补传"设计的,不是给全量失败兜底用的:如果 checksum 对不上(说明此前上传的二进制与当前 formula 不一致),仍应报错而不是被静默放行。
第 4 步:发布成功后回到默认分支并清理备份
发布成功后,把规范检出交还给默认远端分支(origin/HEAD 在步骤 0 已 fetch):
git -C "$(brew --repository homebrew/core)" reset --hard origin/HEAD
随后确认备份分支不再需要之后再删除它。通常的判据是:重放后的 origin/HEAD 已包含所有预期提交、远端也完成推送。清理命令(由维护者本人依据实际情况执行):
git -C "$(brew --repository homebrew/core)" branch -D pr-pull-recovery-backup
此流程的禁用条件
文档给出了一条硬性红线:
Do not use this recovery procedure when the canonical checkout contains work that has not been committed or preserved elsewhere.
只要规范检出中还有未被提交、或未在其他地方保留的工作,就绝不能走 reset --hard 这套恢复流程。宁可先在临时 clone 或 Git worktree 中复现旧状态、把现场工作另行保存,也不要让共享检出的未提交成果成为 reset 的牺牲品。
场景二:面对陌生构建失败,先定位再动手
第二类"常见问题"不是操作恢复,而是一条纪律性指引,针对的是维护者在 CI 或本地构建中偶遇的、先前未见过(unfamiliar)的构建失败。
不要用"一句旧错 + 一条旧 issue"打永久补丁
文档的忠告直白且具有普遍性:
Do not add a permanent workaround based only on an old issue or an exact linker error string.
原因在于:链接错误字符串(如某符号未定义、某库缺失)在不同 formula、不同系统版本上的表象可能完全相同,但根因却分属三类不同主体(见下)。把一个基于"看起来像同一错误"的临时 workaround 固化进 formula,等于把一个未经验证的假设写进了长期维护路径——今后每次构建都可能背着这个错误补丁。
可复现的定位流程
正确路径是先复现、再归因、后决策:
- 在受支持的 runner 上复现失败(如 GitHub Actions 上的标准镜像,或与 CI 环境一致的本地环境),确保失败可稳定重现而不是一次 flake。
- 检查编译器与链接器的实际调用(invocation):查看完整的编译/链接命令行、flag、环境变量、搜索路径,而不是只看报错的最后几行。Homebrew 会在构建日志中打印完整的 compiler/linker invocation,源码层面对构建环境的接管逻辑可参考 compilers.rb 与 compiler_selector.rb。
- 判定问题来源,三类主体三选一:
- formula 自身:版本更新带来的新构建要求、依赖声明缺失、patch 失效等;
- 上游(upstream):源码在特定平台/编译器的真实缺陷或行为变更;
- runner 镜像:构建环境本身的问题(工具链更新、镜像组件漂移),与 formula 无关。
归因直接决定了下一步动作:若是 upstream 或 runner 的问题,不应把 workaround 写进 formula;若是 formula 问题,才进入正常的修复与评审流程。
有价值的结论应沉淀到指南而不是错误清单
Document reusable findings in the relevant maintainer or formula-author guide instead of adding one-off historical errors to this page.
这条规则保护了 Common-Issues-for-Maintainers.md 的定位:它只承载可复用的流程与机制,不承担"历史错误堆砌"的功能。一次性的、仅对某个历史版本成立的错误详情,应当沉淀到 docs/Homebrew-homebrew-core-Maintainer-Guide.md、docs/BrewTestBot-For-Maintainers.md 或面向 formula 作者的 docs/Formula-Cookbook.md 等主题指南中;而本文件只收录"下次还会遇到、且动作可以标准化"的流程。
与相邻文档的分工
要正确使用本文的恢复流程,建议先明确以下文档边界:
- docs/Common-Issues.md:面向普通用户的排障;
- docs/Common-Issues-for-Maintainers.md:本文主体,仅记录维护者独有的恢复流程;
- docs/Homebrew-homebrew-core-Maintainer-Guide.md:维护者在合并 PR、管理依赖、处理 rebase/cherry-pick 时的完整规范;其中明确"不要 rebase 已推送到
main的提交,只改写未发布提交",与本文件"只针对失败pr-pull创建的未发布提交做 reset"的原则互为表里; - docs/BrewTestBot-For-Maintainers.md:说明何时需要人工用
brew pr-pull介入(需要下载 bottle 产物并本地改写未发布提交时,第 37 行); - docs/How-to-Create-and-Maintain-a-Tap.md:第三方 tap 如何用
brew pr-pull立即发布 bottles,可作为非homebrew/core场景的参照; - docs/Bottles.md:bottle 的 DSL(
bottle do ... end块)、root_url/sha256/cellar的语义——恢复流程中判断"checksum 是否与现有bottle do块一致"正是围绕该结构进行。
小结:一套可执行的决策树
把上述内容压缩为维护者现场可用的决策路径:
- 失败发生在哪一段? 若 formula 提交已正确、仅是上传失败 → 从失败 workflow 下载 artifact、进入其目录、执行
brew pr-upload --no-commit补传(场景一基础流程)。 - 需要从更早状态整体重放? → 先在规范检出上确认
git status --short干净并git fetch origin,创建备份分支pr-pull-recovery-backup,reset --hard到失败pr-pull之前的COMMIT_SHA,以原始选项重跑brew pr-pull。 - 只是部分上传成功? → 仅在 checksum 与既有
bottle do块可确认一致的前提下加--warn-on-upload-failure。 - 收尾 → 成功后
reset --hard origin/HEAD;确认无用时再删备份分支。检出中任何未提交成果未保全时,一律拒绝该流程。 - 遇到陌生构建失败 → 先在受支持 runner 上复现、检查 compiler/linker 调用,判定根因归属 formula/upstream/runner 三者之一,再把可复用的结论写进对应指南,而不是给本页堆积一次性错误案例。
这套流程的本质,是围绕 brew pr-pull 与 brew pr-upload 对"规范检出"和"当前目录"的不同依赖关系(分别见 dev-cmd/pr-pull.rb 与 dev-cmd/pr-upload.rb)建立的可逆操作纪律:任何破坏性 git 操作之前先留备份,任何恢复操作都以最小的动作集完成目标,任何结论都要能被复现所支撑。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00