Have you ever?
- 😠 been mad about bad commit messages?
- 🤔 agonized over the correct semantic version bump for your project?
- 🤯 found it challenging or expensive to align releases on GitHub and npm?
- 😟 missed a useful changelog?
If any of the above seems familiar, you might want to learn how to integrate semantic-release into your npm package and/or GitHub project! It’s a guide you can follow to get all these problems solved in an automated and standardized way.
We’ll learn some good practices for setting up and combining a bunch of npm packages in order to
- 💬 have commits in a standardized format
➡️ help collaborators write meaningful commit messages
- 🤖 automate semantic versioning based on commits
➡️ no more mind-twisting discussions
- 📦 enforce semver releases and sync GitHub and npm
➡️ no more fiddling around
- 📝 auto-create change logs or release notes from commits
➡️ saves effort and motivates for even better commit messages
To achieve this, we’re going to combine two crucial concepts, Conventional Commits and Semantic Releases.
Core Concepts
Conventional Commits
Just in case you’ve never heard about Conventional Commits before, let me quote their website:
The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.
Sounds neat but also sounds like overhead? Don’t worry, we’ll make it slick. What we’ll earn are commit messages that…
- are interpretable by machines
- are structured with types, scopes, and breaking change markers
- can be translated to semver patch, minor, major
Message scheme:
Semantic Release
semantic-release automates the whole package release workflow. This includes determining the next version number, generating the release notes, and publishing the package.
- When a release is triggered, it looks into commits since the last release to detect the next semantic version.
- It translates the commit types from the Conventional Commits concept into a new version number:
- The commit type fix increases the PATCH version.
- The string BREAKING CHANGE in the commit footer increases the MAJOR version.
- Any other commit type increases the MINOR version.
Preparations
To get to know the basic concepts and setup requirements, start with an empty project. While it is absolutely possible to add semantic-release to an existing project, maybe even with existing versions and releases, we want to focus on the fundamentals here and what a good basic setup can look like.
Packages We'll Install
To give you an overview of what does what, here's a list of npm packages we'll use. All of these packages will be devDependencies (or global if you like). Don’t faint, we’ll install these packages together on the way. The touch points you are going to have later during your daily work will be quite comfy, promised!
Conventional Commits
- @commitlint/cli
the CLI is the main way to interact with commitlint
- @commitlint/config-conventional
one of several available configurations for commit message shapes
- commitizen
a CLI tool assisting with writing conventional commits
- cz-conventional-changelog
the adapter for commitizen to use the commit configuration we chose (conventional changelog)
Semantic Release
semantic-release automates the whole package release workflow. This includes determining the next version number, generating the release notes, and publishing the package.
- When a release is triggered, it looks into commits since the last release to detect the next semantic version.
- It translates the commit types from the Conventional Commits concept into a new version number:
- The commit type fix increases the PATCH version.
- The string BREAKING CHANGE in the commit footer increases the MAJOR version.
- Any other commit type increases the MINOR version.
Preparations
To get to know the basic concepts and setup requirements, start with an empty project. While it is absolutely possible to add semantic-release to an existing project, maybe even with existing versions and releases, we want to focus on the fundamentals here and what a good basic setup can look like.
Packages We'll Install
To give you an overview of what does what, here's a list of npm packages we'll use. All of these packages will be devDependencies (or global if you like). Don’t faint, we’ll install these packages together on the way. The touch points you are going to have later during your daily work will be quite comfy, promised!
Semantic Release
semantic-release automates the whole package release workflow. This includes determining the next version number, generating the release notes, and publishing the package.
- When a release is triggered, it looks into commits since the last release to detect the next semantic version.
- It translates the commit types from the Conventional Commits concept into a new version number:
- The commit type fix increases the PATCH version.
- The string BREAKING CHANGE in the commit footer increases the MAJOR version.
- Any other commit type increases the MINOR version.
Preparations
To get to know the basic concepts and setup requirements, start with an empty project. While it is absolutely possible to add semantic-release to an existing project, maybe even with existing versions and releases, we want to focus on the fundamentals here and what a good basic setup can look like.
Packages We'll Install
To give you an overview of what does what, here's a list of npm packages we'll use. All of these packages will be devDependencies (or global if you like). Don’t faint, we’ll install these packages together on the way. The touch points you are going to have later during your daily work will be quite comfy, promised!
Conventional Commits
- @commitlint/cli
- the CLI is the main way to interact with commitlint
- @commitlint/config-conventional
- one of several available configurations for commit message shapes
- commitizen
- a CLI tool assisting with writing conventional commits
- cz-conventional-changelog
- the adapter for commitizen to use the commit configuration we chose (conventional changelog)
Semantic Release
- semantic-release
core package that automates the whole package release workflow including
- determining the next version number
- generating the release notes
- publishing the package
- semantic-release-cli
CLI tool for conveniently configuring semantic-release
- @semantic-release/changelog
plugin to create or update a changelog file
- @semantic-release/git
plugin to commit release assets (like the CHANGELOG.md) to the project’s git repository
- @semantic-release/github
standard plugin to publish a GitHub release and comment on released pull requests/issues (we won't install this explicitly, since semantic-release has it on board)
- @semantic-release/npm
- standard plugin to publish an npm package (we won't install this explicitly, since semantic-release has it on board)
Setup Conventional Commits
Let’s get serious! A functioning team is able to align on many things. Why not standardized commit messages, too? 😉
Install Commitlint To Enforce The Commit Format
Commitlint will ensure the correct format of commits.
Install Commitlint CLI:
To know how to validate commit messages, Commitlint needs to be configured. We use a very common ruleset:
To tell Commitlint to use this ruleset, we need to write a configuration. There are some options for where to put it. We'll go for a .commitlintrc file here since that makes it very obvious that there’s a configuration in place. Let’s create this file and put it into our project root. This is the file content:
We add the rule "body-max-line-length" because if we have bots (e.g. Dependabot) that create long lines in commit bodies (e.g. by adding URLs), the CI workflow we'll create later would break.
⚡️ Admittedly, we’ll weaken the rules for commit messages here. This is sort of opposed to what we want to achieve—good commit messages for good change logs. So, it’s not a perfect solution, but a simple one that’ll do for a basic setup.
Commit With Commitlint
Hey, we’ve added a file! What a good opportunity to make a commit. Stage your file(s) and commit them:
There’s also a way to test if and how Commitlint works without making a commit (and if the last commit will match the needs). Just run
⚡️ This will produce an error if your repo has only one commit.
➡️ The test was positive if the output looks like this:
Use Commitizen For More Convenience
To support new devs (and ourselves) with using the conventional commit format, we can make use of Commitizen. This is optional but will make writing commits with the correct shape easier. It’ll provide a form in the terminal that guides us step by step.
We install the CLI first:
Then we use commitizen to install the correct ruleset, also called adapter:
This will add a section to our package.json:
The Commitizen CLI is configured now to create the same format that Commitlint considers valid.
To create a new commit, but with guidance, run:
You’ll see that Commitizen creates a handy form in your terminal:
So, why not use it to commit the latest changes?
- choose “chore” and hit Enter
- next, you’re asked for the optional scope
- this would result in a commit message starting with chore(scope)
- let’s skip this one now by hitting Enter
- then we get to write the actual description
- type “add commitizen” and hit enter
- the message will now be chore: add commitizen
- what follows is an option to enter a longer message body
- something to play around with later, useful for instance to create a verbose changelog
- let’s move on for now; skip with Enter
- then we’re asked if there’s a breaking change; use an “N” in this case
- the last question is if the change affects any open issues; let’s also say “N” now
➡️ Of course, you can still do git commit -m "chore: add commitizen". If you know how to, that'll be faster. Consider Commitizen an assistant for contributors that are not yet familiar with the Conventional Commits format.
⚙️ If you prefer to do your commits right within VS Code’s UI, there’s also a Conventional Commits extension for VS Code. For other editors or IDEs, there may be similar plugins.
Use Commitlint In Pre-commit Hook
Standardized commit messages and a nice input helper aren’t worth anything if the rules aren’t enforced. Woof! You’ve seen it coming, it’s time for Husky! 🐶
We’ll use Husky as it’s a popular solution to this challenge, following their instructions for automatic installation:
⚡️ If this fails, your npm/Node may be outdated. If updating Node and npm is not an option, the instructions for manual installation may help.
The above command will install Husky and create a sample pre-commit hook under .husky/pre-commit. We don't need this hook, so let's delete it to not get into our way.
And we can create the one we need with husky add:
The file created, .husky/commit-msg, should look like this:
Now, with every commit we make, Husky will bark at us if the message does not fit what we configure. More specifically, Husky will run commitlint and if that throws no errors, Husky will allow the commit.
➡️ But what about commits that bypass Husky, like merge commits created through GitHub PRs?
→ Merge commits are cleverly sorted out by semantic-release!
Now, we’ve made changes by adding Husky, so let’s test Husky right away by staging and committing it.
Conclusion
💚 That's it, part 1 is done! Now we have...
✅ a commit format according to the Conventional Changelog
✅ commit support by commitizen
✅ enforcement by commitlint via Husky
Our commit messages are in good shape, so let’s put semantic releases into action (literally). Have ready your npm credentials and a GitHub personal access token.
The way semantic-release will work (and what we’re going to configure it for) is this:
- analyze commits
- create a new GitHub release
- This requires semantic-release to have push access to our repository. For this purpose, we need to have a GitHub personal access token and provide it as a variable called GH_TOKEN.
- create a new npm release
- Here semantic-release needs to access the npm registry on your behalf. It’ll ask for your credentials during setup.
Install semantic-release
First, we install semantic-release. And for a convenient configuration, we'll use semantic-release-cli. It's recommended you install it globally, so you can use it on other projects:
Configure semantic-release
We’ll go through a guided configuration. Run
and follow the steps. These questions are asked:
We'll choose GitHub Actions as the CI tool here. The GitHub token as well as the NPM token will be required by the CI workflow. There, they'll be available as the variables GH_TOKEN and NPM_TOKEN. The former is your personal access token set up in your user account. The latter was added to the repository settings by running npx semantic-release-cli setup.
npm Access For semantic-release
Make sure it worked by checking your repository settings on GitHub. You should find a Repository secret called NPM_TOKEN.
As you can see, the NPM_TOKEN secret was put here magically.
⚡️ This looks nice, but there’s a little flaw since npm introduced to 2FA. The only 2FA-mode from npm that semantic-release supports is the auth-only mode.
If you’re not using that mode in your npm profile, you’ll see semantic-release error when trying to release to npm.You can change your 2FA mode with the command:
If you experience an error here and you already have set up 2FA for your npm account, set up 2FA again from scratch may help. Unset it first:
And then use
GitHub Access For semantic-release
For GitHub it’ll be a bit more straight-forward:
Create a new personal access token and select the scope “repo”.
⚡ For security reasons, the token should have an expiration date. But be aware that it can be confusing and frustrating, even expensive, when semantic-release suddenly stops working. It’s not so easy to dig out the reason when it’s the token being expired. Note this well in your project docs so others won’t need to spend too much time searching for the cause. Even better, create a recurring reminder to renew the token in time.
CI Setup
semantic-release provides a nice recipe for usage with GitHub actions (among others), that we slightly alter.
Create the file .github/workflows/release.yml with the following content:
In the package.json we need to define a release branch by adding this block:
We’re going to keep it simple, though, and release from the main branch to npm. If you want to use a different branch, replace main by the name of the branch you want to release from.
📘 semantic-release supports releases to different release channels from different branches. It’s somewhat advanced and their GitBook provides some interesting articles about this topic.
Also, in the beginning, we don’t want that semantic-release runs automatically. Instead, we choose to run it manually, until we’re sure everything works as desired. This means an event trigger is what we’d go for. In the release.yml above, we have ensured this with this configuration:
This way we’ll be able to run a workflow in the GitHub actions UI by clicking the workflow_dispatch event trigger.
⚡ Don’t run the workflow yet, we’ll now make a test workflow that won’t release 😊
Dry-run CI For Testing
To have the option for testing semantic-release, especially during setup and configuration, there's the command semantic-release --dry-run. It will perform all the release steps without making an actual release.
Copy the ./github/workflows/release.yml to ./github/workflows/release-dry-run.yml. Replace the workflow name in line 1:
Also, replace the last line with this:
Now we have a dry-run workflow that we can run manually from the GitHub Actions interface.To try it out, let’s merge our changes into main (or what the name of your release branch is) and push it to remote.
➡️ Keep in mind that in this action semantic-release is not armed and ready. We can make changes to our release branch on remote and it will not get released accidentally.
Once the changes are on your release branch in GitHub, go to the “Actions” tab. You'll find both actions listed. Select "Release Dry-run" and click the workflow_dispatch event trigger at the right, saying "Run workflow". Make sure to choose your release branch as the branch to work on.
If everything is okay, the workflow should run successfully.
The First Release
💚 Now we have semantic-release basically running. It is already able to
✅ analyze commits (with its on-board plugin @semantic-release/commit-analyzer)
✅ release on npm (with its on-board plugin @semantic-release/npm)
✅ release on GitHub (with its on-board plugin @semantic-release/github)
✅ generate release notes (with its on-board plugin @semantic-release/release-notes-generator)
So, why not make a real release? 🤩
⚡ First, make sure that your release branch has a commit of a type that actually does semantic-release count up the version number! In this example, we've only used type “chore”, so far. This won’t make for a version change. As you may have seen in the logs of the dry-run action, at the end of the Release step there was this output from semantic-release:
Of course, something we do behind the curtains that neither is user-facing nor changes the API or has any whatsoever effect on the surface of our project should not affect the version number.
Let’s create a commit of a relevant type. Just to test it, imagine we’ve added a new feature, now in the shape of a new file called great-feature.js. Stage and commit it. Can you guess the commit message format?
It should look like this:
On the remote release branch, we now could run the Release action. But first, let’s give the npm release a deeper thought.
(No) Release On npm
Things to notice:
- if our repo is private,
- we won't be able to release as a public npm package
- if our package name exists
- we won't be able to release to npm
- => give the package a namespace in package.json, e.g. "name": "@your-awesome-npm-user-name/your-awesome-package-name",
- if we don't want to release on npm
- we can configure this by adding an entry to the "release.plugins" section in package.json:
Release On GitHub
It’s time to run the workflow_dispatch event trigger for the Release workflow. What we find when it's through:
The Release workflow run was successful! 🎉
The release on GitHub is there! 🎉🎉
Bonus Level: Adding Release Notes To Docs
We want a CHANGELOG.md file to be part of our code base, so devs don't need to look up the changes on GitHub. To do so, we need two extra plugins for semantic-release: @semantic-release/changelog and @semantic-release/git:
The changelog plugin for semantic release will write into the changelog file. In the "release.plugins" section of package.json, we need to define the position of the file:
The git plugin is a generic plugin for semantic-release that is able to commit release assets, such as our CHANGELOG.md. We need to tell it which files to consider, so we include another entry in the "release.plugins" section:
And at the very start of the plugins section, we add this:
It ensures that the plugins, also the default ones, get run in the right order.
So now we can commit our additions with the message, say, "chore: add changelog automation". Before we push and proceed to release, let’s do a local dry-run first to see if it succeeds:
All seems good, but the last line of the output—if you stayed close to this article so far—says: There are no relevant changes, so no new version is released. So we wouldn’t get a new release and see our changelog update at work!
Therefore, let’s simply add another feature in the shape of another file, greater-feature.js. And we commit it:
In the output, we now find Published release 1.1.0 on default channel. Looking good, so far! Let’s push it and trigger the Release action on GitHub.
We can now find the new CHANGELOG.md in our files!
Note the commit message, we never wrote that one. It’s created automatically by semantic-release to include the changelog file, using the [skip ci] annotation to skip any automatically triggered CI run. If we look inside, we see that a neat little changelog was auto-generated.
I’ve made some other commits/releases for this example so we can see two entries:
Just like in the GitHub release notes, the changelog features an automatically detected structure, where changes get separated into features, bug fixes, etc.—almost a document you could hand out to users, especially if you start writing commit message bodies with some more explanatory words.
☝️ One last thing!
Let's double-check the "release" section in package.json. This is how the complete section should look like:
Notice that we include plugins that we don't even configure specifically. Since we define the plugins manually, we need to list all those we need.
⚡ Not sure if this is stated anywhere in the semantic-release docs. But I noticed that, for instance, if you leave away the (built-in!) plugin "@semantic-release/npm", here, you won’t get any more releases to npm.
Finish Line
💚 And that’s it!
We have…
✅ standardized, clean commits
✅ automatic semantic versioning
✅ synchronous releases on GitHub and npm
✅ auto-generated changelog
➡️ More Ideas for You To Play Around With
Got intrigued with semantic-release? Here are some suggestions you could look further into!
- automatically trigger the release workflow
- try other CI integrations
- build an entire release workflow with semantic-release
- publish on different distribution channels from different branches
- publish maintenance releases
- publish alphas, betas, or rcs