Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Automated Versioning with Conventional Commits

Hello! In our last lesson, we put the finishing touches on our library's API documentation by setting up TypeDoc. With a professional documentation site ready to be generated on demand, we're now approaching the final steps before our library is ready to be shared with the world.

This lesson focuses on a crucial aspect of modern software development: automating the release process. Manually bumping version numbers, writing changelogs, and creating Git tags is tedious and error-prone. We will solve this by adopting a professional workflow that is standard in many open-source projects.

You will learn to:

  1. Adopt the Conventional Commits specification, a widely-used standard for writing meaningful Git commit messages.
  2. Configure release-it, a powerful CLI tool that will read your commit history to automatically determine the correct version bump, generate a CHANGELOG.md file, and create the necessary Git tags.

By the end of this lesson, you'll have a fully automated, single-command release process.

1. The Foundation: Conventional Commits

Before we can automate anything, we need a consistent, machine-readable format for our Git commit history. This is where the Conventional Commits specification comes in. It's a simple set of rules for structuring your commit messages that allows tools to understand the nature of your changes.

To get a solid grasp of the specification, its structure, and its benefits, please start by reading the official documentation.

Conventional Commits

The official Conventional Commits website is the definitive source for understanding the specification. It clearly lays out the rules, provides examples, and explains the 'why' behind this standard.

Please read the 'Summary', 'Examples', 'Why Use Conventional Commits', and the 'How does this relate to SemVer?' question in the FAQ. This will give you a comprehensive overview of the core concepts.

Key Structure and Types

As you saw in the documentation, the core structure is:

type(scope): description

The type is the most important part for automation. It communicates the intent of your change and directly maps to Semantic Versioning (SemVer):

  • feat: A new feature. This corresponds to a MINOR version bump (e.g., 1.2.0 -> 1.3.0).
  • fix: A bug fix. This corresponds to a PATCH version bump (e.g., 1.2.0 -> 1.2.1).
  • BREAKING CHANGE: A change that breaks backward compatibility. This can be indicated by adding a ! after the type/scope (e.g., feat(api)!:) or by adding a BREAKING CHANGE: footer in the commit body. This corresponds to a MAJOR version bump (e.g., 1.2.0 -> 2.0.0).

Other types like docs, chore, refactor, test, etc., are also common. By default, these do not trigger a version bump, but they can be configured to appear in the changelog.

This short video provides a great visual summary of the commit structure.

Write git commit messages like a PRO with Conventional Commits

This video from the Justin Brooks channel gives a quick, practical overview of the five parts of a conventional commit message.

Watch the first two sections, 'Introduction to Conventional Commits' (00:29 - 01:19) and 'Structure of a Conventional Commit' (01:19 - 02:53). Focus on how the type, scope, description, body, and footer work together.

Test your understanding!

For each of the following code changes, write the header (the first line) of an appropriate conventional commit message.

  1. You fixed a memory leak in the Subscription class.
  2. You added a new distinctUntilChanged operator.
  3. You refactored the internal structure of the Subject class, which changes its public API for creating instances.
Show answer
  1. fix(subscription): resolve memory leak on teardown
    • type: fix, because it's a bug fix.
    • scope: subscription, to specify which part of the codebase is affected.
  2. feat(operators): add distinctUntilChanged operator
    • type: feat, because it's a new feature.
    • scope: operators, as you're adding to the collection of operators.
  3. refactor(subject)!: overhaul internal state management
    • type: refactor, as the primary purpose is restructuring code.
    • scope: subject, to identify the component.
    • !: The exclamation mark signifies a BREAKING CHANGE, which is crucial since the public API was altered.

2. Automating the Release with release-it

Now that we have a standard for our commit history, we can use a tool to automate our releases. We'll use release-it, a flexible and powerful CLI for this purpose.

The article "Release-it — Automation for versioning and changelog" provides an excellent walkthrough of the setup process. We will follow its guidance.

Release-it — Automation for versioning and changelog

This article by Rasmus P. is a practical guide to setting up release-it with conventional commits, which is exactly our goal.

Read the sections from the start through 'Conventional Changelog Configuration'. This will cover the motivation, installation, and the exact configuration we need for the changelog plugin.

Step 1: Installation

Following the article's instructions, let's install release-it and its conventional changelog plugin as development dependencies.

npm install --save-dev release-it @release-it/conventional-changelog

Next, add a release script to your package.json:

// package.json
{
  // ...
  "scripts": {
    // ... your other scripts
    "release": "release-it"
  }
}

Step 2: Configuration

Create a configuration file named .release-it.json in the root of your project. This keeps your release configuration separate from package.json.

