How to Use GoReleaser to Automate GoLang Build Releases

Written by antgubarev | Published 2022/04/11
Tech Story Tags: golang | goreleaser | github | tutorial | github-actions | golang-tools | go | programming

TLDRGoReleaser automates the build of releases of golang projects almost without cost. It is necessary to write automation of this process because doing it by hand is long and routine. To make the project more user-friendly and attractive authors add docker images, make distributions builds for many different platforms. In this article, all examples will be for GitHub. But these same techniques can be easily adapted to close projects, too. GoReaser is a utility written in Go that can perform all these actions based on a simple yaml script.via the TL;DR App

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:

  • binary server (mac/linux/windows)
  • binary client (mac/linux/windows)

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.


Written by antgubarev | Software engineer with 11 years of expirience. Focused on fault tolerant, distributed systems, PaaS, golang, HA.
Published by HackerNoon on 2022/04/11