Automated version management and changelog

In my twelve-factors methodology blog post I have mentioned Semantic Versioning (SemVer) which defines a set of rules on how the version should be incremented on new changes. A few weeks later I have been searching for tools which can generate changelog from commits and bump up the version in the package.json file for me. Unsurprisingly, there are many packages available for this and semantic-release stands out of the crowd and offers many features beyond what I am after. A rough workflow is as follows:

  1. Commit new changes to the dev branch. Merge new features into the main branch.
  2. CI/CD pipeline runs all tests on new codes.
  3. Semantic-release package analysis commit messages, increment the version number in the package.json file accordingly, update the changelog file, and make a new commit to include all files that have changed.
  4. Semantic-release package creates a new release on GitHub/GitLab.
  5. A new production is built and deployed.

At the moment you can have automated releases pushed to NPM, GitHub, and GitLab, or all of them if you wish to. In this post, I will give a simple guide on how to set up this package in GitHub actions and GitLab CI/CD pipeline for any JavaScript project.

Prerequisite

  • You need to have the package manager npm installed.
  • A GitHub/GitLab account.
  • You are committed to write commit messages that follows certain convention, e.g. Conventional commits, Angular.

GitHub

The following steps will help you install and configure the semantic-release package for use in GitHub actions.

  1. Install the package using npm install --save-dev semantic-release.

  2. Install extra plugins:

    npm install @semantic-release/git @semantic-release/changelog conventional-changelog-conventionalcommits -D
  3. Add a new file called release.config.js to the root directory containing the following (see this [link] for detailed configuration):

    module.exports = {
      defaultBranch: 'main', // change this to match your repo
      branches: [
        '+([0-9])?(.{+([0-9]),x}).x',
        'main',
        'next',
        'next-major',
        { name: 'beta', prerelease: true },
        { name: 'alpha', prerelease: true },
      ],
      plugins: [
        [
          '@semantic-release/commit-analyzer',
          {
            preset: 'conventionalcommits', // see the official manual for more details
            releaseRules: './releaseRules.js', // custom release rules
            parserOpts: {
              noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'],
            },
          },
        ],
        [
          '@semantic-release/release-notes-generator',
          {
            preset: 'conventionalcommits',
            presetConfig: {
              types: [
                // map commit type to sections in the changelog.
                // If your commit message does not include any of these, it will not but included in the changelog
                {
                  type: 'feat',
                  section: '✨ Features',
                  hidden: false, // whether to show this type of changes in the changelog
                },
                {
                  type: 'fix',
                  section: '🐛 Bug Fixes',
                  hidden: false,
                },
                {
                  type: 'docs',
                  section: '📝 Documentation',
                  hidden: false,
                },
                {
                  type: 'style',
                  section: '🎨 Styles',
                  hidden: false,
                },
                {
                  type: 'refactor',
                  section: '♻️ Code Refactoring',
                  hidden: false,
                },
                {
                  type: 'perf',
                  section: '⚡️ Performance Improvement',
                  hidden: false,
                },
                {
                  type: 'test',
                  section: '✅ Testing',
                  hidden: false,
                },
                {
                  type: 'build',
                  section: '🔨 Build/Dependencies',
                  hidden: false,
                },
                {
                  type: 'ci',
                  section: '🔧 Continuous Integration',
                  hidden: false,
                },
                {
                  type: 'chore',
                  hidden: true,
                },
              ],
            },
          },
        ],
        [
          '@semantic-release/changelog',
          {
            changelogFile: 'CHANGELOG.md', // location of the changelog
          },
        ],
        [
          '@semantic-release/npm',
          {
            npmPublish: false, // disabled if you are not publish package to NPM
          },
        ],
        '@semantic-release/github',
        [
          '@semantic-release/git',
          {
            assets: [
              // files to include with the release commit
              'CHANGELOG.mdx',
              'package.json',
              'package-lock.json',
              'npm-shrinkwrap.json',
            ],
          },
        ],
      ],
    };

    Add another file releaseRules.js containing all release rules:

    module.exports = [
      { type: 'feat', release: 'minor' },
      { type: 'fix', release: 'patch' },
      { type: 'perf', release: 'patch' },
      { type: 'docs', scope: 'new-*', release: 'minor' }, // e.g. docs(new-blog): my new blog post
      { type: 'docs', release: 'patch' },
      { type: 'refactor', scope: 'core-*', release: 'minor' }, // e.g. refactor(core-utils): modify ... file
      { type: 'refactor', release: 'patch' },
      { type: 'style', release: 'patch' },
      { type: 'test', release: 'patch' },
      { type: 'build', release: 'patch' },
      { type: 'ci', release: 'patch' },
      { type: 'chore', release: 'patch' },
      { breaking: true, release: 'major' },
      { revert: true, release: 'patch' },
      { scope: 'no-release', release: false },
    ];

    Note that the version number is expressed as major.minor.patch.

  4. Run npx semantic-release --dry-run (or -d) to do a dry-run which skips the prepare, publish, success and fail steps, and to get a preview of the pending release. In this step you will get an error something like SemanticReleaseError: No GitHub token specified. The reason is obvious, we have included the plugin @semantic-release/github but didn’t pass a GitHub personal token. The semantic-release package is supposed to be used and run in the CI pipeline after all tests are passed. To pass a GitHub token simply run GH_TOKEN=<your_github_token> npx semantic-release -d.

    To actually making releases from a local machine (not recommended) you can run GH_TOKEN=<your_github_token> npx semantic-release --no-ci (or --ci false) to skip Continuous Integration environment verifications.

  5. Add npx semantic-release to your GitHub Actions deployment workflows:

- name: Semantic Release
  run: npx semantic-release

Some tips

  • Make sure the repo url in the package.json file matches the one you are working on to avoid pushing to a different repository.
  • The latest tag must be in the format of vx.x.x, otherwise the version number will start from v1.0.0.
  • If you are using the husky package, add HUSKY=0 before npx semantic-release to disable it in CI.
  • To set the author of releases, pass the built-in token {{ secrets.GITHUB_TOKEN }} to either GH_TOKEN or GITHUB_TOKEN variable in GitHub actions workflow files.
  • To set the author & committer details, include the following environment variables:
env:
  GIT_AUTHOR_NAME: 'github-actions-bot'
  GIT_AUTHOR_EMAIL: 'support+actions@github.com'
  GIT_COMMITTER_NAME: 'github-actions-bot'
  GIT_COMMITTER_EMAIL: 'support+actions@github.com'
  • If you want to push to protected branches, make sure you have included the following in the workflow:
- uses: actions/checkout@v3
  with:
  ref: master
  persist-credentials: false

...

- name: Semantic Release
  run: HUSKY=0 npx semantic-release
  env:
    GH_TOKEN: ${{ secrets.GH_TOKEN }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

// both GH token are included where GITHUB_TOKEN is the default one
...

Once you have included this package in the CI/CD pipeline, there is no more you need to do other than creating more fancy features to your applications!

GitLab

The set up for GitLab CI/CD is identical to GitHub except the following:

  • Install @semantic-release/gitlab and replace '@semantic-release/github' with '@semantic-release/gitlab' in the release.config.js file.
  • Add npx semantic-release to the .gitlab-ci.yml file.

Result

Click here to see an example of the end result, and the corresponding markdown file. In addition, if you have linked any issue or pull request in the commit message, the semantic-release package will also add a comment in the relevant issue/PR to say this is included in the new release. How exciting!

Other languages?

This package will also work with languages other than JavaScript, and there is an official guidance on this.