When we are developing JavaScript packages, there is a series of repetitive tasks that we have to complete manually every time we have to publish a new release to npm:
- Change the version field in
package.json
- Create a new Git tag and a GitHub release
- Execute any build steps to create the release artifacts
- Update the changelog
- Publish to npm
Wouldn't be great if we could automate all of these tasks?
GitHub Actions and semantic-release have us covered!
GitHub Actions is a GitHub feature that lets us build, test, and deploy our GitHub hosted projects. You can think of it as the CI/CD pipeline for GitHub. It uses YAML files, called workflows
, that trigger based on specific events (e.g. when a commit is pushed).
semantic-release is a tool that uses the Conventional Commits message format to determine the type of changes in our code base. It automatically sets the next semantic version number, generates the changelog and publishes the release.
Let's start by preparing our repository.
Check existing version tags
If we are going to use semantic-release in an existing repository we'll first have to make sure that the most recent commit included in the last published npm release is in the release branches history and is tagged with the version released.
You can skip this step if you are setting up semantic-release in a new repository.
Assuming our release branch is main
, last commit SHA
is 1234567
and current published version of our project is v1.1.0
# Make sure the commit 1234567 is in the release branch history
$ git branch --contains 1234567
# If the commit is not in the branch history
# we need to configure our repository to have the last release
# commit in the history of the release branch
# List the tags for the commit 1234567
$ git tag --contains 1234567
# If v1.1.0 is not in the list we have to add it with
$ git tag v1.1.0 1234567
$ git push origin v1.1.0
Remove version from package.json
Since semantic-release takes care of updating the package.json’s version before publishing to npm, we can set "version": "0.0.0-semantic-release"
inside our package.json
.
Create an npm token
In order for our GitHub action to be able to publish our package to npm, we're going to need an npm authentication token.
Login into your npm account, click on the profile icon, and select Access Tokens. Then click on Generate New Token -> Classic Token, fill in a name for your token e.g. my_token
, select the Automation token from the list and click Generate Token. Copy the token and make sure you don't lose it, as we're going to need it for the next step.
Add the npm token to the GitHub's repository secrets
Navigate to your GitHub repository page, click Settings and then Secrets and variables -> Actions. Click on the New repository secret, fill in NPM_TOKEN
as the Name, paste the npm token created on the previous step inside the Value field and hit Add secret.
Next, on the left menu, navigate to Actions -> General scroll down, and in Workflow permissions select Read and write permissions.
That's it, now the NPM_TOKEN
can be used as an environment variable inside our GitHub release action.
Create the GitHub release action
Let's create the GitHub release action that will run every time we push a commit to our main
and beta
branches. The beta
branch will be used for our pre-releases in case we need any.
Create a .github/workflows/release.yml
file in the project's root with the following contents.
.github/workflows/release.yml
name: Release
on:
push:
branches: [main, beta]
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install dependencies
run: npx ci
- name: Install semantic-release extra plugins
run: npm install --save-dev @semantic-release/changelog @semantic-release/git
- name: Lint
run: npm run lint-fix
- name: Test
run: npm run test:unit
- name: Build
run: npm run build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
Here we're using Node.js version 20, as it's a requirement of semantic-release
, so make sure you align that with your project's node version.
We also have steps for linting, testing, and building our code. Go ahead and remove or change these as you see fit.
The important parts are the Install semantic-release extra plugins
and the Release
steps.
Notice that we don't have to install semantic-release as it comes pre-installed in GitHub actions by default. We just need the
@semantic-release/changelog
and@semantic-release/git
plugins.
Inside the Release
action you'll notice two environment variables
GITHUB_TOKEN
That is the token used to authenticate to GitHub. This is an automatically created secret to use in our workflow and it is needed by semantic-release in order to be able to create Git tags.NPM_TOKEN
Is the npm authentication token that we created and added to our repository previously. We'll need this in order for our action to be able to publish our package to npm.
semantic-release configuration
semantic-release configuration can be set by using a .releaserc
file, a release
key inside package.json
or a release.config.js
file in the project's root. We'll use the latter.
release.config.js
module.exports = {
branches: [
'main',
{
name: 'beta',
prerelease: true
}
],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/changelog',
{
changelogFile: 'CHANGELOG.md'
}
],
'@semantic-release/npm',
'@semantic-release/github',
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'dist/**'],
message: 'chore(release): set `package.json` to ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
}
]
]
}
The branches
attribute includes the branches on which releases should take place. Apart from main
we also include a beta
branch with prerelease: true
, this way we can have beta versions published using a beta
branch.
In the plugins
section we define the list of semantic-release plugins to use. The plugins we have defined are already part of semantic-release so we don't have to install them separately.
@semantic-release/commit-analyzer
It determines the type of our release (e.g.major
,minor
,patch
) by analyzing commits with conventional-changelog. semantic-release uses Angular Commit Message Conventions by default.@semantic-release/release-notes-generator
It generates the release notes for the changelog.@semantic-release/changelog
It creates and updates the changelog file, with the content created by therelease-notes-generator
in the previous step.@semantic-release/npm
It publishes the npm package@semantic-release/github
It publishes the GitHub release and comment.-
@semantic-release/git
It commits the release artifacts to the project's Git repository. In this example we're committing the changelog file and all files inside thedist
folder. We're also defining the message for the release commit.Note:
[skip ci]
in the commit message is used in order to not trigger a new build.
Enforce conventional commits with commitlint and husky
Since semantic-release
uses the conventional commits format to automate the versioning, we need to make sure all commits in our repository follow the appropriate format.
For this purpose we are going to use commitlint and husky.
We'll leverage husky to add a Git hook that uses commitlint to check whether our commit message meets the conventional commit format, every time we commit.
Install commitlint
npm install -D @commitlint/cli @commitlint/config-conventional
add the commitlint config file into the project's root
commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional']
}
Install husky
npm install -D husky
Enable husky
npx husky init
The npx husky init
command sets up husky in the project by creating a pre-commit
script in .husky/
and inserting/updating the prepare
script in package.json
.
Add a hook to lint commits using commitlint before they are created, using husky's commit-msg
hook:
echo 'npx --no-install commitlint --edit "$1"' > .husky/commit-msg
Ready to publish
We've finished the setup and configuration of semantic-release in our GitHub repository. From now on we have to use the Conventional Commits specification for our commit messages.
For example, if our package is now at version 1.0.0, a commit message with this format:
fix(homepage): fixed image gallery
will bump the version to 1.0.1
feat(logging): added logs for failed signups
will bump the version to 1.1.0
That's all there's to it!
semantic-release and GitHub Actions will take care of the rest, determining the next version number, generating the release notes and publishing the package to npm.
Top comments (9)
Hi @kouts
Thanks a lot for your article, it was immensely helpful in my research.
I have brought together ideas from your article and others into a GitHub NPM starter project.
Kindly check it out and let me know what you think 🙏
Hi. I followed the steps as outlined above, but am experiencing an issue and searching online has not helped me find an answer. After completing the above, I committed my changes to the beta branch (next, in my case), and when I check my git log locally, I see:
However, when I push the branch to origin, my new Release action runs and fails with the following error:
Note that a long hash link is added to the end of my commit message, which causes the line to exceed the 100 character limit. Why is this being added and how can I avoid it?
I was able to fix the issue by overriding the the max length rule on the body and footer as follows:
This is gold! One more thing is that you have to ensure that the build directory (
dist
for instance) is not git ignored, if not you npm will ignore it too.You don't actually need to put the
dist
folder inside.gitignore
, @semantic-release/git options take care of adding thedist
folder to the release.Great article - thank you!
This is immensely useful, thank you very much.
Am I right in thinking the version in package.json should be updated?
Thanks @dlw, the version in
package.json
gets automatically populated by Semantic Release.For anyone who's interpreter who did not support the husky command above, you can see different versions here:
github.com/conventional-changelog/...