Using Homebrew to Distribute Early Access Binaries from Private Github Repositories
Building for macOS has been… interesting. After falling into the trap of Microsoft’s 10x price gouging for macOS runners on GitHub Actions and ultimately switching to using the Mac Mini under my television as a self-hosted runner, the next thing I wanted to do was distribute my build artifacts.
I’m trying a different approach to building a new project this time around. Maintaining a popular piece of software is very draining, and working on a much-requested port of a popular piece of software to another operating system was not something I wanted to do in public.
So I’m building in private until I’m ready for an initial public release, and as a bonus, I finally have something concrete to offer to people who sponsor me on GitHub (early access).
Homebrew is, for better or worse, the most popular package manager on macOS and it can only be avoided for so long.
After setting up nightly builds on GitHub Actions, I wanted to create a
Homebrew Tap and Formula for people who sponsor me on GitHub for early access -
being able to brew install is a much more pleasant onboarding experience for
a new sponsor than having to compile from source.
For public repositories, it’s possible to just use what the GitHub API refers
to as the browser_download_url in a Homebrew Formula, take
ripgrep
for example:
class Ripgrep < Formula
desc "Search tool like grep and The Silver Searcher"
homepage "https://github.com/BurntSushi/ripgrep"
url "https://github.com/BurntSushi/ripgrep/archive/refs/tags/15.1.0.tar.gz" # so easy!
sha256 "046fa01a216793b8bd2750f9d68d4ad43986eb9c0d6122600f993906012972e8"
license "Unlicense"
head "https://github.com/BurntSushi/ripgrep.git", branch: "master"
#...
end
However, there is no combination of headers (that I could find) that can be
used with a browser_download_url for a release artifact from a private
repository which will allow it to be downloaded by Homebrew.
Instead we have to make a call to https://api.github.com, which will accept
an Authorization header.
The specific endpoint we want is
https://api.github.com/repos/$ORG/$REPO/releases/assets/$ASSET_ID.
While $ORG and $REPO are static, $ASSET_ID will change every time a new
nightly release is created and a new artifact is attached to the release. But
we can deal with that later.
We can get something hardcoded working if we target a specific asset:
class KomorebiForMacNightly < Formula
desc "Tiling window manager for macOS (nightly build)"
homepage "https://github.com/KomoCorp/komorebi-for-mac"
url "https://api.github.com/repos/KomoCorp/komorebi-for-mac/releases/assets/308949887",
headers: [
"Accept: application/octet-stream",
"X-GitHub-Api-Version: 2022-11-28",
"Authorization: bearer #{ENV.fetch("HOMEBREW_GITHUB_API_TOKEN")}",
]
version "nightly"
sha256 "d91583e5479ed0a23bfb9c7253e8f1f32dcedc0e0d1b886ff54d43894f670724"
license "Komorebi License 2.0.0"
def install
bin.install Dir["*"]
end
def caveats
<<~EOS
This formula requires access to a private GitHub repository.
Make sure you have set your GitHub token:
export HOMEBREW_GITHUB_API_TOKEN=your_token_here
EOS
end
test do
system "#{bin}/komorebi", "--help"
end
end
The well-documented HOMEBREW_GITHUB_API_TOKEN
from the user’s environment is used in the Authorization header to allow
sponsors with access to the private repository to download the release
artifact, with a useful caveat message if the user doesn’t have one set.
The next problem to solve: the $ASSET_ID and sha256 hash need to be updated
whenever there is a new nightly release.
To handle this, I currently keep a template file in the project repo into which I inject updated values before committing it to my Homebrew Tap:
class KomorebiForMacNightly < Formula
desc "Tiling window manager for macOS (nightly build)"
homepage "https://github.com/KomoCorp/komorebi-for-mac"
url "https://api.github.com/repos/KomoCorp/komorebi-for-mac/releases/assets/REPLACE_ASSET_ID", # <-- Update this
headers: [
"Accept: application/octet-stream",
"X-GitHub-Api-Version: 2022-11-28",
"Authorization: bearer #{ENV.fetch("HOMEBREW_GITHUB_API_TOKEN")}",
]
version "nightly"
sha256 "REPLACE_SHA_256" # <-- Update this
license "Komorebi License 2.0.0"
def install
bin.install Dir["*"]
end
def caveats
<<~EOS
This formula requires access to a private GitHub repository.
Make sure you have set your GitHub token:
export HOMEBREW_GITHUB_API_TOKEN=your_token_here
EOS
end
test do
system "#{bin}/komorebi", "--help"
end
end
In the final step of my GitHub Actions workflow, I create a new commit on my Homebrew Tap using this file, and then push the changeset:
jobs:
build:
steps:
# previous build steps
- shell: bash
run: |
ASSET_ID=$(gh api repos/${{ github.repository }}/releases/tags/nightly \
--jq '.assets[] | select(.name=="komorebi-nightly-aarch64-apple-darwin.zip") | .id')
SHA256=$(cat checksums.txt | cut -d' ' -f1)
echo "ASSET_ID=$ASSET_ID" >> $GITHUB_ENV
echo "SHA256=$SHA256" >> $GITHUB_ENV
- shell: bash
run: |
sed "s/REPLACE_ASSET_ID/$ASSET_ID/g; s/REPLACE_SHA_256/$SHA256/g" formula.template.rb > komorebi-for-mac-nightly.rb
cat komorebi-for-mac-nightly.rb
git clone https://x-access-token:${{ secrets.GH_USER_API_TOKEN }}@github.com/LGUG2Z/homebrew-tap
cd homebrew-tap
cp ../komorebi-for-mac-nightly.rb ./Formula
git add .
git commit -m "Update komorebi-for-mac-nightly to $ASSET_ID ($SHA256)"
git push
In my opinion, there is a lot more friction in this process than there should be. The systems we use do not encourage the use case of independent developers trying to sustain themselves financially.
In any case, my GitHub Sponsors (thank you, all 52 of you!) who use Homebrew can now simplify their updates by updating through Homebrew instead of pulling the latest git changes and building from source.
brew tap lgug2z/tap
brew install lgug2z/tap/komorebi-for-mac-nightly
Who knows, maybe you’ll want to try developing something in private with early access for sponsors in the future - hopefully this post will help future you.
If you have any questions or comments you can reach out to me on Bluesky and Mastodon.
If you’re interested in what I read to come up with software like komorebi, you can subscribe to my Software Development RSS feed.
If you’d like to watch me writing code while explaining what I’m doing, you can also subscribe to my YouTube channel.
If you would like early access to komorebi for Mac, you can sponsor me on GitHub.