Building a Private Homebrew Tap with GitLab and Nexus: A Practical, End-to-End Guide

Introduction

Distributing internal CLI tools in a controlled, scalable, and reproducible way is a recurring challenge in enterprise environments. While package managers like APT or YUM provide strong distribution mechanisms, they come with significant overhead and infrastructure complexity.

On macOS, Homebrew offers a much lighter alternative. However, that simplicity comes with a trade-off: distribution, hosting, and access control are entirely your responsibility.

This article walks through a real-world implementation of:

  • A private source repository hosted on GitLab
  • Binary artifact distribution via Sonatype Nexus Repository
  • A public Homebrew tap repository
  • Multi-architecture builds (amd64 and arm64)
  • End-to-end installation using brew install

We will not only explain how to do it, but why each decision matters.


1. Architecture Overview

The key architectural insight is separation of concerns:

GitLab (private) → source code
↓
CI / local build → binaries
↓
Nexus (public read) → artifact distribution
↓
Homebrew tap (public) → installation metadata

Why this separation?

Homebrew does not authenticate when downloading binaries. Therefore:

The binary URL must be publicly accessible via curl without authentication.

This immediately invalidates:

  • GitLab uploads (require session or auth)
  • Private endpoints
  • Token-based downloads

Instead, we use Nexus as a public artifact distribution layer, while keeping source code private.


2. Preparing the Go Project

This step must be executed in the source code repository, not in the Homebrew repository.

In this setup, the source repository is:

git@gitlab.devops-db.internal:infrastructure/resources/pssql.git

This repository contains:

  • the Go source code
  • the pssql.json configuration template
  • the build logic

Why this separation matters

The Homebrew tap is not responsible for building software. It only defines: what to download + how to install it

All build artifacts must originate from the source repository.


Module Initialization

go mod init gitlab.devops-db.internal/infrastructure/resources/pssql
go mod tidy

This ensures:

  • consistent dependency resolution
  • reproducible builds
  • alignment with internal repository structure

Building Multi-Architecture Binaries

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o pssql_darwin_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o pssql_darwin_arm64

These binaries are produced from the source repository, and not from the Homebrew tap.


3. Packaging Strategy (Critical Step)

Homebrew expects a very specific archive structure.

Correct Layout

pssql
pssql.json

All packaging steps must also happen here:

mkdir -p dist/amd64
mv pssql_darwin_amd64 dist/amd64/pssql
cp pssql.json dist/amd64/

mkdir -p dist/arm64
mv pssql_darwin_arm64 dist/arm64/pssql
cp pssql.json dist/arm64/


tar -czf pssql_1.1.3_darwin_amd64.tar.gz -C dist/amd64 .
tar -czf pssql_1.1.3_darwin_arm64.tar.gz -C dist/arm64 .

🔥 Important: The Homebrew repository never contains compiled binaries or .tar.gz files.

Calculating SHA256 Checksums

After creating the archives, generate their SHA256 hashes:

shasum -a 256 pssql_1.1.3_darwin_amd64.tar.gz
shasum -a 256 pssql_1.1.3_darwin_arm64.tar.gz

Example output:

656c8a281c46ab6c626c91f409485877cef9b9b180ce4a5be1a3730ab806c2d6  pssql_1.1.3_darwin_amd64.tar.gz

08956802444143beca75a596e0ef9a9c5c44a0b77199df4da40c82831e50060d  pssql_1.1.3_darwin_arm64.tar.gz

Why This Is Required

Homebrew uses SHA256 hashes to:

  • verify download integrity
  • prevent tampering
  • ensure reproducibility

Important Note

These SHA256 values will be required later when defining the Homebrew formula.

Without correct hashes, the installation will fail during the verification step.


Common Pitfall

Any change to the archive (even a single byte) will:

invalidate the SHA256 hash

This means:

  • if you rebuild the binary
  • or recreate the .tar.gz

👉 you must recalculate the hashes.


4. Versioning Strategy

git tag v1.1.3
git push origin v1.1.3

Why tagging still matters (even without releases)

Even if you don’t use GitLab Releases:

  • tags provide traceability
  • link binary → source commit
  • enable reproducible builds

5. Using GitLab Releases for Homebrew (Behavior and Limitations)

At first glance, GitLab Releases appear to be a natural solution for distributing binaries, similar to how it is commonly done with GitHub.

However, unlike GitHub, GitLab behavior depends heavily on instance configuration.


Expected Behavior (When It Works)

If the repository is public and the instance allows anonymous access, release assets can be downloaded directly:

curl -I https://gitlab.example.com/-/project/<id>/uploads/<hash>/file.tar.gz

Result:

HTTP/2 200

In this scenario:

✔️ The URL is publicly accessible
✔️ Homebrew can download the binary
✔️ GitLab can be used as a distribution source


⚠️ Why It Sometimes Fails

In many self-hosted GitLab environments, additional security settings are enabled, such as:

enforce_auth_checks_on_uploads = true

Or global authentication requirements.

In these cases:

  • upload URLs require authentication
  • anonymous requests are redirected or rejected
  • the same URL may work in a browser but fail in curl

🔍 Observable Behavior

ScenarioResult
Browser (logged in)✔️ Works
curl (anonymous)❌ 302 / 404
Homebrew❌ Fails

💡 Key Insight

GitLab can serve as a valid Homebrew distribution backend — but only if anonymous HTTP access to artifacts is allowed.


🧪 Validation Rule

Before using any URL in a Homebrew formula:

