Distribute your Go CLI tools with GoReleaser and Homebrew

40percentironman

Tim Bright

Posted on September 1, 2023

Distribute your Go CLI tools with GoReleaser and Homebrew

Cobra is awesome.

At Duro, my team and I made a sweet CLI tool that allows us to create microservices for our platform quickly and easily. The tool sets up a standard service configuration for TypeScript, linting, integration with our shared library, and specific Github Actions for pushing the service to the cloud. It's been really nice to have.

We're constantly tweaking things with it, fixing little things and changing our configurations. We decided to use GoReleaser to help us version the tool and create the binaries and publish them to Github. It's been awesome.

Until we had to distribute it.

Revisions started increasing and it was a hassle trying to make sure that you have the latest version of the tool. In the spirit of "get things done", we just had everyone download it and delete it when they were done. Not a great solution, but hey, we had more things to worry about.

Lately, I decided to take it upon myself to show this tool some love (and curry favor with my fellow devs) to try to manage versions of this tool via Homebrew. I didn't know the internals of how Homebrew worked, so I decided to dig in.

Homebrew Basics

Homebrew logo image

Homebrew is a package manager that allows users to install packages through a simple interface. If you need curl and you don't have it, you can simply run brew install curl and Homebrew will fetch it. You can even specify specific versions of a package if you need.

How does Homebrew know where to get things from?

Homebrew has a list of core packages you can browse here and those are available when you type in brew install. Each of these packages has a formula, which is just a Ruby file that shows what the latest SHA is of a package and allows Homebrew to select the right binary based on your operating system. Here's an example of a formula:

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
Enter fullscreen mode Exit fullscreen mode

The repository that this formula is stored in is called a tap. In fact, the list of core package are stored in the core tap. This is not only tap you can have; you can create your own tap.

How do I create a tap and what happens when I do?

There's standard documentation on how to create a tap.

The short version is that when you type in brew tap <tap_name> <URL>, it will go to that URL and clone the repository there and save it in $(brew --repository)/Library/Taps. Then, when you type in brew install <package>, it will search the core tap first and then any additional taps you've created. It will then look for the formula Ruby file and grab the latest package (or the version you specify) from the specified location in the formula and put it in Homebrew's path so you can access it.

How do I create a formula?

You can create one by hand, but GoReleaser can do it for you.

Here's the basic config:

brews:
  -
    # Name of the recipe
    #
    # Default: ProjectName
    # Templates: allowed
    name: some_app

    # Folder inside the repository to put the formula.
    folder: Formula

    # Your app's description.
    #
    # Templates: allowed
    description: "Put in description here."

    # Repository to push the generated files to.
    repository:
      owner: example
      name: some_app
      branch: brew-releases/{{ .Version }}
      token: "{{ .Env.GITHUB_TOKEN }}"
      pull_request:
        enabled: true
        base:
          owner: example
          name: some_app
          branch: master

    # NOTE: make sure the url_template, the token and given repo (github or
    # gitlab) owner and name are from the same kind.
    # We will probably unify this in the next major version like it is
    # done with scoop.

    # URL which is determined by the given Token (github, gitlab or gitea).
    #
    # Default depends on the client.
    # Templates: allowed
    url_template: "https://github.com/example/some_app/releases/download/{{ .Tag }}/{{ .ArtifactName }}"

    # Allows you to set a custom download strategy. Note that you'll need
    # to implement the strategy and add it to your tap repository.
    # Example: https://docs.brew.sh/Formula-Cookbook#specifying-the-download-strategy-explicitly
    download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy

    custom_require: './custom_release_strategy'

    # Git author used to commit to the repository.
    commit_author:
      name: goreleaserbot
      email: goreleaserbot@example.com

    # The project name and current git tag are used in the format string.
    #
    # Templates: allowed
    commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
Enter fullscreen mode Exit fullscreen mode

Here, GoReleaser will generate the Ruby formula file for you and try to commit it into the Formula folder in this repository. In my case, we made our main branch protected so we had to specify the section that auto-creates a PR with the new formula.

We also have a Github Action that uses GoReleaser's custom action to trigger a release whenever a new tag is created in our repo. Now all one of us has to do is create a release with a new tag and the GitHub Action will prepare a release with the binaries and then auto-create the PR with the updated formula. Once it's merged in, Homebrew should take care of updating the revisions.

Then it's just the brew tap and brew install combo FTW!(You might need to have a GitHub personal access token in your HOMEBREW_GITHUB_API_TOKEN env variable).

That doesn't sound so bad! Why were you complaining about it being a pain?

Ah, that's right. The gotcha.

Homebrew uses several different methods to download/clone repos, such as CurlDownloadStrategy (which does exactly what it says).

They used to support one called GitHubPrivateRepositoryReleaseDownloadStrategy. It's exactly what the name implies: it downloads releases from GitHub private repositories. At the time I was doing my research into doing this myself, there was a good amount of documentation floating around that says to use that strategy for fun and profit.

Problem is, Homebrew no longer supports that strategy. Then GoReleaser stopped supporting it.

Morpheus what if I told you

Screwed? Not really, thanks to some help from the community and open source software!

This comment in a GoReleaser issue showed me I can copy the old code from older versions of Homebrew and include them into my formula with GoReleaser.

I think in the end we used the recommendation made here with the code contained in this Gist link. We put that code into the same Formula folder in the repo and named it custom_release_strategy.rb. That file is pointed to in the custom_require field in the brews section and automatically includes it in the formula file so Homebrew knows how to download the packages.

Hope that helps anyone who wants to build their own CLI tool and ship it via Homebrew!

Other Helpful Links:

https://hackernoon.com/building-homebrew-taps-for-private-github-repos
https://dev.to/jhot/homebrew-and-private-github-repositories-1dfh

💖 💪 🙅 🚩
40percentironman
Tim Bright

Posted on September 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related