IT技术博客大学习 共学习 共进步

How to Set Up Homebrew Tap for Private CLI Tools: A Complete Guide

Channel [K] 2026-06-03 09:03:24 累计浏览 8 次
本机暂存

如何为私有 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. 前期准备工作

  1. 建立公开的 Tap 仓库
  2. 命名规范:在你的组织下新建一个公开仓库,通常命名为 homebrew-taphomebrew-brew
  3. 这样用户可以使用 brew tap org/tap 引入。
  4. 准备具有仓库读写权限的 Token
  5. 在 GitHub 中创建一个具有访问目标 Tap 仓库推拉权限的 PAT (Personal Access Token),或使用细粒度的 Token。
  6. 将该 Token 配置为 CLI 源码仓库的 Actions Secret(例如 SKILLS_REPO_TOKEN)。
  7. 规范化打包产物
  8. 确保你的打包脚本能生成跨平台的压缩包(例如 cli-0.1.0-macOS-arm64.zip)。
  9. 必须生成带有文件 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

建议继续学习

  1. MySQL数据库数据类型之ENUM、SET、BOOL/BOOLEAN、TINYINT (累计阅读 3,820)
  2. MySQL数据库之数据类型集合类型和枚举类型测试环境 (累计阅读 2,940)
  3. How to Install Native Homebrew on an Apple Silicon M1 Mac (累计阅读 2,582)
  4. MySQL数据库之集合类型SET的DDL变更测试总结 (累计阅读 2,461)
  5. MySQL数据库数据类型之集合类型SET测试总结 (累计阅读 2,400)
  6. The “Bug-Free” Workforce: How AI Efficiency Is Subtly Disrupting The Interactions That Build Strong Teams (累计阅读 3)
  7. How To Improve UX In Legacy Systems (累计阅读 3)
  8. A Practical Guide To Design Principles (累计阅读 2)
  9. Moving From Moment.js To The JS Temporal API (累计阅读 1)
  10. A Designer’s Guide To Eco-Friendly Interfaces (累计阅读 2)