How to setup auto semantic-release

Also on:
Cover image

Semantic Versioning

Semantic Versioning (AKA SemVer) is a convention for package versions. Since it is the most used convention (AFAIK, I don't know about others...), it is well documented and widely used around the open-source, so I'm not going to describe it. You can read more here.

Semantic Release

Semantic Release is a tool to implement SemVer automatically in a project.

Fully automated version management and package publishing from Semantic Release docs.

In this post, I want to share with you the process of adding and configuring the semantic-release tool, because I had to collect the information from multiple sources, even from the source code of the tool.

Let's start.

Prerequisites

In this post, I assume you have

  • a NodeJS project (with a package.json file)
  • in a Github repository
  • with basic knowledge in Github Actions
  • and fearlessly use CLI and read its output

Command Line

The semantic-release feels like a great tool, and it does a lot of things by default, and does them right.

The first thing you want to do is to run the tool, just like that. Don't worry, it will identify that you're not in a CI environment, and it will automatically run on dry mode.

In your project folder, run npx semantic-release, it will print some beautifully organized output. Read that output.

First of all, it failed because of missing tokens, follow the links inside the errors to create the tokens.

It will not create a version (dry run), and you expect one of the next messages:

There is no previous release, the next release version is 1.0.0

If you never published a release before (and release means git tag), semantic-release will create a default version 1.0.0 in any way.

configured to only publish from master

The whole line:

This test run was triggered on the branch foo, while semantic-release is configured to only publish from master, therefore a new version won’t be published.

We will see the semantic-release is triggering a version when pushing to specific branches. If you're not in the master branch, the semantic-release will not process your git history to create a version.

There are no relevant changes, so no new version is released.

Maybe you see messages about Analyzing commit because you committed some changes since your last version, but eventually, the semantic-release didn't find a commit message in a pattern to create a new release.

We will touch on the commit messages pattern later.

Published release 1.1.0 on default channel

OK, but it did nothing because, with dry-run mode, all the actions were skipped, this is just a completion message.

Commit Format

We ran the semantic-release tool with the default configurations. Before any other configuration adjustment, the most important thing you may ask is, why is semantic-release analyzing my commits but There are no relevant changes?

That is because semantic-release uses commit message format to decide if and what version should be bumped.

Try to commit with the message perf(pencil): remove graphiteWidth option and see if now it's deciding to bump your version.

Configuration

I saw the default commit format (Angular Conventions) and I felt it will add overhead and will be less readable, and it is not worth the time I'm saving with automatic releasing. I wanted to change the commit format.

To do that, we need to configure the semantic-release, and since any configuration section will override the default configuration we used so far, I think the best idea is first to configure the default configuration explicitly, and then start to adjust it.

The default configuration for the relevant sections (in release.config.js format) is:

module.exports = {
  branches: [
    "+([0-9])?(.{+([0-9]),x}).x",
    "master",
    "next",
    "next-major",
    {
      name: "beta",
      prerelease: true,
    },
    {
      name: "alpha",
      prerelease: true,
    },
  ],
  plugins: [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/npm",
    "@semantic-release/github",
  ],
};

Branch Configuration

The first configuration I wanted to change is the branches. For me and for now, I just need to trigger the release on the master branch, you can see how simple is the, just branches: ['master'] instead of the current config.


So far so good?

You have that configuration, you may push and start using automatic release. What else?

For me, I wanted to take it few steps forward, to

  • Use more convenient and readable commit messages.
  • Set the commit message just before finishing the PR, and not on each commit.
  • Manage a CHANGELOG file.
  • Bonus: Tweet about the new version

Let's continue.

Custom Commit Message Convention

The default plugin @semantic-release/commit-analyzer is responsible to analyze the commit message, and it is configured by two settings:

  1. The convention (preset)
  2. The rules (releaseRules)
  3. (Other options: parserOpts)

I decided to follow the ESLint convention:

# Format:

Tag: Short description (fixes #1234)

# Examples:

Build: Update Travis to only test Node 0.10 (refs #734)
Fix: Semi rule incorrectly flagging extra semicolon (fixes #840)
Upgrade: Esprima to 1.2, switch to using comment attachment (fixes #730)

This format is better, right?

To use this format, we first need to select eslint as our parser, but surprise! It is not coming with all its default available tags (the Build, Fix, Upgrade and more), and instead, the @semantic-release/commit-analyzer will handle only a four tags:

{tag: 'Breaking', release: 'major'},
{tag: 'Fix', release: 'patch'},
{tag: 'Update', release: 'minor'},
{tag: 'New', release: 'minor'},

You can keep it with this default, or if you want to add more tags, you can add your custom object to replace the default one.

For me, the plugins are now:

plugins: [
  [
    "@semantic-release/commit-analyzer",
    {
      preset: "eslint",
    },
  ],
  [
    "@semantic-release/release-notes-generator",
    {
      preset: "eslint",
    },
  ],
  "@semantic-release/npm",
  "@semantic-release/github",
];

Note that I had to update the @semantic-release/release-notes-generator settings since it is also analyzing the commits to generate the release notes.

And finally, I documented the default tags in my repo, just to easily find them if I forgot.

Conventional PullRequest title

For now, you expect the contributors to use the conventional commit messages (and you may use commitlint). But you don't have to force it.

First of all, you can keep it optional, and if you want to trigger a new release, make sure that one of the commits in a PR is in the convention.

In my repos, I'm using two conventions, side by side:

Merge Commit

If the Pull Request contains more than one change (and of course, in multiple commits), although it is not best practice, you know, a small repo without contributors... Anyway, in that case, I'm looking at the commits to see if there are conventional commits there, and if so, I'm merging the PR, and the commits will be in the master branch and will trigger the release.

Squash Commits

If the PR is only about one change, we don't have the commits to be conventional. Instead, we can change the pull request title to be conventional, and squash the pull request to be only one commit in master. In Github, by default, the pull request title becomes to be the commit message in squash merge.

To help me to remember that, I created a Github Actions workflow to validate the pull request title and comment on the PR if the title does not match my convention.

name: PR Title

on:
  pull_request:
    types: [opened, edited, synchronize, reopened]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: deepakputhraya/action-pr-title@3864bebc79c5f829d25dd42d3c6579d040b0ef16
        name: Validate PR Title
        continue-on-error: true
        with:
          regex: '\w+:( [\w\.,]+)+( \(\w+ #\d+\))?' # Regex the title should match.
          # allowed_prefixes: 'feature,fix,JIRA' # title should start with the given prefix
          # disallowed_prefixes: 'feat/,hotfix' # title should not start with the given prefix
          # prefix_case_sensitive: false # title prefix are case insensitive
          # min_length: 5 # Min length of the title
          max_length: 50 # Max length of the title
          github_token: ${{ secrets.GITHUB_TOKEN }} # Default: ${{ github.token }}
      - uses: mshick/add-pr-comment@07f690343c25a94e24a8acb70d03c86b701ae322
        name: Comment on PR
        if: ${{ failure() }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          message: |
            Please fix the title of this PR.

            Example: `New: Add new feature`

            See more in `/docs/RELEASE.md`.

I compared a lot of optional actions to do this validation and comment, and I'm not sure I selected the best. Please share with me your suggestions.

Manage a CHANGELOG file

There is a practice to manage a file with all the release notes, for all the versions, listed on it.

Unfortunately, I can't think about a beautiful way to update the CHANGELOG without adding another commit after the release.

To generate the CHANGELOG and commit it, we will add two (obvious) plugins to semantic-release: @semantic-release/changelog and @semantic-release/git.

plugins: [
  ...
  [
   "@semantic-release/changelog",
   {
    "changelogFile": "CHANGELOG.md"
   }
  ],
  [
   "@semantic-release/git",
   {
    "assets": ["CHANGELOG.md"],
    "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
   }
  ]
  '@semantic-release/npm',
  '@semantic-release/github'
]

Please note the plugins are running sequentially, and you have to put the changelog plugin before the git plugin.

This configuration will first update the CHANGELOG.md file in the local folder, and after that will commit it with a custom message, includes the [skip ci], to mark this commit as out of release process.

Bonus: Tweet about a new release

Now you have a fully working process to automate your release, and more than that, I hope you got enough information to continue to adjust the process to your needs.

One more thing I'm doing is to tweet about new releases, to implement, follow the instructions in ethomson/send-tweet-action.

If you want to get the new version number (or any other value) from semantic-release, you need to put it in Environment Variables, and you can do it with @semantic-release/exec:

plugins: [
  ...[
    "@semantic-release/exec",
    {
      successCmd: 'echo"SEMVER_VERSION=${nextRelease.version}" > $GITHUB_ENV',
    },
  ],
];

Summary

Finally, all my configuration files.


Update: while writing this post to document my process, I finally found another blog post that already documented it, you are welcome to use both :-).

Thanks to my teammate Leonid Weinberg who review the article to correct language errors.