semantic-release is a great tool for automating version management for a project. I prefer semantic-release
because I can enforce properly structured commit messages using commitlint and properly bump package / project versions based on commit history.
GitHub has branch protection rules that enforces that all changes merged to main
or master
should be through an approval process. If your company is, or wants to be SOC (Type 2) compliant, this should be enabled.
When branch protection rule is enabled, actors cannot directly push to the protected branch and can only get their changes on that branch by raising a pull request. This will obviously break release workflows as changes to package.json
and CHANGELOG.md
will be a new commit on the branch that needs to be pushed.
The *easiest* way to solve this is to create a GitHub bot user that has write access to protected branches and use that to run your workflows.
If you do not want to do that, there is a workaround:
- Merge a PR to
main
- This will trigger a "Release" GitHub action that does the following:
- Run
semantic-release
package on the branch - Commit the changes to a temporary branch
- Raise a PR from that branch titled
chore(release): {new_version_number}
- Run
- Merge the new PR to
main
after approval- You can configure your other deployments (like a production build) to run after this PR gets merged
The "Release" workflow will look something like this
name: Release
# Trigger this job whenever something gets
# pushed to main. You can also use "workflow_dispatch"
# to trigger it manually.
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
# Run your "checkout" and other necessary steps here
- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Your "release" command that will trigger semantic-release
run: npm run release
- name: Set Tag
id: tag
# Did not have great success trying to get version number from semantic-release
# so a simple workaround to give me that.
run: echo "tag=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
# [skip-ci] is necessary to stop an infinite loop of this workflow
# trigger itself after PR gets merged.
title: "chore(release): ${{ steps.tag.outputs.tag }} [skip ci]"
body: |
### ${{ steps.tag.outputs.tag }}
Automated PR created by the Release GitHub Action.
Make sure to trigger the `Deploy (Production)` action **after** merging this PR.
commit-message: "chore(release): ${{ steps.tag.outputs.tag }} [skip ci]"
base: main
# Temporary branch name, can be anything
branch: release
And, your semantic-release
config should look something like this:
/**
* @type {import('semantic-release').GlobalConfig}
*/
module.exports = {
// Releases can only be created from "main" branch
branches: ["main"],
plugins: [
"@semantic-release/release-notes-generator",
[
// This will help write the CHANGELOG
// to the specified path
"@semantic-release/changelog",
{
changelogFile: "docs/CHANGELOG.md"
}
],
[
// "npm" plugin is needed to properly
// increase the "version" field in package.json
// This might not be needed for you if your's is
// not a Node / JS project
"@semantic-release/npm",
{
npmPublish: false
}
]
]
};
Note that in the above release config I have not used the "git" plugin that semantic-release
uses to create a "release" commit. Not using the git plugin is the important bit here, this makes sure that semantic-release
creates the tag from the last commit to the branch and not the new commit it will create when using the plugin.
You can use the "github" plugin that will help drafting a GitHub release for that tag, but I don't need it so I have not.