🍻

各リポジトリをプライベートにしたままHomebrew Caskで自身のツールを管理する

に公開

🆕 追記:2025/08/22
この記事を投稿したところ、X(Twitter)を通じてGoReleaserのドキュメントのコントリビュートに共同著者として参加しませんか? というお誘いを頂きました。

https://x.com/sushichan044/status/1951251177708069325

https://github.com/goreleaser/goreleaser/pull/5944

@sushichan044 さん、この場を借りて厚く御礼を申し上げます 🙇‍♂️

また、上記PRの中でOS判別の方法について良い方法を知ることができたので、その部分を記事本文中にも反映しております。

https://github.com/goreleaser/goreleaser/pull/5944/files#diff-71089e38cb6ce974bd3aef436301b14a3df647fd2ae1d28351c18b75b7601fdeR239

- if RbConfig::CONFIG["host_os"].include?("darwin")
+ if OS.mac?
  system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/foo"]
end

OSの判別ができるようになってるのは便利ですね。


様々なルーティンワークを自動化するために、空いているスキマ時間を使い、CLIツールを日々開発して面倒くさい作業を自動化しています。
ふと開発が一段落したツールを go run main.go ~~~~ と書かず、直接バイナリ名で実行したいなと思った場合、以下の2パターンが思いつくのではないでしょうか。

  1. ホームディレクトリ配下に bin ディレクトリを作成、 PATH に登録し、その下にビルドしたバイナリを配置する
  2. Homebrewを利用する

今までは1. の方法でやっていたのですが、開発PCを買い替えるなどにより環境が大きく変化した場合、また PATH に登録するなどの環境構築が面倒であったり、DockerやGitHub Actionsなどの仮想環境で自分が作成したツールをインストールして実行したい場合、この環境を整える作業が面倒だなと思っていました。
また、brew install で自分のツールが入れられるのもかっこいいな!?と思い、2. の方法を試すことにしました。

その際、実装したツールは自身で使うのみとしたいので、GitHubのリポジトリをプライベートにしたままHomebrew経由でインストールできないかと思い試行錯誤した結果、現段階でベストだと思う方法が見つかりましたので共有します。

前準備

ツールの開発

まずは、作りたいツールを検討し開発しましょう。これがなければ始まらない!

その際、リポジトリは一般公開したくないためプライベートに設定し、自分以外はアクセスできないようにします。

Homebrew Cask用のリポジトリを作成

Homebrew経由で開発したツールをインストールするための設定を管理するHomebrew Cask用のリポジトリを別途作成しましょう。
こちらも外部に一般公開したくないため、プライベート設定にします。

なお、リポジトリ名は brew installbrew tap を実行する際に指定する名称にもなるため、なるべく推奨されたリポジトリ名にしましょう。

https://docs.brew.sh/Taps

私は自身の開発したツールを管理することを意味付けしたいため、以下のようなリポジトリ名にしました。

<ユーザ名>/homebrew-tools

上記の場合、 brew コマンドを使う際は以下のように指定することとなります。

$ brew tap <ユーザ名>/tools
$ brew install <ユーザ名>/tools/<ツール名>

GitHub上で空のリポジトリを作成したら、後は後述するGoReleaserによって自動的にファイルが追加・更新されていきますので、これ以降はほぼ触りません。

GoReleaser CLIツールのインストール

今回は GoReleaser を使い、Go言語で開発したツールの以下作業を自動化することにしました。

  • ビルド
  • アーカイブ
  • GitHubのRelease作成とバイナリのアップロード
  • Homebrew Caskの設定更新

そのため、ローカル環境にGoReleaserのCLIツールをインストールします。

https://goreleaser.com/install/

GoReleaserの初期化

では、まず作業するにあたり自身で開発をしたツールのリポジトリにてGoReleaserの設定を初期化しましょう。

以下のコマンドを実行します。

$ goreleaser init
  • generating .goreleaser.yaml
  • setting up .gitignore
  • done! please edit .goreleaser.yaml and .gitignore accordingly.
  • thanks for using GoReleaser!

