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 namePROJECT-OWNER
: Your GitHub usernamePROJECT-REPO
: Your repository namePROJECT-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 ActionsHOMEBREW_TOKEN
: Must be manually configured
To set up the HOMEBREW_TOKEN
:
- Go to GitHub Settings → Developer Settings → Personal Access Tokens
- Create a new token with
repo
scope - Add the token to your repository’s secrets (Settings → Secrets and variables → Actions)
- 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:
- Go to your repository’s Releases page
- Find the draft release
- Click “Edit”
- Uncheck “Set as a draft”
- 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:
- Build binaries for all platforms
- Generate checksums
- Create a GitHub release
- Update your Homebrew formula
You can also trigger releases manually through the GitHub Actions interface
using the workflow_dispatch
event.
Best Practices
- Always test builds locally first:
goreleaser release --snapshot --clean
- Use semantic versioning for tags
- Keep your
HOMEBREW_TOKEN
secure and rotate it periodically - 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