I started a new job in July 2022. While the developer experience is great, one of the things that I missed at my new job was a CLI (like I had at my old job). I thought of building an easily extensible CLI tool which I did with and cobra. To handle distribution to the engineers, we decided to use both and . to rule them all go go install homebrew Homebrew is a very popular package manager for macOS and . Users can easily install new packages with . Homebrew handles the downloading and the installation of the packages to the appropriate directories and then creates symlinks to (on Intel machines). Linux users brew install [PACKAGE] /usr/local To create a Homebrew package, we create Ruby classes called . A Formula looks like this: Formulas class Wget < Formula homepage "https://www.gnu.org/software/wget/" url "https://ftp.gnu.org/gnu/wget/wget-1.15.tar.gz" sha256 "52126be8cf1bddd7536886e74c053ad7d0ed2aa89b4b630f76785bac21695fcd" def install system "./configure", "--prefix=#{prefix}" system "make", "install" end end While adding an open-source package is fairly straightforward, doing the same with a private package involves a bit of hacking. First Steps The first thing to do is create a Github release, if you use , I highly recommend using I wrote a tutorial on how to do that . It truly makes releases in easy. If you're using another language, you should check out how to manage releases in GitHub . go . goreleaser here go here Create a Homebrew Tap The next thing to do is to create a . A Homebrew tap is a GitHub repository that houses our Formulas. To add non-Homebrew core taps, we need to add the taps locally to homebrew with . Homebrew tap brew tap username/package For us to use the single argument form of , we need to follow the and name our GitHub repo since the GitHub repo must start with . brew tap naming convention homebrew-tap homebrew- Create a Formula If you have a release already in Github, we can go ahead with creating the Formula. In our repo, we'll create a folder called . In the directory, we'll create a file called . homebrew-tap Formula Formula mytool.rb Now, in our , we cannot use the standard way of downloading the packages since our package is hosted in a private repo. To do this we simply add a new download strategy like such: Formula require "formula" require_relative "../custom_download_strategy.rb" class Wget < Formula homepage "https://www.gnu.org/software/wget/" url "https://ftp.gnu.org/gnu/wget/wget-1.15.tar.gz", :using => GitHubPrivateRepositoryReleaseDownloadStrategy sha256 "52126be8cf1bddd7536886e74c053ad7d0ed2aa89b4b630f76785bac21695fcd" def install system "./configure", "--prefix=#{prefix}" system "make", "install" end test do system "#{bin}/wget --help" end end Note the addition of on the third line of the class. , :using => GitHubPrivateRepositoryReleaseDownloadStrategy In the root folder of , we'll create a new file called . Here, we'll add some code to support our download strategy. homebrew-tap custom_download_strategy.rb require "download_strategy" class GitHubPrivateRepositoryDownloadStrategy < CurlDownloadStrategy require "utils/formatter" require "utils/github" def initialize(url, name, version, **meta) super parse_url_pattern set_github_token end def parse_url_pattern unless match = url.match(%r{https://github.com/([^/]+)/([^/]+)/(\S+)}) raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Repository." end _, @owner, @repo, @filepath = *match end def download_url "https://#{@github_token}@github.com/#{@owner}/#{@repo}/#{@filepath}" end private def _fetch(url:, resolved_url:, timeout:) curl_download download_url, to: temporary_path, timeout: timeout end def set_github_token @github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"] unless @github_token raise CurlDownloadStrategyError, "Environmental variable HOMEBREW_GITHUB_API_TOKEN is required." end validate_github_repository_access! end def validate_github_repository_access! # Test access to the repository GitHub.repository(@owner, @repo) rescue GitHub::HTTPNotFoundError # We only handle HTTPNotFoundError here, # becase AuthenticationFailedError is handled within util/github. message = <<~EOS HOMEBREW_GITHUB_API_TOKEN can not access the repository: #{@owner}/#{@repo} This token may not have permission to access the repository or the url of formula may be incorrect. EOS raise CurlDownloadStrategyError, message end end # GitHubPrivateRepositoryReleaseDownloadStrategy downloads tarballs from GitHub # Release assets. To use it, add # `:using => GitHubPrivateRepositoryReleaseDownloadStrategy` to the URL section of # your formula. This download strategy uses GitHub access tokens (in the # environment variables HOMEBREW_GITHUB_API_TOKEN) to sign the request. class GitHubPrivateRepositoryReleaseDownloadStrategy < GitHubPrivateRepositoryDownloadStrategy def initialize(url, name, version, **meta) super end def parse_url_pattern url_pattern = %r{https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\S+)} unless @url =~ url_pattern raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Release." end _, @owner, @repo, @tag, @filename = *@url.match(url_pattern) end def download_url "https://#{@github_token}@api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}" end private def _fetch(url:, resolved_url:, timeout:) # HTTP request header `Accept: application/octet-stream` is required. # Without this, the GitHub API will respond with metadata, not binary. curl_download download_url, "--header", "Accept: application/octet-stream", to: temporary_path, timeout: timeout end def asset_id @asset_id ||= resolve_asset_id end def resolve_asset_id release_metadata = fetch_release_metadata assets = release_metadata["assets"].select { |a| a["name"] == @filename } raise CurlDownloadStrategyError, "Asset file not found." if assets.empty? assets.first["id"] end def fetch_release_metadata GitHub.get_release(@owner, @repo, @tag) end end We should have the following folder structure: ├── homebrew-tap │ ├── Formula │ │ ├── mytool.rb └── custom_download_strategy.rb Usage We're almost done. To install our tool via Homebrew, we need to export a Github token that provides access to our private repos. A new token can be created . The token needs to have permissions. We need to export the token as . To export the token, we run: here repo HOMEBREW_GITHUB_API_TOKEN export HOMEBREW_GITHUB_API_TOKEN=<GITHUB_TOKEN> # if you're using fish shell like me, run `set -x HOMEBREW_GITHUB_API_TOKEN <GITHUB_TOKEN>` Then, we can run the command: brew install username/homebrew-tap/mytool This automatically adds the homebrew tap and installs our tool. Summary Create a GitHub release with the binaries. This can be done very easily with in the case of . goreleaser go Create a GitHub repo called . An example is . homebrew-tap here Create a folder. Formula In the root of the repo, create a file called . The contents are shown above custom_download_strategy.rb In the folder, create a ruby file with the name of your tool like Formula mytool.rb Create a GitHub token and give permissions to the token. repo Export the token with export HOMEBREW_GITHUB_API_TOKEN=<GITHUB_TOKEN> Install the tool with brew using . brew install username/homebrew-tap/mytool Also published here.