Release Go Projects with GoReleaser
GoReleaser automates building, packaging, and publishing releases for Go projects from a single config file.
Release on a tag push
This run definition releases on every Git tag push:
on:
github:
push:
if: ${{ event.git.tag != '' }}
init:
commit-sha: ${{ event.git.sha }}
base:
image: ubuntu:24.04
config: rwx/base 1.0.2
tasks:
- key: code
call: git/clone 2.0.7
with:
repository: https://github.com/YOUR-ORG/YOUR-REPO.git
ref: ${{ init.commit-sha }}
github-token: ${{ github.token }}
preserve-git-dir: true
fetch-full-depth: true
- key: go
call: golang/install 1.2.1
with:
go-version: 1.26
- key: goreleaser
run: |
arch="$(uname -m)"
[ "$arch" = "aarch64" ] && arch="arm64"
curl -fsSL "https://github.com/goreleaser/goreleaser/releases/download/${VERSION}/goreleaser_Linux_${arch}.tar.gz" \
| sudo tar -xz -C /usr/local/bin goreleaser
env:
VERSION: v2.15.4
- key: release
use: [code, go, goreleaser]
cache: false
run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ github.token }}
A few things worth noting:
if: ${{ event.git.tag != '' }}filters the run to tag pushes. See GitHub Push Trigger for the full set of event fields.preserve-git-dir: trueandfetch-full-depth: truekeep the.gitdirectory and full history so GoReleaser can compute version metadata and changelogs.
Validate the config on pull requests
Run goreleaser release --snapshot --skip=publish on pull requests to catch config errors before they break a tag push:
on:
github:
pull_request:
init:
commit-sha: ${{ event.git.sha }}
# ... base, code, go, goreleaser tasks as above ...
tasks:
- key: snapshot
use: [code, go, goreleaser]
run: goreleaser release --snapshot --clean --skip=publish
Building Docker images
If your .goreleaser.yaml declares dockers: or docker_manifests:, two things need to change in the release task:
- Set
docker: trueso RWX runs a Docker daemon for the task. Without it, GoReleaser's calls todocker buildanddocker pushwill fail. - Add a
docker loginto the run script beforegoreleaser release. GoReleaser doesn't authenticate for you — it expects the daemon to already have credentials. Pull registry credentials from a secret and pass them as env withcache-key: excludedso the value isn't part of the cache key.