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 tomain
. - 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: ${{ secrets.GITHUB_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:
- It checks out the code using the action/checkout v2
- 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
andissuesWoLabels: true
) - All pull requests should be part of the Changelog, including those without labels (
pullRequests: true
andprWoLabels: 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
- All closed issues should be part of the Changelog, including those without labels (
- 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: ${{ 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: ${{ secrets.GITHUB_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 ${{ env.CURRENT_VERSION }}
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: ${{ 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: ${{ env.CURRENT_VERSION }}
path: ./CHANGELOG.md
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog_reader.outputs.log_entry }}
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:
- It relies on the updated Changelog, thus the presence of
needs: changelog
to force it to wait for the previouschangelog
job completion. - Using the mindsers/changelog-reader-action, it proceeds to select the changelog entry associated with the tag being pushed.
- 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
andpullRequests: true
default totrue
on the underlyinggithub-changelog-generator
gem, but are required as part of the action, otherwise they get set tofalse
. 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 theentrypoint.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!!