如何为私有 CLI 工具提供 Homebrew 一键安装能力(完全指南)
这份指南总结了如何为内部或半公开的 CLI 工具构建稳定、全自动且支持降级的 Homebrew Tap 分发流程。主要经验基于为 mes-cli 开发 brew install 功能的实践。
1. 核心挑战与架构决策
在实现 brew install 时,由于 CLI 源码及 Releases 是私有仓库 (Private Repository),外部用户或内部员工在使用 brew install 下载时,如果在终端没有配置强权限的 GITHUB_TOKEN,会直接报 404 错误。
我们的解决方案:脱离 GitHub Releases,使用公开的 OSS / CDN
1. 源码编译与发布:CLI 仓库依然通过 GitHub Actions 完成编译跨平台包并生成 Releases。
2. 资产分发:流水线将生成的 ZIP 压缩包和 checksums.txt 同步推送到公开的阿里云 OSS(或 CDN)上。
3. Formula 托管:建立一个公开的 homebrew-tap 仓库。流水线根据 OSS 上的资源,自动拼接出 Ruby 安装脚本(Formula),推送到该 Tap 仓库。
4. 客户端安装:用户的 brew install 会从公开的 Tap 仓库拉取脚本,并从公开的 OSS 高速下载压缩包,全程无权限阻碍。
2. 前期准备工作
- 建立公开的 Tap 仓库:
- 命名规范:在你的组织下新建一个公开仓库,通常命名为
homebrew-tap或homebrew-brew。 - 这样用户可以使用
brew tap org/tap引入。 - 准备具有仓库读写权限的 Token:
- 在 GitHub 中创建一个具有访问目标 Tap 仓库推拉权限的 PAT (Personal Access Token),或使用细粒度的 Token。
- 将该 Token 配置为 CLI 源码仓库的 Actions Secret(例如
SKILLS_REPO_TOKEN)。 - 规范化打包产物:
- 确保你的打包脚本能生成跨平台的压缩包(例如
cli-0.1.0-macOS-arm64.zip)。 - 必须生成带有文件 SHA256 校验值的清单文件,如
checksums.txt,供后续提取使用。
3. GitHub Actions 流水线自动化
核心的魔法在于 CLI 源码仓库中的发布流水线(如 .github/workflows/release.yml)。它在每次打标签发布后,需要执行以下脚本去自动化生成 .rb 文件并提交。
自动化脚本模板
update-homebrew-tap:
name: Update Homebrew Tap Formula
runs-on: ubuntu-latest
needs: upload-oss # 必须等你的产物上传到公共 OSS 之后执行
steps:
- name: Checkout tap repo
uses: actions/checkout@v4
with:
repository: your-org/homebrew-tap
token: ${{ secrets.SKILLS_REPO_TOKEN }}
path: homebrew-tap
- name: Generate Formula and update tap
env:
VERSION: ${{ github.ref_name }} # 比如 v0.4.9
REPO: ${{ github.repository }}
run: |
VER_NUM=${VERSION#v}
# 1. 直接从公共 OSS 下载 checksums.txt (避免 GitHub 私有权限问题)
wget "https://your-public-oss.com/tools/cli/${VER_NUM}/checksums.txt" -O checksums.txt
# 2. 从 checksums.txt 中精准提取各平台的 SHA256
SHA_MAC_ARM=(grep "cli-{VER_NUM}-macOS-arm64.zip" checksums.txt | awk '{print $1}')
SHA_MAC_AMD=(grep "cli-{VER_NUM}-macOS-amd64.zip" checksums.txt | awk '{print $1}')
SHA_LIN_ARM=(grep "cli-{VER_NUM}-linux-arm64.zip" checksums.txt | awk '{print $1}')
SHA_LIN_AMD=(grep "cli-{VER_NUM}-linux-amd64.zip" checksums.txt | awk '{print $1}')
OSS_URL="https://your-public-oss.com/tools/cli"
mkdir -p homebrew-tap/Formula
# 3. 生成无特殊字符的类名后缀 (解决 Ruby 类名规范:例如 0.4.9 变成 049)
CLASS_SUFFIX=(echo "VER_NUM" | sed 's/[^a-zA-Z0-9]//g')
# --------------------------------------------------------------------
# 生成 1:永远指向最新的主 Formula (cli.rb)
# --------------------------------------------------------------------
cat > homebrew-tap/Formula/cli.rb <
class Cli < Formula
desc "Your awesome CLI tools"
homepage "https://github.com/$REPO"
version "${VER_NUM}"
if OS.mac? && Hardware::CPU.arm?
url "{OSS_URL}/{VER_NUM}/cli-${VER_NUM}-macOS-arm64.zip"
sha256 "${SHA_MAC_ARM}"
elsif OS.mac? && Hardware::CPU.intel?
url "{OSS_URL}/{VER_NUM}/cli-${VER_NUM}-macOS-amd64.zip"
sha256 "${SHA_MAC_AMD}"
elsif OS.linux? && Hardware::CPU.arm?
url "{OSS_URL}/{VER_NUM}/cli-${VER_NUM}-linux-arm64.zip"
sha256 "${SHA_LIN_ARM}"
elsif OS.linux? && Hardware::CPU.intel?
url "{OSS_URL}/{VER_NUM}/cli-${VER_NUM}-linux-amd64.zip"
sha256 "${SHA_LIN_AMD}"
end
def install
# 将二进制安装到系统 PATH
bin.install "bin/cli"
# 避坑点:如果有其他额外的目录或文件(如 skills/、assets/),必须显式复制到 prefix 下!
prefix.install "skills"
end
def test
system "#{bin}/cli", "--version"
end
end
EOF
# --------------------------------------------------------------------
# 生成 2:带有版本号的防灾降级 Formula (cli@${VER_NUM}.rb)
# --------------------------------------------------------------------
# 代码完全同上,唯一区别是 Ruby 的类名必须加上 AT 版本号后缀
cat > homebrew-tap/Formula/cli@${VER_NUM}.rb <
class CliAT${CLASS_SUFFIX} < Formula
# 内容与上方一致...
EOF
- name: Commit and push formula
run: |
cd homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/
if git diff --cached --quiet; then
echo "No changes to commit."
else
git commit -m "chore: release formula for cli $VERSION"
git push origin HEAD
fi
4. 关键经验与防坑指南
4.1 额外目录被 Homebrew 丢弃的问题
现象:用户执行 brew install cli 后,发现二进制和 README.md 都有了,但压缩包里的其它自定义目录(如 skills/)不见了。
原因:Homebrew 默认只关心你在 def install 里指明的安装内容。
对策:如果 ZIP 包里附带了需要长期存储的文件夹,你必须通过 prefix.install "your-folder" 将其强行放入 Homebrew 的 Cellar 根目录。
4.2 提供“降版本”的后悔药机制
现象:如果最新发布的 CLI 携带了阻断性 Bug,CLI 自身的自动更新程序无法向下降级。而 Homebrew 原生的 brew switch 已经被官方弃用。
对策:如上述自动化脚本中的“生成 2”,除了生成固定的 cli.rb 之外,必须针对每一次发布生成带有版本号的文件,如 cli@0.4.9.rb。
Ruby 语法限制:文件名包含 @ 与 . 时,Ruby 内部的 Class 名字必须去掉标点并转化为大驼峰。例如 cli@0.4.9.rb 内部类名必须叫 CliAT049,否则脚本报错。所以我们在流水线中加入了 CLASS_SUFFIX=(echo "VER_NUM" | sed 's/[^a-zA-Z0-9]//g') 进行动态替换。
5. 最终暴露给用户的完美用法
你只需要在文档中提供这段精简的指引即可:
安装与更新:
brew tap your-org/tap
brew install cli
# 未来的更新(自动更新本体、附加目录和文档)
brew upgrade cli
回退稳定旧版本:
# 假设最新版有问题,想要退回 0.4.9
brew install cli@0.4.9
brew unlink cli
brew link --overwrite cli@0.4.9