Here is a recommended configuration to start with. It configures Git integration (tagging and pushing) and the conventional changelog plugin.

// .release-it.json
{
  "git": {
    "commitMessage": "chore(release): v${version}",
    "tagName": "v${version}"
  },
  "github": {
    "release": true
  },
  "plugins": {
    "@release-it/conventional-changelog": {
      "preset": {
        "name": "conventionalcommits",
        "types": [
          { "type": "feat", "section": "Features" },
          { "type": "fix", "section": "Bug Fixes" },
          { "type": "perf", "section": "Performance Improvements" },
          { "type": "refactor", "section": "Code Refactoring" },
          { "type": "docs", "hidden": true },
          { "type": "test", "hidden": true },
          { "type": "chore", "hidden": true },
          { "type": "style", "hidden": true },
          { "type": "ci", "hidden": true },
          { "type": "build", "hidden": true }
        ]
      },
      "infile": "CHANGELOG.md"
    }
  }
}

Configuration Breakdown:

  • git: Configures how release-it interacts with Git. We've set a conventional commit message for the release commit itself.
  • github: Tells release-it to create a GitHub Release, which is a great way to present your changelog to users.
  • plugins: This is where we configure the @release-it/conventional-changelog plugin.
    • preset.name: We use the conventionalcommits preset, which understands the feat/fix/! mapping to SemVer.
    • preset.types: This array configures which commit types appear in the changelog. We've included sections for Features, Bug Fixes, Performance Improvements, and Code Refactoring. Other common types like docs and test are set to hidden: true to keep the changelog focused on user-facing changes.
    • infile: Specifies the name of the changelog file.

3. Performing a Release

With everything configured, the release process is now reduced to a single command.

Step 1: Commit Your Changes Conventionally

Before running a release, make sure all your work is committed to your main branch using conventional commit messages. For example:

# Make some changes...
git add .
git commit -m "fix(core): ensure observer.complete is always called"

# Make more changes...
git add .
git commit -m "feat(operators): add 'filter' operator"

Step 2: Run a Dry Run

It's always a good practice to preview what a command will do before executing it for real. release-it provides a --dry-run flag for this. It will simulate the entire process without writing any files or pushing to Git.

npm run release -- --dry-run

You will see an interactive prompt asking you to select the version. Based on our example commits (fix and feat), release-it will correctly recommend a minor version bump. After you select it, the tool will output the pending changes, including the changelog entries it's about to generate.

Step 3: Run the Real Release

Once you are satisfied with the dry run, execute the command for real.

npm run release

release-it will now perform the following steps automatically:

  1. Determines the new version number based on your commits.
  2. Updates the version field in package.json.
  3. Generates or updates CHANGELOG.md with the new version and its corresponding commits, grouped by section.
  4. Commits package.json and CHANGELOG.md with the message chore(release): vX.Y.Z.
  5. Creates a Git tag for the new version (e.g., vX.Y.Z).
  6. Pushes the commit and the tag to your remote repository.
  7. Creates a new Release on GitHub with the changelog contents.

Your release is done!

Automating More with Hooks

release-it also supports hooks to run custom commands at different stages of the release process. For example, you could ensure all tests pass and the project builds before a release starts.

You would add a hooks section to your .release-it.json:

// .release-it.json
{
  // ... other config
  "hooks": {
    "before:init": ["npm test", "npm run build"]
  }
}

This before:init hook will run npm test and npm run build at the very beginning. If any of these commands fail, the release process is aborted. This is a powerful way to protect your releases from accidental errors.

Conclusion

You have successfully automated your library's release workflow. By combining the discipline of Conventional Commits with the power of release-it, you have eliminated manual chores and ensured that every release is consistent, well-documented, and follows Semantic Versioning principles. This is a significant step in professionalizing your project and preparing it for public consumption.

Key Takeaways:

  • Conventional Commits provide a machine-readable standard for your Git history (feat -> MINOR, fix -> PATCH, ! -> MAJOR).
  • release-it is a tool that automates the entire release process: version bumping, changelog generation, Git tagging, and creating GitHub releases.
  • Configuration is managed in a .release-it.json file, where you can define Git behavior and configure plugins like @release-it/conventional-changelog.
  • Using --dry-run is a best practice to preview changes before applying them.
  • Hooks allow you to integrate other scripts (like tests and builds) directly into the release lifecycle.

Next Up:

Our library is now packaged, documented with TypeDoc, and has an automated release process. The final piece of the puzzle is the front door to your project: the README.md file. In the next lesson, we will focus on writing a comprehensive README that tells potential users what your library is, why they should use it, and how to get started.

Can't find a good explanation? Sign up and we'll make it for you

Sign up