実行した場所に .goreleaser.yaml を生成し、 .gitignore にコミット対象外とするディレクトリを設定してくれます。

GoReleaserの設定ファイル変更

次に、 .goreleaser.yaml の内容を変更します。

以下の公式ドキュメントを見て、自分の環境に合わせて変更しましょう。

https://goreleaser.com/customization/

私は以下のようにしました。

.goreleaser.yaml
version: 2

project_name: <開発したツールのプロジェクト名>

before:
  hooks:
    - go mod tidy
    - go generate ./...

builds:
  - flags:
      - "-trimpath"
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - "arm"
      - "arm64"
      - "amd64"
    ignore:
      - goos: darwin
        goarch: "arm"
      - goos: windows
        goarch: "arm"

universal_binaries:
  - replace: true

archives:
  - formats: [zip]
    name_template: |
      {{ .ProjectName }}_
      {{- if eq .Os "darwin" }}macOS
      {{- else }}{{- title .Os }}{{ end }}
      {{- if eq .Arch "all" }}
      {{- else if eq .Arch "amd64" }}_x86_64
      {{- else }}_{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}

changelog:
  sort: asc
  filters:
    include:
      - "^feat:"
      - "^fix:"
      - "^docs:"
      - "^refactor:"

release:
  draft: false
  replace_existing_artifacts: true

homebrew_casks:
  - binary: <ツールのバイナリ名>
    description: <ツールの説明>
    homepage: <ツールのURL>
    # プライベートリポジトリのCasksから取得するための設定
    custom_block: |
      module GitHubHelper
        require 'net/http'
        require 'uri'
        require 'json'
        def self.get_asset_api_url(tag, name)
          response = Net::HTTP.get(URI.parse("https://api.github.com/repos/<ユーザ名>/<リポジトリ名>/releases/tags/#{tag}"),
            {"Accept" => "application/vnd.github.v3+json",
            "Authorization" => "Bearer #{token}",
            "X-GitHub-Api-Version" => "2022-11-28"})
          release_info = JSON.parse(response)
          release_info["assets"].find { |asset| asset["name"] == name }["url"]
        end
        def self.token
          require "utils/github"
          github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"]
          unless github_token
            github_token = GitHub::API.credentials
            raise "Failed to retrieve token" if github_token.nil? || github_token.empty?
          end
          github_token
        end
      end
    url:
      template: "#{GitHubHelper.get_asset_api_url('{{.Tag}}', '{{.ArtifactName}}')}"
      headers:
        - "Accept: application/octet-stream"
        - "Authorization: Bearer #{GitHubHelper.token}"
        - "X-GitHub-Api-Version: 2022-11-28"
    repository:
      owner: <Homebrew Cask用リポジトリのユーザ名>
      name: <Homebrew Cask用リポジトリ名>
      token: "{{ .Env.HOMEBREW_GITHUB_TOKEN }}"
    hooks:
      post:
        install: |
          puts "Removing quarantine attribute from the binary to avoid macOS Gatekeeper issues"
          system_command "/usr/bin/xattr", args: ["-d", "com.apple.quarantine", "#{ENV["HOMEBREW_PREFIX"]}/bin/<バイナリ名>"]

設定を変更したら、設定ファイルの内容をチェックする機能がありますので、必ず実行しエラーが無いことを確認しましょう。

$ goreleaser check
  • checking                                  path=.goreleaser.yaml
  • 1 configuration file(s) validated
  • thanks for using GoReleaser!

1 configuration file(s) validated と表示されていれば設定に問題が無いことを示しています。左記のように表示されず、エラーが出ている場合は設定を見直しましょう。

$ goreleaser check
  ⨯ command failed
    error=
    │ yaml: unmarshal errors:
    │   line 21: field flagss not found in type config.Build

続いて、上記の設定のうち重要な部分について解説します。

GitHub API経由でプライベートリポジトリ上のバイナリファイルを取得

GoReleaserのドキュメントに、プライベートのGitHubリポジトリからバイナリファイルを取得するための設定例が載っています。

