My personal blog

How to publish your Go binary as Homebrew Formula with GoReleaser

2020.09.25

TIL; There is a cool FOSS tool called GoReleaser. This will make releasing Golang project a lot easier. You can see the source code of this example in https://github.com/franzramadhan/homebrew-go-example

Background

We want to automate binary release process of our Golang code. We also want the binary to be installable using Homebrew without needing to manually create the Homebrew Formula.

In this example we want to demonstrate how easily we can do a release for our Go binary project.

Solution and Constraints

  • We can utilize GoReleaser to automate the release and Homebrew Formula creation.
  • GoReleaser also already has Github Action so we can easily integrate it in Github workflow
  • We will only build and release commit-hash that has semantic versioning tag. e.g: v1.0.0, v0.0.1-rc, etc
  • In order to host Homebrew formula, the git repository name is mandatory to have homebrew- prefix

Workflow

Write the code for main.go. Basically it will just do HTTP request to http://numbersapi.com/ to show the fact of the day.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/hashicorp/go-retryablehttp"
)

func main() {
    // use retryable http
    client := retryablehttp.NewClient()
    client.RetryMax = 10

    // get current time and set the date and month format
    currentTime := time.Now()
    month := currentTime.Format("1")
    date := currentTime.Format("2")

    req, err := retryablehttp.NewRequest(http.MethodGet, "http://numbersapi.com/"+month+"/"+date+"/date?json", nil)
    if err != nil {
        log.Fatalln(err)
    }

    res, err := client.Do(req)
    if err != nil {
        log.Fatalln(err)
    }

    defer res.Body.Close()

    var response map[string]interface{}

    fail := json.NewDecoder(res.Body).Decode(&response)
    if fail != nil {
        log.Fatalln(err)
    }

    resp, err := json.Marshal(response)
    if err != nil {
        log.Fatalln(err)
    }

    fmt.Println(string(resp))
}

Write the unit test

package main

import (
    "testing"
)

func TestMain(t *testing.T) {
    main()
}
   # Check https://goreleaser.com/customization/env/
env:
  - GO111MODULE=on
  - GOPROXY=https://goproxy.io

# Check https://goreleaser.com/customization/hooks/
before:
  hooks:
  - go mod download

# Check https://goreleaser.com/customization/build/
builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - darwin
      - linux
    goarch:
      - amd64
    ignore:
      - goos: darwin
        goarch: 386
      - goos: linux
        goarch: arm
        goarm: 7
      - goarm: mips64
        gomips: hardfloat
    # Run upx after build finished
    hooks:
      post: ./upx.sh

# Check https://goreleaser.com/customization/archive/
archives:
  - name_template: "homebrew-go-example_{{ .Version }}_{{ .Os }}_{{ .Arch }}"  
    replacements:
      amd64: x86_64

project_name: homebrew-go-example

# Check https://goreleaser.com/customization/homebrew/
brews:
  - homepage: 'https://github.com/franzramadhan/homebrew-go-example'
    description: 'Example binary distribution using homebrew.'
    folder: Formula
    commit_author:
      name: franzramadhan
      email: franzramadhan@gmail.com
    tap:
      owner: franzramadhan
      name: homebrew-go-example

Create the github workflow for running unit test. This will run the unit test for every pull request and push.

name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # checkout the repository
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      # Install specific version of go
      - name: Setup go
        uses: actions/setup-go@v2
        with:
          go-version: 1.15
      # Run unit test
      - name: Test
        run: go test ./...

Create workflow for release. This will only run when there is a semantic version format tag pushed.

name: Release

on:
  push:
    tags:
        - 'v[0-9]+.[0-9]+.[0-9]+' # Only build tag with semantic versioning format

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # checkout the repository
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      # install upx to compress the binary
      - name: Install upx
        run: sudo apt-get install -y upx
      # Install specific version of go
      - name: Setup go
        uses: actions/setup-go@v2
        with:
          go-version: 1.15
      # Run goreleaser with command line flag
      - name: Release
        uses: goreleaser/goreleaser-action@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          version: latest
          args: -f .goreleaser.yml release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Trigger release

Install and Test

  • Tap the homebrew repo brew tap franzramadhan/go-example
  • Install the binary brew install homebrew-go-example
  • Test to run it
 homebrew-go-example | python -m json.tool
2020/09/25 11:15:23 [DEBUG] GET http://numbersapi.com/9/25/date?json
{
    "found": true,
    "number": 269,
    "text": "September 25th is the day in 2003 that a magnitude-8.0 earthquake strikes just offshore Hokkaid\u014d, Japan.",
    "type": "date",
    "year": 2003
}

Conclusion

  • GoReleaser can make life a lot easier for everyone that want to automated release for their golang project
  • Aside of able to release and create the homebrew formula automatically, it also has other built in feature to release artifact for another type or platform. Such as Docker, NFPM( Deb or RPM ), Artifactory, etc. Complete features and documentation, are listed in the official site
comments powered by Disqus