Automate changelog and releases creation in GitHub

Keeping up to date the Changelog and generating GitHub releases is one of those tasks I always think it is important to do, but I feel it becomes a chore the more you have to do it manually on a given project. In one of my recent OSS projects, camper, I decided from the beginning that I didn't want to be manually generating the Changelog and the GitHub releases information.

After researching different options, I landed on github-changelog-generator. A neat project, it is a Ruby gem that allows you to automatically generate a changelog based on tags, issues and merged pull requests.

Implementation #

Since the project is hosted on GitHub, I went with GitHub Actions for the implementation as part of the CI process. It was an opportunity to put Actions in practice and get familiar with it. This post is not about an introduction to GitHub Actions, check instead the Actions Docs for getting started and diving deep into the subject.

Already back!! Great, let's go into the details.

First let's discuss the main requirements that I had in mind:

When committing to main branch:

  • A new updated Changelog should be generated and committed to main. This would account for merged PRs, as well as any direct commit to main.
  • All latest changes that are not already part of the tagged released, should be grouped under an Unreleased section at the top of the Changelog

When pushing a new tag:

  • Update the Changelog moving all the unreleased changes under the new tag.
  • Create a new GitHub release containing all the information associated with the latest Changelog tagged entry.

CI - Changelog workflow #

As I explained in the previous section, the Changelog update process consists of two parts. One when merging to main and the other when pushing a new tag. The CI - Changelog workflow, as shown below, fulfills the requirement of updating the Changelog on every push to the main branch. You can find the most up to date version at here

name: CI - Changelog

on:
push:
branches: [ main ]

jobs:
changelog_prerelease:
name: Update Changelog For Prerelease
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: main
- name: Update Changelog
uses: heinrichreimer/github-changelog-generator-action@v2.1.1
with:
token: $
issues: true
issuesWoLabels: true
pullRequests: true
prWoLabels: true
unreleased: true
addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}'
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Changelog for PR
file_pattern: CHANGELOG.md

It works as follows:

  1. It checks out the code using the action/checkout v2
  2. It proceeds to generate an update for the Changelog by using the heinrichreimer/github-changelog-generator-action action with the following customizations:
    • All closed issues should be part of the Changelog, including those without labels (issues: true and issuesWoLabels: true)
    • All pull requests should be part of the Changelog, including those without labels (pullRequests: true and prWoLabels: true)
    • It should group all latest changes under an Unreleased section (unreleased: true)
    • It adds a new Documentation section to group issues and pull requests with the documentation label
  3. Then it commits the modified Changelog file back to main using the stefanzweifel/git-auto-commit-action action

Release workflow #

The Release workflow is a more complex pipeline since it not only updates the Changelog, but also handles the publishing of a new gem version as well as a new GitHub release associated with the tag being pushed. For this post, we are only focusing on the Changelog and GitHub release related jobs. If you are interested, check the full workflow here

name: Release

on:
push:
tags:
- v*

jobs:
# Other jobs
# ...
changelog:
name: Update Changelog
runs-on: ubuntu-latest
steps:
- name: Get version from tag
env:
GITHUB_REF: $
run: |
export CURRENT_VERSION=${GITHUB_TAG/refs\/tags\/v/}
echo "::set-env name=CURRENT_VERSION::$CURRENT_VERSION"

- name: Checkout code
uses: actions/checkout@v2
with:
ref: main
- name: Update Changelog
uses: heinrichreimer/github-changelog-generator-action@v2.1.1
with:
token: $
issues: true
issuesWoLabels: true
pullRequests: true
prWoLabels: true
addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}'
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Changelog for tag $
file_pattern: CHANGELOG.md

release_notes:
name: Create Release Notes
runs-on: ubuntu-latest
needs: changelog
steps:
- name: Get version from tag
env:
GITHUB_REF: $
run: |
export CURRENT_VERSION=${GITHUB_TAG/refs\/tags\/v/}
echo "::set-env name=CURRENT_VERSION::$CURRENT_VERSION"


- name: Checkout code
uses: actions/checkout@v2
with:
ref: main

- name: Get Changelog Entry
id: changelog_reader
uses: mindsers/changelog-reader-action@v1
with:
version: $
path: ./CHANGELOG.md

- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: $ # This token is provided by Actions, you do not need to create your own token
with:
tag_name: $
release_name: Release $
body: $
draft: false
prerelease: false

The first job, Update Changelog, is almost the same as the one described on the previous section.The difference is that we are generating released versions only and thus, there is no unreleased: true entry.

The second , Create Release Notes, works as follows:

  1. It relies on the updated Changelog, thus the presence of needs: changelog to force it to wait for the previous changelog job completion.
  2. Using the mindsers/changelog-reader-action, it proceeds to select the changelog entry associated with the tag being pushed.
  3. Using the actions/create-release, it generates the GitHub Release using the content of the changelog entry extracted on the previous step

GitHub action gotchas #

The changelog generator action has some gotchas that are not easy to spot:

  • The majority of options specified in the Update Changelog step, such as issues: true and pullRequests: true default to true on the underlying github-changelog-generator gem, but are required as part of the action, otherwise they get set to false. That tripped me over for a while, until I read the action's implementation, specifically the entrypoint.sh
  • Adding a new section using the addSections field fails if you specify a prefix with multiple words (e.g, Documentations updates as the changelog generator wiki suggests). The issue is with word splitting on the entrypoint.sh as discussed in issue#3.

Changelog generator limitation #

While iterating on the output produced by the changelog-generator gem, I realized that I was getting double entries between PRs that are linked to issues (i.e. PRs that close issues when merged). I dug in the documentation trying to find a way of just showing either the issue or PR to no avail. Then I posted an issue on the github repo and confirmed my suspictions that it is not currently a way to this, due to limitations on the GitHub REST API.

Conclusions #

In this post, we discussed how to automate Changelog and GitHub Releases creation. We went over the details of each of the workflows and describe the steps for each job involved in the workflows. We also mentioned some limitations and gotchas for the github actions and the github-changelog-generator gem.

To conclude, thank you so much for reading this post. Hope you enjoyed reading it as much as I did writing it. See you soon and stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.