To make the project more user-friendly and attractive, authors add docker images and make distribution builds for many different platforms. It is required for each new version of the project, even the minor. Therefore, it is necessary to write automation of this process because doing it by hand is very long and routine and it’s easy to make mistakes or forget something. Below I will tell you about the GoReleaser which automates the build of releases of golang projects almost without cost.
In the article, all examples will be for GitHub. But these same techniques can be easily adapted to close projects, too.
I have prepared a simple project to demonstrate the possibilities of GoReleaser. This project consists of two parts client and server. The server can count the number of words in the text, and the client can address the server with a query for word count.
I need to make the use of the application as user-friendly as possible and I will need to:
GoReleaser is a utility written in Go that can perform all these actions based on a simple yaml script (in fact, the utility can still do a lot of things useful).
After installing on your OS, you need to run a command to get started:
goreleaser init
• Generating .goreleaser.yaml file
• config created; please edit accordingly to your needs file=.goreleaser.yaml
This command creates a file .goreleaser.yaml
in the project root. So far it is empty and does nothing and I have to fill it. So,
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
I’ll start by describing the builds section. It defines the go builds that should be started. This will create the binaries that we need. Let’s describe this:
builds:
- id: srv
binary: srv
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: ./cmd/server/main.go
- id: cli
binary: cli
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: ./cmd/client/main.go
In this configuration, I have set two builds because it is necessary to build the server and the client. Both build look about the same. I specify which main.go
should be used and how the resulting binary should be called.
Also, I pointed GOOS and listed the platforms for which I need to compile binaries, that is, the result should not be one client and server file, but three: Linux, Mac, and Windows. If the building were done manually, it would look like this:
CGO_ENABLED=0 GOOS=darwin go build -o srv
This is an example only for Mac and only for the server. That is, one building of 6. In addition to OS you can also specify architectures:
goarch:
- amd64
- arm
- arm64
And then, each OS will still be built in additional binary for each architecture. In order not to collect any specific OS/architecture pairs that you do not need, you need to specify them in the list:
ignore:
- goos: darwin
goarch: 386
- goos: linux
goarch: arm
goarm: 7
In my example, I will not do this so as not to complicate the understanding of the result with a large number of resulting files. The reader will later be able to add this to their real projects. Additional environment variables may also often be affected. For example on my latest projects I used GOPROXY
, GOPRIVATE
. All of them can be specified in the section env
, which is separate for each assembly.
Let me show you how building works:
goreleaser release --skip-publish --snapshot
• releasing...
• loading config file file=.goreleaser.yaml
• loading environment variables
• getting and validating git state
• ignoring errors because this is a snapshot error=git doesn't contain any tags. Either add a tag or use --snapshot
• building... commit=87eea7148d7e07b947cdef49e0b1b6a8c406a60e latest tag=v0.0.0
• pipe skipped error=disabled during snapshot mode
• parsing tag
• running before hooks
• running hook=go mod tidy
• running hook=go generate ./...
• setting defaults
• snapshotting
• building snapshot... version=0.0.1-next
• checking distribution directory
• loading go mod information
• build prerequisites
• writing effective config file
• writing config=dist/config.yaml
• building binaries
• building binary=/Users/antgubarev/project/gorelex/dist/srv_darwin_arm64/srv
• building binary=/Users/antgubarev/project/gorelex/dist/srv_linux_amd64/srv
• building binary=/Users/antgubarev/project/gorelex/dist/srv_windows_386/srv.exe
• building binary=/Users/antgubarev/project/gorelex/dist/srv_linux_arm64/srv
• building binary=/Users/antgubarev/project/gorelex/dist/srv_windows_arm64/srv.exe
• building binary=/Users/antgubarev/project/gorelex/dist/srv_windows_amd64/srv.exe
• building binary=/Users/antgubarev/project/gorelex/dist/srv_linux_386/srv
• building binary=/Users/antgubarev/project/gorelex/dist/srv_darwin_amd64/srv
• building binary=/Users/antgubarev/project/gorelex/dist/cli_windows_386/cli.exe
• building binary=/Users/antgubarev/project/gorelex/dist/cli_windows_amd64/cli.exe
• building binary=/Users/antgubarev/project/gorelex/dist/cli_linux_386/cli
• building binary=/Users/antgubarev/project/gorelex/dist/cli_linux_amd64/cli
• building binary=/Users/antgubarev/project/gorelex/dist/cli_linux_arm64/cli
• building binary=/Users/antgubarev/project/gorelex/dist/cli_darwin_amd64/cli
• building binary=/Users/antgubarev/project/gorelex/dist/cli_darwin_arm64/cli
• building binary=/Users/antgubarev/project/gorelex/dist/cli_windows_arm64/cli.exe
• archives
• creating archive=dist/gorelex_0.0.1-next_Linux_x86_64.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Linux_arm64.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Darwin_x86_64.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Darwin_arm64.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Windows_arm64.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Windows_i386.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Linux_i386.tar.gz
• creating archive=dist/gorelex_0.0.1-next_Windows_x86_64.tar.gz
• calculating checksums
• storing artifact list
• writing file=dist/artifacts.json
• release succeeded after 3.39s
From the log, you can see which files were collected and that all of them are in the /dist
directory. I passed two arguments at the start of the command:
--skip-publish
By default, GoReleaser will publish files immediately. This is not required yet, as it is We’re still debugging, but we’ll need it later.--snapshot
Release should be created with a version. GoReleaser takes the version from the latest git tag. And since not yet, I need that flag.
Both of these flags will be frequently required during the preparation and debugging of future releases. I sometimes use a draft repository on the GitHub on which releases will be posted. So I have the ability to edit and improve my release system without affecting users. I highly recommend to do likewise.
As you may have noticed in addition to the build files have also been archived. This is the next opportunity to talk about. The archive must contain everything your user needs to use the product. In the current example, this is the server and client file.
By default, the archive name is built using the following template {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}
(you can also replace this template for your needs). The replacement section specifies appropriate replacements for variables in the template. By default this parameter is empty, but fortunately, the created .goreleaser.yaml
has already specified the combinations, which are enough.
The currently created archives contain both srv and client files. It may sometimes be necessary to have them in different archives. This can easily be done by:
archives:
-
id: srv
builds:
- srv
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
name_template: "{{ .ProjectName }}_srv_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE
- README.md
- doc/server/*
-
id: cli
builds:
- cli
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
name_template: "{{ .ProjectName }}_cli_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE
- README.md
- doc/cli/*
In this example, I have listed the archives that need to be created. In the builds
section, I listed the builds that need to be archived. That is, you can distribute any files for the convenience of users. This can be useful in such projects where many components.
For example, in addition to the client and the server, there may be other agents and utilities for backups and monitoring. Then there will be a reason to separate the CLI utility from the other servers.
Executable files are not the only things that can get into the archive. As you could see in the example, I added a file with license and reads. And for each archive, you can set your own sets. It is convenient for the example which I quoted above. For CLI utility, the documentation set will be different from server utilities.
And I can’t help but mention another possibility. Its hooks.
before:
hooks:
- make clean
- go mod tidy
This functionality allows you to perform additional actions before building. For example, remove extra packages, or create a default configuration file, etc. I would recommend doing this as a step of the pipeline when it comes to embedding the GoReleaser in your continuous delivery system. But when this option is not available, hooks will be very useful. I usually add:
rm -rf dist/
This is convenient when debugging because for building the target directory should be empty.
So, the project is ready for the first release. Let me remind you that as part of this article I will release it on GitHub. Commit our .goreleaser.yaml
. Don’t forget to add /dist
to . gitignore
! These artifact files don’t exactly belong in the repository. Now you need to create a tag and immediately push it.
git tag v0.1.0
git push --tags origin master
GoReleaser requires a GitHub token to be able to use the GitHub API to create and edit releases. You can create it in your settings. You just need to put the flag on management of public repositories Access public repositories
.
export GITHUB_TOKEN=xxx
Finally, start the building without additional flags
The logs added information about the release creation in the GitHub repository.
• publishing
• scm releases
• creating or updating release repo=antgubarev/pet tag=v0.1.0
• release updated url=https://github.com/antgubarev/pet/releases/tag/v0.1.0
• uploading to release file=dist/checksums.txt name=checksums.txt
• uploading to release file=dist/pet_cli_0.1.0_Linux_x86_64.tar.gz name=pet_cli_0.1.0_Linux_x86_64.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Windows_i386.tar.gz name=pet_srv_0.1.0_Windows_i386.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Linux_x86_64.tar.gz name=pet_srv_0.1.0_Linux_x86_64.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Linux_i386.tar.gz name=pet_srv_0.1.0_Linux_i386.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Darwin_x86_64.tar.gz name=pet_srv_0.1.0_Darwin_x86_64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Linux_i386.tar.gz name=pet_cli_0.1.0_Linux_i386.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Windows_arm64.tar.gz name=pet_srv_0.1.0_Windows_arm64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Darwin_arm64.tar.gz name=pet_cli_0.1.0_Darwin_arm64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Windows_i386.tar.gz name=pet_cli_0.1.0_Windows_i386.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Linux_arm64.tar.gz name=pet_srv_0.1.0_Linux_arm64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Windows_arm64.tar.gz name=pet_cli_0.1.0_Windows_arm64.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Darwin_arm64.tar.gz name=pet_srv_0.1.0_Darwin_arm64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Windows_x86_64.tar.gz name=pet_cli_0.1.0_Windows_x86_64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Darwin_x86_64.tar.gz name=pet_cli_0.1.0_Darwin_x86_64.tar.gz
• uploading to release file=dist/pet_cli_0.1.0_Linux_arm64.tar.gz name=pet_cli_0.1.0_Linux_arm64.tar.gz
• uploading to release file=dist/pet_srv_0.1.0_Windows_x86_64.tar.gz name=pet_srv_0.1.0_Windows_x86_64.tar.gz
• announcing
• release succeeded after 7.08s
Go to the release page and see
Hurrah, everything worked out. Besides all the archives still created and changelog, which contains the commits that were made from the previous tag. This will help users to understand what has changed, and the developer from routinely describing it manually.
And that’s the kind of simple thing that I did, in a short time, to organize the releases of the project. Of course, it was even more convenient to automate it with GitHub Workflow. But it goes beyond this topic. In future articles, I will describe how can open source project be made even more attractive with GitHub Workflow and golangci-lint
capabilities.