GitHub Actions: Create Releases With Changelog and Assets

2020-06-22

GitHub Actions is all about automating things. Creating releases can include annoying manual work, though; for instance, there is little out-of-the-box support for adding the correct assets and a meaningful description.

Today I learned how to build a workflow that proceeds as follows.

  1. Triggered by semver-style tags,

  2. build Go binaries for multiple platforms,

  3. collect build artifacts,

  4. create a GitHub release with

  5. the relevant CHANGELOG section as description, and

  6. attach an archive with binary, license, and README for each target platform.

I will explain the most relevant parts with snippets below; find the full workflow I created here.

Building Many Binaries

We can use a build matrix as a convenient way to build different combinations of binaries:

build:
  runs-on: 'ubuntu-18.04'
  strategy:
    matrix:
      target-os:
        - 'linux'
        - 'darwin'
        - 'windows'
      target-arch:
        - 'amd64'

It is important to note here that target-os and target-arch are my choice, not built-ins — use whichever parameters your build requires! We can use those names as variables in a step later on:

- run: |
    go build -o my-app ./...

    version="${GITHUB_REF#refs/tags/}"
    archive_name="my-app_${version}_${GOOS}-${GOARCH}"
    if [[ "${GOOS}" == "windows" ]]; then
      zip "${archive_name}.zip" my-app LICENSE README.md
    else
      tar -czf "${archive_name}.tar.gz" my-app LICENSE README.md
    fi
  env:
    GOOS: ${{ matrix.target-os }}
    GOARCH: ${{ matrix.target-arch }}

Assemble Artifacts

By using a build matrix above, we have created many independent jobs. When we create the release later, we do not see their respective file systems! Therefore, we have to store the archives we created as build artifacts. Using action upload-artifact, we can even collect all archives in a single artifact:

- uses: actions/upload-artifact@v2
  with:
    name: "Build Archives"
    path: 'my-app_*'

Prepare Release

Now, in a separate job without a build matrix, we can assemble our release. We will use the neat action git-release, but some preparation is needed. First, we download the build archives from the artifact store and create a list of all included files.

- uses: actions/download-artifact@v2
  with:
    name: "Build Archives"

- id: asset_names
  run: echo ::set-output name=LIST::$(ls my-app_*.{tar.gz,zip})
NoteThis seems a little more cumbersome than necessary; I created a ticket.

Then, because I want to release tags of the form +X.Y.Z-foo as pre-releases, another step to inspect the Git tag:

- id: is_pre_release
  run: |
    version="${GITHUB_REF#refs/tags/}"
    if [[ "${version}" =~ -.*$ ]]; then
      echo ::set-output name=IS_PRERELEASE::true
    else
      echo ::set-output name=IS_PRERELEASE::false
    fi

Note the use of workflow command set-output to persist information for a later step.

Create Release

Finally, we can call the release action:

- uses: docker://antonyurchenko/git-release:v3
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    DRAFT_RELEASE: "false"
    PRE_RELEASE: ${{ steps.is_pre_release.outputs.IS_PRERELEASE }}
    CHANGELOG_FILE: "CHANGELOG.md"
    ALLOW_EMPTY_CHANGELOG: "false"
    ALLOW_TAG_PREFIX: "true"
  with:
    args: "${{ steps.asset_names.outputs.LIST }}"

This does quite a few things automatically; in particular, it will pull the description from our keep-a-changelog-style CHANGELOG.md (and fail if we forgot to add one).

GitHub ActionsCI/CD
Comment & Share on Twitter 

Postpone Discussions in GitLab

Control SVG Groups in LaTeX