Currently, I am working on a Command Line Interface (CLI) tool in Go on GitHub.

In addition to this, I want to implement an automatic build and deployment process so that for all common operating systems, complete binaries can be created.

What is needed for this, I will showcase in today’s post.

GitHub Actions

GitHub offers an easy way to build and deploy projects through its actions.

To achieve this, workflow steps are typically defined in YAML files, which should be executed when specific events occur (like pushing or tagging).

Therefore, GitHub automatically recognizes these files, usually placing them directly in the .github/workflows directory within the repository.

Create credentials

To create resources on GitHub we have to generate a Personal Access Token (PAT).

For this we have to select Generate new token (classic).

Define a Note, set the Expiration to No expiration and check repo scope and all of its children.

Then click on Generate token button below and save the displayed token at a safe place.

Setup secrets

Secrets are very similar to environment variables that, however, are stored in a way that allows them to be read only from GitHub Actions for the purpose of execution.

To create secrets, we have to switch to the repository settings under https://github.com/<owner>/<repo>/settings/secrets/actions.

In the Repository secrets section, we now add an entry called GH_PAT and set its value to the token that was generated in the previous section.

First action: Create a Release on GitHub

For the entire process, we need 2 actions.

In the first one, we have to create a release entry in our GitHub project.

To do this, we need to add the file .github/workflows/tag.yaml with the following content to GitHub and upload it there:

name: Release by tag

on: 
  push:
    tags:
    - '*'

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: ncipollo/release-action@v1
      with:
        token: ${{ secrets.GH_PAT }}

This action is executed the time we push tags to the repository.

After the checkout, ncipollo/release-action will create an empty entry in the release section under https://github.com/<owner>/<repo>/releases with the name of the new, underlying tag, using GH_PAT secret.

Second action: Build and deploy

Now we need to create a second file, called .github/workflows/tag.yaml, with the following content:

name: Build binaries for release

on: 
  release:
    types: [created]

permissions:
    contents: write
    packages: write

jobs:
  releases-matrix:
    name: Release Go binaries
    runs-on: ubuntu-latest
    strategy:
      matrix:
        goos: [darwin, freebsd, linux, openbsd, windows]
        goarch: ["386", amd64, arm, arm64]
        exclude:
          - goarch: "386"
            goos: darwin
          - goarch: arm
            goos: darwin
          - goarch: arm64
            goos: windows
    steps:
    - uses: actions/checkout@v4
    - uses: wangyoucao577/go-release-action@v1
      with:
        retry: 10
        overwrite: true
        github_token: ${{ secrets.GH_PAT }}
        goos: ${{ matrix.goos }}
        goarch: ${{ matrix.goarch }}
        goversion: "1.22"
        md5sum: false
        sha256sum: true
        project_path: "./"
        binary_name: "my-cli-app"

The action is executed after a new release entry has been created in our repository.

First we define a matrix of target operating systems and CPU architechtures:

# ...

    strategy:
      matrix:
        goos: [darwin, freebsd, linux, openbsd, windows]
        goarch: ["386", amd64, arm, arm64]
        exclude:
          - goarch: "386"
            goos: darwin
          - goarch: arm
            goos: darwin
          - goarch: arm64
            goos: windows

# ...

This will include MacOS, FreeBSD, OpenBSD, Windows and Linux for Intel and ARM environments (32- and 64-bit). But on the other hand we need to exclude Apple Systems with 32-bit and Windows ARM with 64-bit CPUs, because they are not supported (anymore).

After the checkout part, we need to setup wangyoucao577/go-release-action, which helps us to build and upload our binaries:

# ...

    steps:
    - uses: actions/checkout@v4
    - uses: wangyoucao577/go-release-action@v1
      with:
        retry: 10
        overwrite: true
        github_token: ${{ secrets.GH_PAT }}
        goos: ${{ matrix.goos }}
        goarch: ${{ matrix.goarch }}
        goversion: "1.22"
        md5sum: false
        sha256sum: true
        project_path: "./"
        binary_name: "my-cli-app"

At the end you should adjust goversion and binary_name settings for your needs.

Start first release

Push both .yaml files to your GitHub repository with a first working version of your Go code.

Then create a new Git tag locally and use a valid version number as name, beginning with a v prefix:

git tag -a v0.0.1 -m "version 0.0.1"

To synchronize anything with the remote server, you have to execute

git push --tags

In conclusion, you can now observe via the address https://github.com/<owner>/<repo>/actions to see if everything is working as expected.

To build and deploy a new version, simply repeat the last two Git commands again.