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

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

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

如何为私有 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. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,800)
  2. 应该知道的Linux技巧 (累计阅读 8,860)
  3. 完全用命令行工作 -- 一年后的思考 (累计阅读 7,400)
  4. 程序员装逼神器-TPP (累计阅读 7,240)
  5. dig挖出DNS的秘密 (累计阅读 5,680)
  6. php实现百度音乐采集下载 (累计阅读 5,460)
  7. Linux常用命令,命令行技巧 (累计阅读 5,100)
  8. 开启命令行下的社交 (累计阅读 5,060)
  9. Kindle 电子书生成工具 (累计阅读 5,040)
  10. 数据即代码,我和小伙伴们都惊呆了! (累计阅读 4,380)