This article is still related with How to publish your Go binary as Homebrew Formula with GoReleaser Full source code of this example can be seen in https://github.com/franzramadhan/homebrew-go-example
Introduction
This article will cover how to publish Homebrew formula to Private Github repository easily using GoReleaser.
Background
Sometimes, we need to publish our artifact or Homebrew formula privately. So it is only accessible within our organization or our teammates. Therefore, we need to create one logic to Homebrew so it is able to handle this scenario.
Solution
To do that we need to create a script as a download strategy, then tells the Homebrew formula to use the strategy instead of using normal one.
Luckily, GoReleaser support this adjustment. We only need to set it in the .goreleaser.yml
.
Workflow
First we need to implement the download strategy. For this example, we will put it inside lib/
directory with private.rb
name.
require "download_strategy"
# GitHubPrivateRepositoryDownloadStrategy downloads contents from GitHub
# Private Repository. To use it, add
# `:using => :github_private_repo` 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. This
# strategy is suitable for corporate use just like S3DownloadStrategy, because
# it lets you use a private GitHub repository for internal distribution. It
# works with public one, but in that case simply use CurlDownloadStrategy.
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:)
curl_download download_url, to: temporary_path
end
def set_github_token
@github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"]
unless @github_token
raise CurlDownloadStrategyError, "Environment 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 => :github_private_release` 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:)
# 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
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
release_url = "https://api.github.com/repos/#{@owner}/#{@repo}/releases/tags/#{@tag}"
GitHub.open_api(release_url)
end
end
class DownloadStrategyDetector
class << self
module Compat
def detect_from_symbol(symbol)
case symbol
when :github_private_repo
GitHubPrivateRepositoryDownloadStrategy
when :github_private_release
GitHubPrivateRepositoryReleaseDownloadStrategy
else
super(symbol)
end
end
end
prepend Compat
end
end
Then we need to also add additional configuration in .goreleaser.yml so it will update the previous formula to use the download strategy we created previously. We need to add following lines in brew
section
download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy
custom_require: "lib/private"
Complete section should look like this:
# Check https://goreleaser.com/customization/homebrew/
brews:
- homepage: 'https://github.com/franzramadhan/homebrew-go-example'
description: 'Example binary distribution using homebrew.'
folder: Formula
download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy
custom_require: "lib/private"
commit_author:
name: franzramadhan
email: franzramadhan@gmail.com
tap:
owner: franzramadhan
name: homebrew-go-example
Once the new release is triggered. The homebrew formula should be updated to use the download strategy. See homebrew-go-example.rb:
# This file was generated by GoReleaser. DO NOT EDIT.
require_relative "lib/private"
class HomebrewGoExample < Formula
desc "Example binary distribution using homebrew."
homepage "https://github.com/franzramadhan/homebrew-go-example"
version "0.0.3"
bottle :unneeded
if OS.mac?
url "https://github.com/franzramadhan/homebrew-go-example/releases/download/v0.0.3/homebrew-go-example_0.0.3_darwin_x86_64.tar.gz", :using => GitHubPrivateRepositoryReleaseDownloadStrategy
sha256 "dbf982698730b6f8f2e34a4b2e6e9fe4033bb7d9b388a15a6d2d94f6261261e3"
elsif OS.linux?
if Hardware::CPU.intel?
url "https://github.com/franzramadhan/homebrew-go-example/releases/download/v0.0.3/homebrew-go-example_0.0.3_linux_x86_64.tar.gz", :using => GitHubPrivateRepositoryReleaseDownloadStrategy
sha256 "3f4b0784acfb83217f0335135f91975210c092b055108f9b2a08d1104932c070"
end
end
def install
bin.install "homebrew-go-example"
end
end
See require_relative "lib/private"
and GitHubPrivateRepositoryReleaseDownloadStrategy
line. It tells the Homebrew client to use the download strategy method defined in lib/private.rb
Testing
- Try to install the formula
brew install homebrew-go-example
Updating Homebrew...
==> Installing homebrew-go-example from franzramadhan/go-example
Error: Failed to download resource "homebrew-go-example"
Download failed: Environment variable HOMEBREW_GITHUB_API_TOKEN is required.
- Since the repository is private, we need to set the Github authentication. To authenticate, we need to generate Github Personal Access Token with
repo
scope and export it asHOMEBREW_GITHUB_API_TOKEN
environment variable. e.g
export HOMEBREW_GITHUB_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Try install again
brew install homebrew-go-example
Updating Homebrew...
==> Installing homebrew-go-example from franzramadhan/go-example
==> Downloading https://github.com/franzramadhan/homebrew-go-example/releases/download/v0.0.3/homebrew-go-example_0.0.3_darwin_x86_64.tar.gz
Already downloaded: /Users/franscaisarramadhan/Library/Caches/Homebrew/downloads/9189775534d2c9a1a759ec2fc48ea9bf349c2e00efa501ba8cd7c464b5d48882--homebrew-go-example_0.0.3_darwin_x86_64.tar.gz
🍺 /usr/local/Cellar/homebrew-go-example/0.0.3: 5 files, 2MB, built in 3 seconds
- Test the installed binary
homebrew-go-example
2020/09/27 01:03:26 [DEBUG] GET http://numbersapi.com/9/27/date?json
{
"found": true,
"number": 271,
"text": "September 27th is the day in 1331 that the Battle of Płowce between the Kingdom of Poland and the Teutonic Order is fought.",
"type": "date",
"year": 1331
}
Conclusion
- Homebrew formula is easily extensible to accomodate generic use case
- GoReleaser is powerful tool that can also help internal usage of Homebrew formula.