Assembling a .goreleaser.yaml File: Standards and Best Practices
Over time, I’ve created numerous .goreleaser.yaml files for various projects. While a lot of them are out of date and inconsistent, reviewing them has helped me formulate a standard set of practices for assembling a GoReleaser configuration file.
This post outlines these standards to help maintain consistency, maximize distribution, and simplify CI processes across projects.
General Principles
Naming Packages
I believe that the defaults should work for file and package naming, so I avoid using _template for naming (such as renaming things) unless it is strictly required to prevent a name conflict. I’ve breached this many times, but it has almost always caused me pain. Explicitly naming packages is better.
Rather than point to specific external examples, I tend to avoid specifying keys like id, name_template, or explicitly listing builds when the defaults suffice. When I do create multiple distributions for different binaries because of their vastly different usage intentions, the nFPMs are extensive (supporting 5 formats), but they should ideally consider the different archives and how the installation paths will differ.
CGO_ENABLED
By default, CGO_ENABLED should be set to 0 to disable it, unless specifically required. Be aware that the CI or test suite will need to verify if disabling it can be done without breaking anything in the process.
1builds:
2 - env:
3 - CGO_ENABLED=0
4 goos:
5 - linux
6 - windows
7 - darwin
8 - freebsd
9 - openbsd
10 - netbsd
11 - plan9
12 - illumos
13 goarch:
14 - amd64
15 - 386
16 - arm
17 - arm64
18 - mips
19 - mipsle
20 - mips64
21 - mips64le
22 - ppc64
23 - ppc64le
24 - s390x
25 - riscv64
26 goarm:
27 - 5
28 - 6
29 - 7
Build Flags and Variables
I rely on GoReleaser’s injected version flags a lot. Try to use the default configuration so they don’t need to be specified manually. Overriding defaults just because you can adds no real benefit and might lead to losing other injected variables. I generally try to avoid format overrides, though for Windows it often makes sense.
Target Platforms and Architectures
Remember, the goal is always the maximum number of targets. Specify different architectures (like ARM versions) correctly to expand the output appropriately.
Builds & CI Integration
go mod tidy
Running go mod tidy shouldn’t be part of the GoReleaser process. It puts the repository in an uncommitted state, which GoReleaser is highly sensitive to. This task belongs in the CI system, ideally opening a PR with the updates if possible (the CI system can be updated to add that functionality).
Splitting Complex Builds
I often have to split CGO or library-based configurations into separate, OS-specific files (e.g., .goreleaser-linux.yaml, .goreleaser-windows.yaml) and update the CI accordingly. It’s not ideal, but it’s a practical workaround for complex scenarios.
Packaging (nFPM)
nFPMs should always be included.
When configuring nFPMs, keep the following in mind:
- Archives vs. Packages: An intelligent installer that picks the correct package format for the destination host is ideal, but generally, trying to provide native packages is the goal.
- Man Pages: The man page is important. I try to make the man page generated by the application itself, so generation should be part of the process. Sometimes documentation is included directly via the man page.
- Completions: Shell completions should be included in nFPMs. This is something that should be implemented everywhere.
- Services & Configurations: You can install systemd and rc.d services, as well as install config files and run post-install scripts.
- Explicit Inclusions: Explicitly include the
READMEandLICENSE. The license should match the project license.
1brews:
2 - name: my-project
3 homepage: "https://github.com/arran4/my-project"
4 description: "A description of the project"
5 repository:
6 owner: arran4
7 name: homebrew-my-project
8 commit_author:
9 name: arran4
10 email: arran4@users.noreply.github.com
11 test: |
12 system "#{bin}/my-project --version"
13
14nfpms:
15 - id: default
16 homepage: "https://github.com/arran4/my-project"
17 description: "A description of the project"
18 maintainer: "arran4"
19 license: "CC BY-SA 4.0"
20 formats:
21 - deb
22 - rpm
23 - apk
24 - archlinux
25 - termux
26 contents:
27 - src: README.md
28 dst: /usr/share/doc/my-project/README.md
29 - src: LICENSE
30 dst: /usr/share/doc/my-project/LICENSE
31 - src: my-project.service
32 dst: /etc/systemd/system/my-project.service
33 - src: my-project.rc.d
34 dst: /usr/local/etc/rc.d/my-project
Docker
Docker is a tough one, but I generally try to always get a Docker image published.
- Registries: I generally just submit to one Docker repository (usually the GitHub Container Registry if we are on GitHub).
- Permissions: Building and pushing Docker images requires the appropriate CI permissions.
- Entrypoints: The Docker entrypoint needs to be done intelligently based on the type of project it is. CLI tools should probably bring up a shell rather than executing the tool itself immediately.
- SQL and CGO: Due to SQL components or CGO requirements, we might have to restrict the Docker distributions.
1dockers:
2 - image_templates:
3 - "ghcr.io/arran4/myproject:{{ .Tag }}"
4 - "ghcr.io/arran4/myproject:latest"
5 build_flag_templates:
6 - "--pull"
7 - "--label=org.opencontainers.image.created={{.Date}}"
8 - "--label=org.opencontainers.image.title={{.ProjectName}}"
9 - "--label=org.opencontainers.image.revision={{.FullCommit}}"
10 - "--label=org.opencontainers.image.version={{.Version}}"
Snapcraft
For snapcraft, I usually specify the summary and description, and set the confinement to strict.
1snapcrafts:
2 - summary: "A description of the project"
3 description: |
4 A longer description of the project.
5 This can span multiple lines.
6 publish: true
7 confinement: strict
Future / TODOs
There is a long list of targets and features I want to integrate in the future to maximize distribution:
- Additional Package Managers:
nix,winget,AUR,scoop,snapcraft,flatpak,gemfury. - NPM: I could do
npmpackages, but token expiration would require frequent updates. - Security:
signs,docker_signs, andsboms. If they are not set up, they should be. - Announcements: Integrate announcements properly.
- Upgrades: Upgrade to
docker2. - Release Notes: I like the default Git release notes, but combining them with custom changelogs is something I would like to start doing.
- Gentoo/ebuilds: I am also doing ebuilds/Gentoo, which should be used as a sample for when it’s working and accepted.
For Homebrew Casks, a good example of where I’m starting to use homebrew_casks can be seen in my newer configurations, which should set the template going forward.