Building and distributing cross-platform binaries for Go applications can be challenging. In this post, I’ll show you how I automate this process using GoReleaser and GitHub Actions, making it easy to ship binaries for multiple platforms with minimal effort.

Why This Approach?

This setup provides several key benefits:

  • Automated cross-platform builds for Linux, macOS, FreeBSD, and Windows
  • Automatic checksum generation for security
  • Homebrew formula updates for easy macOS installation
  • Version information embedding in binaries
  • Consistent release process across all projects

The Configuration Files

Let’s break down the two main configuration files that make this possible.


GoReleaser Configuration

The .goreleaser.yml file controls how your binaries are built and distributed. Here’s a detailed look at the configuration:

version: 2

before:
  hooks:
    - go mod tidy
    - go generate ./...
    - go test

These hooks run before the build process, ensuring your code is properly formatted, generated files are up to date, and tests pass.

builds:
  - id: PROJECT-NAME
    binary: PROJECT-NAME
    dir: ./cmd/PROJECT-NAME

This section defines your build configurations. You’ll need to replace PROJECT-NAME with your actual binary name. The dir field points to your main package directory.

The ldflags section embeds version information into your binary:

ldflags:
      - -extldflags "-static" -s -w -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}}

The build matrix is defined by goos and goarch entries, specifying which platforms and architectures to target:

    goos:
      - linux
      - freebsd
      - darwin
    goarch:
      - amd64
      - arm64
      - arm
      - ppc64le

Windows builds are handled separately to enable UPX compression:

- id: PROJECT-NAME-win
    binary: PROJECT-NAME
    # ... windows-specific configuration
    hooks:
      post:
        - upx -9 "{{ .Path }}"

Homebrew Integration

The brews section configures Homebrew formula generation:

brews:
  - name: PROJECT-NAME
    repository:
      owner: PROJECT-OWNER
      name: homebrew-tap

You’ll need to replace:

  • PROJECT-NAME: Your binary name
  • PROJECT-OWNER: Your GitHub username
  • PROJECT-REPO: Your repository name
  • PROJECT-DESC: A short description of your project, used for homebrew

Setting Up GitHub Actions

The .github/workflows/release.yml file defines the automated release process:

on:
  workflow_dispatch:
  push:
    tags:
      - "*"

This workflow triggers on:

  • Manual activation (workflow_dispatch)
  • Any tag push

Required Secrets

Two secrets are needed:

  • GITHUB_TOKEN: Automatically provided by GitHub Actions
  • HOMEBREW_TOKEN: Must be manually configured

To set up the HOMEBREW_TOKEN:

  1. Go to GitHub Settings → Developer Settings → Personal Access Tokens
  2. Create a new token with repo scope
  3. Add the token to your repository’s secrets (Settings → Secrets and variables → Actions)
  4. Name it HOMEBREW_TOKEN

Release Management

The release section in .goreleaser.yml controls release behavior:

release:
  draft: false
  • When draft: true, releases are created as drafts requiring manual publishing
  • When draft: false, releases are published automatically

To manually publish a draft release:

  1. Go to your repository’s Releases page
  2. Find the draft release
  3. Click “Edit”
  4. Uncheck “Set as a draft”
  5. Click “Update release”

Usage

To create a new release:

  • Tag your commit: git tag v1.0.0
  • Push the tag: git push origin v1.0.0

GitHub Actions will automatically:

  1. Build binaries for all platforms
  2. Generate checksums
  3. Create a GitHub release
  4. Update your Homebrew formula

You can also trigger releases manually through the GitHub Actions interface using the workflow_dispatch event.


Best Practices

  1. Always test builds locally first: goreleaser release --snapshot --clean
  2. Use semantic versioning for tags
  3. Keep your HOMEBREW_TOKEN secure and rotate it periodically
  4. Review the generated release notes before final publication

This setup provides a robust, automated release process that scales well as your project grows. The initial configuration might seem complex, but it saves countless hours in the long run and provides a consistent, professional release experience for your users.


Complete Files

.goreleaser

version: 2

before:
  hooks:
    - go mod tidy
    - go generate ./...
    - go test

builds:
  - id: PROJECT-NAME
    binary: PROJECT-NAME
    dir: ./cmd/PROJECT-NAME
    ldflags:
      - -extldflags "-static" -s -w -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}}
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - freebsd
      - darwin
    goarch:
      - amd64
      - arm64
      - arm
      - ppc64le
    goarm:
      - "7"
    ignore:
      - goos: freebsd
        goarch: arm64
      - goos: freebsd
        goarch: arm
      - goos: freebsd
        goarch: ppc64le
      - goos: darwin
        goarch: arm
      - goos: darwin
        goarch: ppc64le

  - id: PROJECT-NAME-win
    binary: PROJECT-NAME
    dir: ./cmd/PROJECT-NAME
    ldflags:
      - -extldflags "-static" -s -w -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}}
    env:
      - CGO_ENABLED=0
    goos:
      - windows
    goarch:
      - amd64
      - arm64
    hooks:
      post:
        - upx -9 "{{ .Path }}"

archives:
  - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    format: tar.xz
    format_overrides:
      - goos: windows
        format: zip
    wrap_in_directory: true
    files:
      - LICENSE
      - README.md

checksum:
  name_template: "{{ .ProjectName }}_{{ .Version }}--checksums.txt"
release:
  draft: false
changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"

brews:
  - name: PROJECT-NAME
    repository:
      owner: PROJECT-OWNER
      name: homebrew-tap
      token: "{{ .Env.HOMEBREW_TOKEN }}"
    commit_author:
      name: PROJECT-OWNER
      email: PROJECT-OWNER@users.noreply.github.com
    homepage: https://github.com/PROJECT-OWNER/PROJECT-REPO
    description: "PROJECT-NAME: PROJECT-DESC"
    test: system "#{bin}/PROJECT-NAME -v"
    install: bin.install "PROJECT-NAME"

.github/workflows/release.yml

  • This file can be used as-is across different repos
on:
  workflow_dispatch:
  push:
    tags:
      - "*"

permissions:
  contents: write

jobs:
  build:
    name: GoReleaser Build
    runs-on: ubuntu-latest

    steps:
      - name: Check out code into the Go module directory
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set Up Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.x"
        id: go

      - name: run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        env:
          HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          args: release --clean