curl -L <url>

If this command fails, Homebrew will also fail.


⚖️ GitHub vs GitLab (Clarified)

FeatureGitHubGitLab
Release assets public by default✔️⚠️ depends
Anonymous download guaranteed✔️❌ not always
Suitable for Homebrew out-of-the-box✔️⚠️ environment-dependent

🧾 Final Takeaway

GitLab Releases are not inherently incompatible with Homebrew —
but their usability depends entirely on instance-level access policies.


6. Uploading to Nexus

As discussed in the previous section, using GitLab as a binary distribution backend is possible, but not guaranteed.

Its behavior depends on:

  • repository visibility
  • instance-level security policies
  • anonymous access configuration

In controlled or enterprise environments, these conditions are often restricted.


Why Nexus Was Chosen

To avoid relying on environment-specific behavior, we intentionally chose Sonatype Nexus Repository as the distribution layer.

This decision was based on the following requirements:

  • predictable, anonymous HTTP access
  • independence from GitLab configuration
  • clear separation between source code and artifacts
  • compatibility with Homebrew’s download model

The repository is now located at: https://nexus.devops-db.internal/repository/homebrew-devopsdb/


Key Requirement

The artifact endpoint must be accessible via curl without authentication.

Nexus satisfies this requirement when configured with:

anonymous read access enabled

This makes it a reliable backend for Homebrew formulas.

curl -u usr_jenkins_nexus:1234qwer  --upload-file pssql_1.1.3_darwin_amd64.tar.gz \
  "https://nexus.devops-db.internal/repository/homebrew-devopsdb/pssql/1.1.3/pssql_1.1.3_darwin_amd64.tar.gz"


curl -u usr_jenkins_nexus:1234qwer --upload-file pssql_1.1.3_darwin_arm64.tar.gz \
  "https://nexus.devops-db.internal/repository/homebrew-devopsdb/pssql/1.1.3/pssql_1.1.3_darwin_arm64.tar.gz"

Repository structure

pssql/
  1.1.3/
    pssql_1.1.3_darwin_amd64.tar.gz
    pssql_1.1.3_darwin_arm64.tar.gz

Critical requirement

Nexus must allow:

anonymous read access

Otherwise:

  • Homebrew fails
  • same issue as GitLab

7. Homebrew Tap Repository

This step must be executed in a separate repository dedicated to Homebrew formulas.

In this setup, the tap repository is:

git@gitlab.devops-db.internal:resources/homebrew-devopsdb.git

Critical Requirement

The Homebrew tap repository must be publicly accessible.

Homebrew performs:

git clone <tap-repository>

with no authentication by default.

Therefore:

  • ❌ private GitLab repo → fails
  • ✔️ public repo → works

Repository Responsibility

This repository must contain only:

Formula/
  pssql.rb

It should not include:

  • source code
  • binaries
  • build artifacts

Formula Definition

Remember the sha256 calculated in step 3? It’s important here.

class Pssql < Formula
  desc "Command-line tool for PostgreSQL (pssql)"
  homepage "https://gitlab.devops-db.internal/infrastructure/resources/pssql"
  version "1.1.3"  if Hardware::CPU.intel?
    url "https://nexus.devops-db.internal/repository/homebrew-devopsdb/pssql/1.1.3/pssql_1.1.3_darwin_amd64.tar.gz"
    sha256 "656c8a281c46ab6c626c91f409485877cef9b9b180ce4a5be1a3730ab806c2d6"
  else
    url "https://nexus.devops-db.internal/repository/homebrew-devopsdb/pssql/1.1.3/pssql_1.1.3_darwin_arm64.tar.gz"
    sha256 "08956802444143beca75a596e0ef9a9c5c44a0b77199df4da40c82831e50060d"
  end  def install
    bin.install "pssql"
    etc.install "pssql.json" => "pssql.json.example"
  end  def caveats
    <<~EOS
      To get started, create your configuration directory and copy the example file:
          mkdir -p ~/.pssql
          cp #{etc}/pssql.json.example ~/.pssql/pssql.json
    EOS
  end  test do
    system "#{bin}/pssql", "--version"
  end
end

💡 Understanding caveats

The caveats block is used to display post-installation instructions to the user.

Unlike automated installation steps, caveats are:

  • not executed automatically
  • shown as informational output after brew install
  • intended to guide the user through manual setup

Why caveats are needed here

In this case, the tool requires a user-specific configuration file:

~/.pssql/pssql.json

However:

  • Homebrew installs files under system-controlled paths (e.g., /opt/homebrew/etc)
  • it does not modify user home directories

What the caveat does

It instructs the user to:

mkdir -p ~/.pssql
cp /opt/homebrew/etc/pssql.json.example ~/.pssql/pssql.json

This ensures:

  • separation between system-managed files and user configuration
  • safe upgrades (user config is not overwritten)
  • predictable behavior across installations

Design Insight

Homebrew formulas should not modify user environments implicitly.

Using caveats respects this principle while still providing a clear onboarding path.


8. Installation Flow

brew tap devopsdb/tools git@gitlab.devops-db.internal:resources/homebrew-devopsdb.git
brew update
brew install devopsdb/tools/pssql

Result

pssql --version
INFO  pssql version 1.1.3

9. Final Architecture (Production-Ready)

GitLab (private source)
↓
Build (Go)
↓
Nexus (public artifacts)
↓
Homebrew Tap (public metadata)
↓
Developer machines (brew install)

Leave a Reply

Your email address will not be published. Required fields are marked *