https://goreleaser.com/customization/homebrew_casks/#private-github-repositories

ただし、上記の設定そのままでは、私の環境で brew install を実行するとエラーが発生し、インストールできませんでした。

そのため、custom_block の部分でAPIを実行する部分について、Ruby標準の net/http を使い、以下のように実装しました。

    custom_block: |
      module GitHubHelper
        def self.get_asset_api_url(tag, name)
          require 'net/http'
          require 'uri'
          require 'json'
          response = Net::HTTP.get(URI.parse("https://api.github.com/repos/tk-hase/rt-nippo/releases/tags/#{tag}"),
            {"Accept" => "application/vnd.github.v3+json",
            "Authorization" => "Bearer #{token}",
            "X-GitHub-Api-Version" => "2022-11-28"})
          release_info = JSON.parse(response)
          release_info["assets"].find { |asset| asset["name"] == name }["url"]
        end
        def self.token
          require "utils/github"
          github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"]
          unless github_token
            github_token = GitHub::API.credentials
            raise "Failed to retrieve token" if github_token.nil? || github_token.empty?
          end
          github_token
        end
      end

GitHub.get_asset_api_url() では、タグ名と対象の成果物の名前(バイナリが格納された .zip.tar.gz ファイルの名称)を引数から取得し、それをもとにReleaseページのAssetsにアップロードされているファイルのダウンロード用URLを取得します。

ダウンロード用URLの取得には、以下のGitHub APIの「Get a release by tag name」を利用します。

https://docs.github.com/ja/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name

なお、プライベートリポジトリとなっているので、APIを実行する際 Authorization ヘッダーにPersonal access tokenを指定しています。
その際、Personal access tokenを直接設定ファイル上に書くのはセキュリティ上良くないので、環境変数経由で設定します。ローカルでGoReleaserを実行する場合は、実行前に環境変数として export HOMEBREW_GITHUB_API_TOKEN="<Personal access token>" と設定し、GitHub Actions上でGoReleaserを実行する場合は、リポジトリの Secret にトークンを設定しておきます。

macOSのGatekeeper機能の回避

ビルドしたバイナリをそのままアップロードし、Homebrew経由でインストールすると、macOSではツール実行時にGatekeeper機能により実行を阻止されてしまいます。

ターミナル上でも以下のように表示され、プロセスが強制的に停止させられていることが分かります。

$ rt-nippo
[1]    45635 killed     rt-nippo

本来であれば、Apple Developer Programを契約したアカウントを使って必要な証明書等を発行し、署名と公証を実施してからアーカイブしてアップロードすべきです。しかし、今回は自身で使うためだけのツールであるため、Gatekeeperによる開発者署名の検証を回避するよう、以下のコマンドをツールのインストール後に実行するように仕込みました。

    hooks:
      post:
        install: |
          if OS.mac?
            puts "Removing quarantine attribute from the binary to avoid macOS Gatekeeper issues"
            system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{ENV["HOMEBREW_PREFIX"]}/bin/<バイナリ名>"]
          end

xattr コマンドを使い、指定するバイナリから com.apple.quarantine 属性を削除することで、Gatekeeper機能により阻害されることなくツールを実行することができます。

署名と公証については以下をご覧ください。

https://goreleaser.com/customization/notarize/

なお、CLIツールの場合、GoReleaserの機能を使って公証はできないようですので、手動での対応が別途必要そうです。

ChatGPTに聞いてみたら、以下のようにするのが良いと回答を受けました。(今回は対応していないため、回答の一部を掲載するのみとします)

✅ CLIツールの署名・公証には「外部ステップ」が必要

CLIバイナリのGatekeeper対応には、以下のようにGoReleaserの処理後に手動 or スクリプトで公証を行うのが実際的な運用になります:

# 1. codesign(GoReleaser内でもOK)
codesign --timestamp --sign "Developer ID Application: ..." ./yourcli

# 2. ZIP化(公証対象)
ditto -c -k --keepParent ./yourcli ./>yourcli.zip

