Homebrew 发布流程全解:brew release 命令、release.yml 工作流与主次版本发布规范
【免费下载链接】brew🍺 The Package Manager for Everywhere项目地址: https://gitcode.com/GitHub_Trending/br/brew
Homebrew 用户通过 GitHub release tag 获取新版 Homebrew/brew,而发布本身由拥有写权限的维护者通过brew release命令与 GitHub Actions 工作流共同完成。本文基于仓库中的 发布流程文档 展开,结合 release 命令实现、release 工作流 与 release 命令测试,完整讲解发布前检查、版本预览与草稿创建、主次版本的强制操作清单,以及发布背后的实现细节,帮助维护者或深度使用者理解并复现一次规范的 Homebrew 发布。
发布模型:谁可以发、用户如何拿到新版
- 用户从 GitHub release tag 接收 Homebrew/brew 的新版本。
- 只有对 Homebrew/brew 有写权限的维护者才能创建 release。
- 常规更新使用 patch 版本(
x.y.z递增 z),重大变更走 major/minor 版本,且受“一个月冷却期”限制。
从源码结构看,整个发布链路由三个环节构成:brew release命令做本地检查与预览 → 通过workflow_dispatch触发.github/workflows/release.yml→ 工作流构建、测试并上传 macOS 安装包,最后用gh release create --draft生成草稿 release,由维护者在网页端确认后正式发布。
发布前准备(Prepare the release)
文档列出了五步检查清单,全部通过后才能进入创建阶段:
- 检查是否有必须在发布前解决的紧急工作:
- Homebrew/brew 的 pull requests 与 issues;
- Homebrew/homebrew-core 的 issues;
- Homebrew 官方 Discussions。
- 给阻塞性 issue/PR 打标签:
- 必须在任何 release 前解决的,标记
release blocker; - 必须在下一个 major 或 minor release 前解决的,标记
major/minor release blocker。 - 只要有处于 open 状态的 issue 或 PR 带有相应标签,
brew release命令和 release 工作流都会拒绝创建发布。
- 必须在任何 release 前解决的,标记
- 确认 CI 通过:Homebrew/brew 的
main分支 workflows 全部通过,且至少有一个近期 Homebrew/homebrew-core 的 PR 成功跑完 CI。 - 留出回归检测时间:最后一次代码改动之后,要留足时间以便发现回归问题。
- 确认
main分支当前状态适合发布。
文档特别强调两条硬性约束:
- 不要从
main的较旧 commit 上创建 release。 - 如果紧急 patch release 必须排除某些未发布变更,正确做法是:先 revert 这些变更 → 走完整发布流程 → 发布后再把变更应用回来,而不是从旧 commit 打 tag。
源码印证:blocker 标签检查如何落地
在 Library/Homebrew/dev-cmd/release.rb#L47-L59 中,命令默认检查release blocker标签;当传入--major或--minor时,额外加入major/minor release blocker标签,通过 GitHub API 查询 open 状态的 issues/PRs,一旦发现 blocker 即打印其 URL 列表并odie退出:
blocking_labels = ["release blocker"] blocking_labels << "major/minor release blocker" if args.major? || args.minor? release_blockers = blocking_labels.flat_map do |label| GitHub.issues(repo: "Homebrew/brew", state: "open", labels: label) end这段逻辑被注释要求与工作流中的 "Check for release blockers" 步骤保持同步,对应 .github/workflows/release.yml#L47-L73:工作流侧用gh api做同样的标签检查,并对 tag 形如^[0-9]+\.[0-9]+\.0$(即 major/minor 版本号)的情况追加检查major/minor release blocker。因此无论本地命令还是 CI 任一侧,只要存在未解决的 blocker,发布都会被拒绝。
测试文件 Library/Homebrew/test/dev-cmd/release_spec.rb 用 RSpec 固定了这些行为:存在 open 的release blocker时命令以SystemExit退出并在 stderr 输出 blocker URL;major release 时还会单独校验major/minor release blocker。
创建发布:brew release 命令
第一步:预览版本与 release notes
brew release默认预览 patch 版本:读取最近一次 release 的 tag,把 patch 位加一,生成候选版本号,并调用 GitHub 的generate-release-notes能力生成 notes 后打印到终端。此时不会创建 release,也不会触发工作流。
需要 major 或 minor 预览时,加上对应参数:
brew release --major brew release --minor两个参数互斥(源码中conflicts "--major", "--minor")。对于 major/minor 发布,命令会额外输出一份面向博客的 notes(见 dev-cmd/release.rb#L91-L103):取上一 major/minor 版本到新版本之间的 release notes,过滤出* 描述 by @用户名 in PR链接格式的行,转成不带用户名的- 描述列表并排序——这就是后续官网 release notes 帖子的基础。
版本号的计算规则
dev-cmd/release.rb#L83-L89 给出了三类版本的推导逻辑,其中latest_version来自GitHub.get_latest_release "Homebrew", "brew":
new_version = if args.major? Version.new "#{latest_version.major.to_i + 1}.0.0" elsif args.minor? Version.new "#{latest_version.major}.#{latest_version.minor.to_i + 1}.0" else Version.new "#{latest_version.major}.#{latest_version.minor}.#{latest_version.patch.to_i + 1}" end.to_s主次版本的一个月冷却期
Homebrew 会拒绝在上一个 major 或 minor release 距今不足一个月时创建新的 major/minor release。实现位于 dev-cmd/release.rb#L68-L81:
one_month_ago = Date.today << 1 latest_major_minor_release = begin GitHub.get_release "Homebrew", "brew", "#{latest_version.major_minor}.0" rescue GitHub::API::HTTPNotFoundError nil end if latest_major_minor_release.blank? opoo "Unable to determine the release date of the latest major/minor release." elsif Date.parse(latest_major_minor_release["published_at"]) > one_month_ago odie "The latest major/minor release was less than one month ago." end它通过查询当前 major.minor 对应.0版本号的 release 发布时间,与“一个月前”比较:若发布日期晚于一个月前则直接报错;若查不到该 release 则仅打印警告。patch release 不受此限制。
第二步:创建草稿并触发工作流
确认预览无误后:
brew release --force必要时同样附带--major或--minor。--force会真正执行:创建草稿 release 并触发 release 工作流。
--force之后命令还会做三重防护(见 dev-cmd/release.rb#L127-L163):
- 重复 release 检查:查询同版本号的既有 release。若已存在草稿,提示先在网页界面删除;若已存在已发布的同号 release,则提示应运行
brew update而不是重新发布。 - main 分支同步检查:比较本地
origin/main的 SHA 与 upstreamHomebrew/brew的maincommit,不一致时报错Run brew update before brew release --force。测试用例 release_spec.rb#L11-L34 专门验证了这一点:当本地 SHA 与 upstream SHA 不同时,命令终止且不会调用workflow_dispatch_event。 - 触发并轮询工作流:以
tag: new_version为输入 dispatchrelease.yml,随后轮询 workflow run(首次等待 15 秒,之后每 5 秒一次,最多 180 次约 15 分钟),期间打印运行页面地址供监控;run 结论不是success时命令以错误退出。成功后打印新 release 的 URL 并用浏览器打开。
完成后,按文档要求:在 GitHub 的 draft release 列表中审阅草稿,确认版本号与 release notes 无误后手动发布(publish)。这一步由维护者在网页端完成,命令本身不会自动发布。
release.yml 工作流:从 tag 到安装包
.github/workflows/release.yml 定义了三段式流水线build→test→upload:
触发条件
workflow_dispatch:输入必填参数tag(release 的 git tag),这是brew release --force的入口;push到package/**、Library/Homebrew/utils/macos_user.sh或工作流自身改动时也触发(用于验证安装包相关变更),但此时不会创建草稿 release。
build 任务(macos 运行器)
workflow_dispatch时先做与本地命令同款的 release blocker 标签检查(见上文);- 清除本地 API 缓存,安装 Pandoc;
- 从 secrets 恢复 Apple Developer 证书到临时 keychain;
- checkout 完整仓库(
fetch-depth: 0)后,若输入了TAG则执行git tag,并用git describe --tags校验 tag 与实际版本一致; - 准备随包 API 缓存(
cache_api目录); - 检查安装包脚本的 Git 安全性(postinstall 中的特权 git 操作必须走
git_no_hooks); pkgbuild构建 Homebrew 组件包,签名(--sign),过滤测试 fixtures,最低系统版本由HOMEBREW_MACOS_OLDEST_SUPPORTED控制;productbuild生成最终Homebrew-<version>.pkg;actions/attest生成构建溯源(build provenance),上传产物到 Actions artifacts。
test 任务(macos-15 / macos-26 矩阵)
下载安装包 → 清掉已有 Homebrew 安装 → 用sudo installer真实安装 → 校验brew --version输出等于Homebrew <TAG>→brew config/brew doctor→ 再次卸载重装并复测,验证安装/升级两条路径。
upload 任务
xcrun notarytool submit --wait对 pkg 做 Apple 公证;- 仅对
workflow_dispatch事件执行gh release create "${TAG}" --draft --generate-notes --fail-on-no-commits,即草稿 release 由工作流创建; - 把 pkg 重命名为无版本号的
Homebrew.pkg并上传到该 release,作为固定路径的安装入口。
可以看到,用户侧的“安装/升级”(brew update)与工作流侧的“构建/发布”是同一 tag 语义的两端:工作流保证 tag 对应一个经过签名、公证并在真实安装器上验证过的安装包。
major / minor 发布的强制操作
patch release 不需要以下操作;创建 major 或 minor release之前必须完成(对应 Releases.md 的 Major and minor releases 小节):
- 删除标记为
odisabled的代码; - 把标记为
odeprecated的代码改为odisabled; - 把标记为
# odeprecated的注释代码取消注释(应进入废弃周期的); - 加入计划中的
odeprecations; - 删除仍带
replacement:参数的命令参数定义(即上一周期已废弃的 CLI flag,其替代方案已稳定,可清理过渡参数)。
底层机制:odeprecated / odisabled 的实现
Homebrew 自身代码的废弃生命周期(deprecate → disable → remove)由odeprecated/odisabled支撑,完整规范见 Deprecating, Disabling and Removing 文档。实现位于 Library/Homebrew/utils/output.rb#L157-L246:
odeprecated(method, replacement:, disable:, disable_on:, disable_for_developers:):向调用者打印 “Calling X is deprecated/disabled!” 及替代方案。在开发模式(HOMEBREW_DEVELOPER=1,disable_for_developers默认为true)下会抛出异常而非仅警告,保证维护者第一时间感知废弃调用;disable_on到期后自动升级为禁用。odisabled(method, replacement:):内部就是odeprecated(..., disable: true),对所有用户直接报错。
这与文档表格中的四阶段一致:# odeprecated注释占位 → 取消注释生效(用户见警告)→ 改为odisabled(用户见错误)→ 随下一个 minor/major release 删除代码与replacement:参数。每次状态迁移都绑定在 minor/major 发布上,patch release 不参与——这正是把上述操作清单挂在“major/minor 发布之前”的原因。
release notes 与对外公告
- 将
brew release [--major|--minor]的输出作为官网 release notes 帖子的底稿,并编辑 notes,解释变更的目的与用户影响,而不只是罗列改了什么。major/minor 版本的 release notes 正文会指向 Homebrew 博客上的对应帖子(见 dev-cmd/release.rb#L106-L110)。 - release 与帖子发布后,通过项目当前维护的官方渠道公告;只有在预期触达规模与审核成本匹配时,才考虑更广泛的公告渠道。
操作速查与适用前提
| 场景 | 命令 / 动作 |
|---|---|
| 预览 patch release | brew release |
| 预览 major / minor release | brew release --major/brew release --minor |
| 创建草稿 + 触发工作流 | brew release --force(按需加--major/--minor) |
| 草稿审阅后 | 在 GitHub releases 页面确认版本与 notes,手动 publish |
| major/minor 发布前 | 完成odisabled删除、odeprecated→odisabled升级、# odeprecated启用、新增 deprecations、清理replacement:五步 |
适用前提与限制:
- 执行者需要对 Homebrew/brew 仓库有写权限(命令帮助文本明确说明 “Requires write access to the Homebrew/brew repository”,且该命令
hide_from_man_page!,不在公开 manpage 中展示); - 执行前必须保证本地
origin/main与 upstreammain一致,否则需先brew update; - 存在
release blocker(或主次版本场景下的major/minor release blocker)open 问题时,命令与工作流双侧都会拒绝发布; - 上一 major/minor release 距今天数不足一个月时,新的 major/minor release 会被命令直接拒绝;
- patch release 只能基于
main的最新 commit;需要剔除变更时用 revert-发布-再应用 的流程,而不是从旧 commit 打 tag。
小结
Homebrew 的发布流程把“人”的检查与“机器”的防线叠在一起:文档层面用 blocker 标签、CI 状态、冷却期约束人的决策;brew release 命令 层面用版本推导、重复 release 检查、SHA 一致性校验和轮询机制约束执行;release.yml 工作流 层面则保证每个 tag 背后是一个签名、公证并在真实安装器上回归过的安装包。理解这条链路后,无论是执行一次 patch 发布,还是主导一次 major/minor 发布(含废弃生命周期的状态迁移),都有明确、可验证的步骤可依。
【免费下载链接】brew🍺 The Package Manager for Everywhere项目地址: https://gitcode.com/GitHub_Trending/br/brew
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考