Homebrew can be a powerful way to manage dependencies in your GitHub Actions CI, but it’s not as straightforward as just running brew install.

Let’s see Homebrew in action within a GitHub Actions workflow. Imagine you need jq to process JSON output from a command, and tree for visualizing a directory structure.

name: Homebrew Dependencies

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Homebrew
        id: setup-homebrew
        uses: Homebrew/actions/setup-homebrew@master
        with:

          cache: ${{ runner.os }}-{{ arch }} # Cache Homebrew packages


      - name: Install tools with Homebrew
        run: |
          brew install jq tree
          # Verify installation
          jq --version
          tree --version

This workflow uses the Homebrew/actions/setup-homebrew action. This action handles the boilerplate of downloading and setting up Homebrew for your runner. The cache option is crucial; it tells the action to use GitHub Actions’ caching mechanism to store downloaded Homebrew formulae and their dependencies. This dramatically speeds up subsequent runs because Homebrew won’t need to re-download everything each time.

The core problem Homebrew solves in CI is dependency management. Instead of manually downloading binaries, compiling from source, or relying on potentially outdated system packages, you get a consistent, versioned, and easily reproducible way to install the command-line tools your CI needs. This is especially useful for tools that aren’t typically found in standard Linux distributions or when you need a specific version.

Internally, the setup-homebrew action does a few key things:

  1. Downloads Homebrew: It fetches the latest Homebrew installer script and runs it.
  2. Configures Environment: It sets up the necessary PATH environment variables so that your installed Homebrew packages are discoverable.
  3. Caches Packages: It integrates with GitHub Actions caching, saving downloaded formulae to a cache that’s restored on subsequent runs. This means if you brew install jq today, the next time the action runs, it checks the cache first. If jq is found, it’s restored instantly without a download or installation.

The brew install jq tree command then uses Homebrew’s standard installation mechanism. Homebrew downloads the formula for jq and tree, resolves any dependencies they might have (like zlib or openssl), compiles them if necessary (though most common tools are pre-compiled binaries), and installs them into Homebrew’s prefix (typically /home/runner/homebrew on GitHub Actions runners). The setup-homebrew action ensures this prefix is added to your PATH, making jq and tree available as if they were installed system-wide.

The most surprising thing about using Homebrew in CI is how effectively it bridges the gap between development environments and CI environments, often eliminating "it works on my machine" issues related to tool availability and versions. It allows you to declare your CI’s toolchain declaratively in your workflow file, just like you declare your application’s dependencies.

You might notice that sometimes your Homebrew cache gets invalidated, even if you haven’t changed your brew install commands. This often happens when the underlying Homebrew installation on the runner changes, or when Homebrew itself updates its core components in a way that breaks compatibility with older cached formulae. When this occurs, you’ll see Homebrew attempting to re-download packages it previously had cached, leading to slower builds.

The next challenge you’ll likely face is managing different versions of tools, or installing tools that require specific configurations or taps.

Want structured learning?

Take the full Homebrew course →