# 3. notarytoolで公証
xcrun notarytool submit yourcli.zip --apple-id ... --wait

# 4. ステープル
xcrun stapler staple ./yourcli

このプロセスは GitHub Actions などのCIに組み込むことも可能です。

GitHub Actionsの設定

設定が完了したら、GoReleaserをGitHub Actionsから利用できるようにし、ビルド〜Homebrew Cask用リポジトリへ設定を反映するまでの作業を自動化しましょう。

公式ドキュメントの設定例を参考に、以下のファイルを追加します。

https://goreleaser.com/ci/actions/

.github/workflows/release.yml
name: goreleaser

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: stable
      # More assembly might be required: Docker logins, GPG, etc.
      # It all depends on your needs.
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          # either 'goreleaser' (default) or 'goreleaser-pro'
          distribution: goreleaser
          # 'latest', 'nightly', or a semver
          version: "~> v2"
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          HOMEBREW_GITHUB_TOKEN: ${{  secrets.HOMEBREW_GITHUB_TOKEN }}

Personal Access Tokenの発行

GitHub ActionsのWorkflowで実行する環境から、プライベート設定されているHomebrew Cask用リポジトリへCaskの設定を書き込むため、read/writeアクセス権を付与したPersonal Access Tokenを発行します。(名前は HOMEBREW_GITHUB_TOKEN

自身のアカウントの設定より、「Developer settings > Personal access tokens > Fine-grained tokens」を開き、右上の「Generate new token」ボタンをクリックします。

「Repository access」では、「Only select repositories」を選択し、Homebrew Cask用リポジトリを選択するようにしてください。

また、「Permissions」では、「Repository permissions > Contents」を「Read and write」に設定します。

あとは、画面最下部の「Generate token」ボタンをクリックし、表示されたトークンをコピーし忘れないようにしましょう。(一度他の画面に遷移するとトークンの文字列をコピーしたり見ることはできません)

Secretへの設定

次に、GitHub Actionsを実行するツール用リポジトリの設定に上記で発行した Personal access token をリポジトリのSecretに設定します。

ツール用リポジトリの Settings より、「Secrets and variables > Actions」を選択し、Actions secrets and variablesのページを開きます。
「Repository secrets」の右にある「New repository secret」ボタンをタップし、スクリーンショットの通りの名前と先ほどコピーしたPersonal access tokenの値を入れ、「Add secret」ボタンをクリックします。

これでGitHub Actionsの設定は完了です。

ローカルPCでプライベートリポジトリに対しHomebrewを実行するためには

開発側の設定は以上となり、ここからは利用する側の設定となります。

通常のHomebrew Caskを利用してツールをインストールする際は、Publicとなっているリポジトリであることがほとんどであるため不要な作業となりますが、今回はPrivate設定のリポジトリからインストールを行うため、ひと工夫が必要となります。

Homebrew Cask用リポジトリを登録する

HomebrewではGitを使ってCaskの設定を取得するようなので、Git経由でアクセスするためのSSHキーを予め準備しましょう。(おそらくGitHubの公式ドキュメントに記載があるかと思います)

SSH公開鍵をGitHubの設定へ登録し、手元にSSH秘密鍵が準備できたら、以下のコマンドを実行しSSH経由でリポジトリを取得する際の秘密鍵をSSHエージェントに登録しておきます。

$ ssh-add <SSHキー>

最後に brew tap コマンドを使ってHomebrew Cask用リポジトリをFomulaeとして管理されるようリストに追加します。

$ brew tap <ユーザ名>/<'homebrew-'を抜いたリポジトリ名> git@github.com:<ユーザ名>/<リポジトリ名>.git

通常通りに brew tap xxxx/yyyy を実行するとHTTP経由でリポジトリをCloneしてしまうようで、その場合ユーザ名とパスワードを求められます。すると、正しくユーザ名/パスワードを入力してもGitHubから「パスワード認証のサポートはすでに終了しました」というようなメッセージが表示され、エラーとなってしまいます。

$ brew tap <ユーザ名>/<'homebrew-'を抜いたリポジトリ名>
==> Tapping <ユーザ名>/<'homebrew-'を抜いたリポジトリ名>
Cloning into '/opt/homebrew/Library/Taps/<ユーザ名>/<リポジトリ名>'...
Username for 'https://github.com': xxxxxx
Password for 'https://<ユーザ名>@github.com': 
remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/<ユーザ名>/<リポジトリ名>/'
Error: Failure while executing; `git clone https://github.com/<ユーザ名>/<リポジトリ名> /opt/homebrew/Library/Taps/<ユーザ名>/<リポジトリ名> --origin=origin --template= --config core.fsmonitor=false` exited with 128.

この場合、brew tap コマンドの最後にSSHのリポジトリURLを指定することで、SSH経由でリポジトリをCloneするようになり、正常にtapが行われるようになります。

brew tap <ユーザ名>/<'homebrew-'を抜いたリポジトリ名> git@github.com:<ユーザ名>/<リポジトリ名>.git
==> Tapping <ユーザ名>/<'homebrew-'を抜いたリポジトリ名>
Cloning into '/opt/homebrew/Library/Taps/<ユーザ名>/<リポジトリ名>'...
remote: Enumerating objects: 47, done.
remote: Counting objects: 100% (47/47), done.
remote: Compressing objects: 100% (37/37), done.
remote: Total 47 (delta 10), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (47/47), 10.79 KiB | 1.54 MiB/s, done.
Resolving deltas: 100% (10/10), done.
Tapped 1 cask (14 files, 20KB).

brew コマンド実行時にツール用リポジトリにアクセスするための設定

ようやく、brew install を使って自身が開発したツールをインストールするわけですが、ここでも問題が発生します。

開発したツールが格納されているリポジトリがプライベート設定となっているため、通常通りに brew install <ツール名> と実行しても権限がなくエラーとなってしまいます。

そのため、Personal access tokenを発行し、そのトークンを使ってプライベートリポジトリからダウンロードするようにします。

GoReleaserによってすでにプライベートリポジトリからダウンロードURLを取得し、そこからバイナリファイルを取得する処理がCaskの設定に記載されていますので、自身のアカウント設定からPersonal access tokenを発行し、そのトークンを .bashrc.zshrc などに記載しておきます。

export HOMEBREW_GITHUB_API_TOKEN="github_pat_xxxxxxxxxx"

ツールのインストール

これで全ての設定が完了しました。
brew install を実行してツールをインストールしましょう!

$ brew install <ツール名>
==> Downloading https://formulae.brew.sh/api/formula.jws.json
==> Downloading https://formulae.brew.sh/api/cask.jws.json
==> Downloading https://api.github.com/repos/<ユーザ名>/<ツールリポジトリ名>/releases/assets/273359974
Already downloaded: /Users/<ユーザ名>/Library/Caches/Homebrew/downloads/151c5abdc3e2fd0db899d62224e82b45ff934c00aa992dcb16ad1cf826eed5d8--<ツール名>_macOS.zip
==> Installing Cask <ツール名>
==> Linking Binary '<ツール名>' to '/opt/homebrew/bin/<ツール名>'
Removing quarantine attribute from the binary to avoid macOS Gatekeeper issues
🍺  <ツール名> was successfully installed!

※すでに過去にダウンロードしたことがあるため、上記ログでは Already downloaded となっています

GJ!👍️

晴れて、全てのリポジトリがプライベート設定のまま、 brew install を使って自身でのみ使用するツールをインストールすることができました。
これ以降はHomebrewがバージョン管理をしてくれますので、新バージョンのツールをリリースしたら、他のツールと同様に brew update && brew upgrade <ツール名> を実行すれば新しいバージョンへアップデートすることができます。

参考

https://zenn.dev/kou_pg_0131/articles/goreleaser-usage

https://www.estie.jp/blog/entry/2024/08/06/100304

https://github.com/goreleaser/goreleaser/pull/5944

Discussion