Setting up Automated Semantic Releases: A Guide to Painless Versioning
But before that, why does it even matter?
Did you ever find yourself shipping a “minor” release, only to realize it released breaking change in your project? Or maybe you forgot to update the package version appropriately? Or maybe, you’re spending countless hours trying to go through the entire commit history since last release, piecing together a changelog and can’t figure out if all of it should be considered a minor or a major release, and if so, what should be the new appropriate version?
If you feel relatable with all of that, don’t worry, you are not alone. Manual versioning is a pain in the neck, a breeding ground for inconsistencies, prone to errors, and last but not least, unexpected headaches, both for and from customers.
Well, what is a Semantic Release?
In it’s entirety, a semantic release conforms to Semantic Versioning, a globally adopted standard that represents the nature of changes in each new version number.
It looks like this: Major.Minor.Patch (for example, 4.2.1).
- Major: Represents breaking/incompatible changes
- Minor: Represents new features in a backwards compatible manner
- Patch: Represents bug fixes
This versioning makes your project predictable. As soon as the users see a specific release, they know whether the new updates contain breaking changes, or if they can upgrade simply with the confidence.
Where’s the automated part?
Automation means that a machine, program or a bot is responsible for determining the version numbers, generating changelog, release notes, and publishing your package. This automation will require your commits messages to be written in a specific format called Git conventional commits, which will then automatically generate appropriate changelog based on your commit history.
Enough talk, show me the way!
In the content system project, we are utilizing automated semantic releases, together with other necessary components that make all of this, smoother than butter.
Enforcing conventional commit messages
As we said before, the automated semantic releases require your commits to be written in a specific format, for them to be included in your releases. This format can be found at conventionalcommits. We enforce this both locally and within CI.
- To enforce this locally, we are using husky. It generates a hook that is run everytime we make a commit, and validates the commit message. If the format follows convention, it lets the commit through. The hook looks like this:
# filename: .github/commit-msg
npx --no -- commitlint --edit "$1"
- To enforce this within CI, so that all commits within a PR must follow the conventional format, we are using commitlint. You can set up a workflow that runs everytime a PR is opened, or updated, which will validate all commits within a PR. This workflow looks like this:
commitlint:
name: Validate commit messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js with Caching
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "yarn"
- name: Restore and Prepare Dependencies
run: yarn --frozen-lockfile
- name: Print versions
run: |
git --version
node --version
yarn --version
npx commitlint --version
- name: Validate current commit (last commit) with commitlint
if: github.event_name == 'push'
run: npx commitlint --last --verbose
- name: Validate PR commits with commitlint
if: github.event_name == 'pull_request'
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
- To enforce the same conventional format on PR titles, given the fact that the commit message a PR generates upon merging is the same as PR title, we are using amannn/action-semantic-pull-request@ Github action in our CI.
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Automated semantic releases with CI
FInally, for automatically generating releases everytime PRs are merged to our main branch, we are utilizing the cycjimmy/semantic-release-action in our CI workflow. This action takes care of the following:
- New tag creation
- New release creation
- Release notes
- Changelog generation
Here’s a sample, minimal workflow job. You can find a detailed release workflow in action on content system’s release workflow.
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v5
id: semantic # Need an `id` for output variables
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Do something when a new release published
if: steps.semantic.outputs.new_release_published == 'true'
run: |
echo ${{ steps.semantic.outputs.new_release_version }}
echo ${{ steps.semantic.outputs.new_release_major_version }}
echo ${{ steps.semantic.outputs.new_release_minor_version }}
echo ${{ steps.semantic.outputs.new_release_patch_version }}
And that’s it! You have successfully implemented Automated Semantic Release System in your project. All of this comes with many benefits like zero-downtime versioning, guaranteed SemVer compliance, automatic changelog generation, improved commit hygiene, and reduced manual labor. So if you’re still manually updating versions and struggling with changelogs, it is time to automate it!