Using GoReleaser and GitHub Actions to release Rust and Zig projects¶
GoReleaser v2.5 is out with Rust and Zig support - let's explore how we can use it!
What is GoReleaser?¶
I'm aware that GoReleaser is kind of famous in the Go community, and I'm also aware most Zig and Rust developers might have never heard of it.
This is an attempt to fix it a little bit.
GoReleaser is a release automation tool. Up until recently, it only supported Go, but now we're slowly adding more languages to it, Zig and Rust being in the first batch.
Some of the key features are:
- Cross-compilation and packaging for multiple platforms
- Automatic changelog generation from Git history
- Creation of Linux packages (deb, rpm, apk)
- Docker image building and publishing
- Artifact signing and SBOM generation
- Integration with package managers (Homebrew, Scoop, Winget, etc.)
- GitHub/GitLab/Gitea releases automation
It handles complex release workflows through a single YAML file, eliminating the need to maintain separate release scripts/complex workflows and ensuring consistent releases.
Goal¶
By the end of this, you should have GitHub action do all the work of releasing your binaries, universal binaries, including packaging, Docker images, signing, and more.
Assumptions¶
- You either have a Zig project that can be cross-compiled with
zig build
, or a Rust project that can be cross-compiled withcargo zigbuild
- Your project is hosted on GitHub
- You have GoReleaser installed2
Getting started¶
Into your project root directory, run:
goreleaser init
This should add some entries to your .gitignore
, and create a .goreleaser.yaml
with builds
, archives
, changelog
and a release footer set up.
You can now run:
goreleaser release --clean --snapshot
And it should complete with no problems.
The build¶
Whether you are using Zig or Rust, GoReleaser should have set up the configuration file accordingly when you ran goreleaser init
.
The main note here is that, with Rust, by default, we'll use cargo-zigbuild. You can change that to use cross-rs or do more complex stuff, like using goreleaser-rust-cross.
If cargo-zigbuild works for you, though, I'd stay with it, as it is simpler.
Adding stuff¶
Let's add some features to our release.
After each step, you should be able to run goreleaser release --clean --snapshot
like we did earlier to verify if it works.
Universal binaries¶
Universal binaries (also known as fat binaries) is a format introduced in macOS a while ago, when Apple migrated from PPC to Intel. It has become common again recently with the introduction of their ARM chips. It's pretty much both binaries glued together plus a special header.
Luckily, you don't really need to know any of that, GoReleaser handles it for us. You can enable it by adding this to your .goreleaser.yml
:
# ...
universal_binaries:
- replace: true
That's it!
There are some customization options available, you can see documentation here.
Linux packages¶
GoReleaser uses nFPM to generate Linux packages for many formats. You can enable it by adding this to your .goreleaser.yml
:
# ...
nfpms:
- file_name_template: "{{ .ConventionalFileName }}" # optional, better file names
formats:
- deb
- apk
- rpm
There you can add more things, for example the maintainer, license, more files, etc. See documentation here.
Sign with cosign¶
It's generally a good idea to sign your artifacts. I usually sign the checksums file, as it ensures that the checksums didn't change, and so you can check that the artifacts haven't changed either.
You can use GPG, but generally, cosign is way easier! It even allows keyless signing, which uses either GitHub actions, or asks you to login to sign.
It works pretty great.
You can enable it by adding this to your .goreleaser.yml
:
# ...
signs:
- cmd: cosign
certificate: "${artifact}.pem"
artifacts: checksum
args:
- sign-blob
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes"
The signing feature in GoReleaser allows for many other options, see documentation here for more details.
This will require you to have cosign
installed and available in your $PATH
.
Software Bill of Materials (SBOM)¶
SBOM is a formal record that lists all software components, dependencies, and their relationships. It's mainly used to assess risk in a software supply chain.
By default, GoReleaser will use syft for it. You can enable it by adding the following to your .goreleaser.yml
:
# ...
sboms:
- artifacts: archive
Generally speaking you can tune this to use any other tool you want, see documentation here for more details.
This will require you to have syft
installed and available in your $PATH
.
Homebrew Tap¶
Homebrew is commonly used on macOS to install software. GoReleaser supports homebrew taps, and the taps generated also work on Linux.
You can enable it by adding this to your .goreleaser.yml
:
# ...
brews:
- repository:
owner: YOUR-USERNAME
name: YOUR-TAP
token: "{{ .Env.GH_PAT }}"
directory: Formula
A couple of notes here:
- You'll need a
GH_PAT
in your actions workflow later, as usually taps are published in their own repositories - You'll need to create the repository beforehand. It can be an empty repository. See some examples.
There you can add more things, for example the maintainer, license, more files, etc. See documentation here.
Docker Images¶
We can also make GoReleaser create, push, and sign Docker images and manifests.
Before proceeding, we need a Dockerfile
:
FROM ubuntu
COPY example /usr/bin/example
ENTRYPOINT [ "/usr/bin/example" ]
Notice that we don't build anything here. GoReleaser will setup the context, so we have the previously built binary available, and can then simply copy it!
Now, we have mainly two options: if you want to push only the Linux/amd64 image, you can simply add something like this to your configuration:
# ...
dockers:
- image_templates:
- "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}"
- "ghcr.io/YOUR_USER/YOUR_REPO:latest"
Note: using GHCR will requires us to log in and have the right workflow permissions on the GitHub Action.
Now, if we want a manifest with multiple platforms, we need a little more setting up:
# ...
dockers:
- image_templates:
- "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}-arm64"
goarch: arm64
use: buildx
build_flag_templates:
# note that this changes according to goarch
- "--platform=linux/arm64"
- image_templates:
- "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}-amd64"
goarch: amd64
use: buildx
build_flag_templates:
# note that this changes according to goarch
- "--platform=linux/amd64"
This will build 2 images (and push them later on), which we can then combine in a single manifest like so:
# ...
docker_manifests:
- name_template: "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}"
image_templates:
- "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}-arm64"
- "ghcr.io/YOUR_USER/YOUR_REPO:{{ .Tag }}-amd64"
Finally, we can also sign the manifests:
# ...
docker_signs:
- cmd: cosign
artifacts: manifests
args:
- "sign"
- "${artifact}"
- "--yes"
Note: the manifest will only be built in "production" builds, e.g. without the
--snapshot
flag.
Additional options are available on everything we used here, take a look at the documentation for each feature:
This will require you to have docker
and cosign
installed and available in your $PATH
.
GitHub Actions¶
Now, you probably don't want to run all this locally on every release! Let's use GitHub Actions - but any CI should do in most cases1.
To enable this, we can simply add something like this to .github/workflows/goreleaser.yml
:
name: goreleaser
on:
push:
# run only against tags
tags:
- "*"
# You might not need all of this...
permissions:
contents: write
packages: write
issues: write
id-token: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# we'll need this for both 'zig build' and 'cargo zigbuild'
- uses: mlugg/setup-zig@v1
# only needed if using signing
- uses: sigstore/[email protected]
# only needed if using SBOMs
- uses: anchore/sbom-action/[email protected]
# only needed if using docker
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: goreleaser/goreleaser-action@v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
# 'latest', 'nightly', or a semver
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# used to push the homebrew tap
GH_PAT: ${{ secrets.GH_PAT }}
# Your GoReleaser Pro key.
# Only needed if you're using the 'goreleaser-pro' distribution.
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
Once that's done, you can push a new tag and watch the magic happen!
If you fancy building the release artifacts on every release, you can set up auto snapshotting as well, check this file out for more details.
This is cool! What else can it do?¶
Glad you enjoy it and glad you asked!
Here's an incomplete list:
- Nightly builds
- S3/GCS/etc
- Winget, AUR3, NUR4, Scoop, Krew
- macOS sign and notarize
- DMG
- MSI
- Chocolatey
- macOS App Bundles
- and many more!
Note: some features deemed "enterprise-ish" require the Pro version, which is paid.
Troubleshooting¶
Some things that can help you verify your configuration:
# Most features allow to be skipped, which can help speed up your local iterations.
# See the available options with `goreleaser release --help`.
goreleaser release --clean --snapshot --skip=[feature1,feature2]
# Verify if you have all the needed binaries in your $PATH:
goreleaser healthcheck
# Check if your configuration is valid (only check syntax):
goreleaser check
# If you only want to build the binaries, the build sub command might help!
goreleaser build
If the build itself fails, it's always good to check if it works outside GoReleaser, e.g. run cargo zigbuild --target the-one-that-failed
and see what happens.
Closing words¶
I hope this clarifies a bit what GoReleaser can do!
If you want to check the examples live, they're available here and here.
Also worth noting that Bun and Deno support are in the works, and we'll probably add Python soon as well, maybe all for v2.6. Stay tuned!
Thanks for reading!
Versions used¶
For reference, these are the tools and versions used:
Tool | Version |
---|---|
goreleaser | 2.5.1 |
zig | 0.13.0 |
cargo | 1.83.0 |
rustc | 1.83.0 |
cargo zigbuild | 0.19.7 |
cosign | 2.4.1 |
syft | 1.18.1 |
docker | 27.3.1 |
docker buildx | 0.18.0 |
Cross-posted from carlosbecker.com.