~ 11 min read

Introducing Changesets: Simplify Project Versioning with Semantic Releases

share this story on
A comprehensive guide to adopting Changesets for semantic versioning and publishing packages in monorepos and non-monorepo projects.

As open source project maintainers, we often encounter challenges when managing project versions and releases. Keeping track of changes, ensuring proper versioning, and automating the release process can be time-consuming and error-prone. Thankfully, open-source tools have emerged to streamline this process, and one such tool is Changesets.

Another tool that enabled maintainers to automate their projects releases and tasks such as publishing a new npm package to the registry or creating a GitHub release is semantic-release.

In fact, semantic-release precedes changesets and I’ve been using it for many of my projects (npq, dockly to name a few) and it works great. It’s straight-forward to setup and configure, and it’s also very flexible and extensible. One of its main benefits is that versioning is done automatically based on the commit messages that are being pushed to the repository and follow the Conventional Commits specification.

Ushering a Change in a Monorepo Project

I’m not a heavy user of monorepos, but I do have one open source project which publishes several connected npm packages and was built as a monorepo with Lerna and Yarn Workspaces.

This project monorepo structure is used by lockfile-lint, which helps developers mitigate supply chain security risks by detecting a lockfile that has been tampered with, also known as a lockfile injection.

Lerna and Yarn had both been great tools but they’ve been outdated in the monorepo. Lerna also had a lot of issues with its maintenance (call for maintainers?) which ended up as being deprecated and later on revived by the community with the help of Nx. Yarn, despite being a great tool by an awesome maintainer, had also its share of big project structures: Yarn, Yarn 2, Yarn 3.

And so with that, and the fact that lockfile_lint receives more than 320,000 downloads a month I went shopping for new, modern tooling to help manage project build, versions, and releases.

Changesets to the Rescue of Monorepos

While I was happy with semantic-release and still using it for non-monorepo projects, there’s no native monorepo support for it. This is where Changesets shines. It addresses the complexities of managing releases within a monorepo, providing a simple and efficient solution.

What is Changesets? Changesets is an open-source project versioning tool that focuses on automating the release process using semantic versioning. It provides a structured way to manage changes and versioning for monorepos, making it easier to maintain and release multiple packages within a single repository.

With Changesets, you can define a set of changes and updates for each package in your monorepo. These changes can include bug fixes, new features, performance improvements, or any other modifications. Changesets allows you to manage these changes and automatically determine the appropriate versioning for each package based on the updates.

How Changesets is different:

  • Monorepos: Changesets is specifically designed for managing releases in monorepos. It helps you handle the complexities of versioning across multiple packages within a single repository.

  • Semantic releases but loosely coupled: Semantic versioning is an important concept, but with semantic-release, the version is tightly coupled to the commit messages. Changesets decouples the versioning from the commit messages, allowing you to define changes and updates separately from the commits. This is by far, the biggest change in mindset when choosing Changesets over semantic-release.

Just as with semantic-release, Changesets will seamlessly integrate with your CI/CD pipeline, whether GitHub Actions or others, allowing you to automate the process of versioning and releasing your packages. However, due to its process of requiring a “changeset” to be specified, it does require a manual intervention that triggers the release.

The Core of Changesets: The Changeset

At the heart of Changesets lies the concept of a changeset. A changeset represents a set of changes or updates made to one or more packages within your monorepo. It captures the modifications, such as bug fixes, new features, or performance improvements, and provides the necessary information to determine the appropriate version for each package.

The Changeset Workflow

When working with Changesets, the typical workflow involves creating changesets, calculating new versions, and publishing the updated packages.

Changesets workflow

Let’s dive into each step of the workflow:

1. Creating Changesets

To create a changeset, you use the npx changeset command. This command opens an interactive prompt that allows you to select the packages you’ve made changes to and specify the type of change:

  • Patch: A patch represents bug fixes or minor updates that do not introduce any breaking changes. It is denoted by incrementing the patch version number (e.g., 1.0.1 -> 1.0.2).

  • Minor: A minor change includes new features or enhancements that are backward-compatible. It increments the minor version number (e.g., 1.0.1 -> 1.1.0).

  • Major: A major change indicates significant updates that introduce breaking changes. It increments the major version number (e.g., 1.0.1 -> 2.0.0).

Additionally, you provide a summary of the changes made in the changeset, helping to document and communicate the modifications effectively.

2. Determining New Versions

Once you have created changesets for your packages, the next step is to determine the new versions for each package. This is where Changesets shines. It analyzes the changesets and intelligently calculates the appropriate version for each package, considering the type of change and the existing version.

Changesets follows semantic versioning principles, which helps ensure that the versioning is consistent and meaningful. By automating this process, Changesets eliminates the need for manual versioning and reduces the chances of human error.

3. Publishing Updated Packages

After the new versions have been determined, you can proceed to publish the updated packages. Changesets provides a command, npx changeset publish, which automates the process of publishing the packages to your configured package registry, such as npm.

This command takes care of updating the package.json files of the affected packages with the new versions. It also creates Git tags for the releases, allowing you to track and reference them easily.

Benefits of the Changeset Approach

The changeset approach in Changesets brings several benefits to your versioning and release management workflow:

  • Granular Control: By defining changes at the package level, Changesets allows for granular control over versioning. Different packages can have different versions based on their respective changes, providing flexibility and precision.

  • Clear Documentation: Changesets encourage developers to provide summaries of their changes, helping document modifications effectively. This documentation serves as a valuable resource for the project and facilitates collaboration within teams.

  • Consistent Versioning: Changesets follow semantic versioning principles, ensuring that version numbers convey the significance of the changes made. This consistency improves communication, compatibility, and the overall stability of your packages.

  • Automated Release Process: Changesets automates the versioning and release process, reducing manual effort and the likelihood of human error. With a few simple commands, you can calculate new versions and publish the updated packages seamlessly.

  • Easy Collaboration: By providing a standardized approach to managing changes, Changesets facilitates collaboration among team members. Everyone can easily understand and work with the changesets, ensuring a shared understanding of the project’s evolution.

The changeset-centered workflow in Changesets brings clarity, efficiency, and reliability to your versioning and release management process.

Getting Started with Changesets

Now that you have a good understanding of Changesets, let’s explore how you can get started with it in a project.

Installation

To start using Changesets, you’ll need to install it as a development dependency. Open your terminal and navigate to your project directory. Run the following command to install Changesets using npm:

npm install --save-dev @changesets/cli

I also want to have my releases published to GitHub, so if you want that too, you should also continue to install the GitHub plugin:

npm install --save-dev @changesets/changelog-github

Configuration

Once Changesets is installed, you’ll need to set up the configuration for your project. Changesets provides an easy way to initialize the necessary files and directories.

Run the following command to initialize Changesets:

npx changeset init

This command will create the .changeset directory in your project, which will store your changesets. It also creates a .changeset/config.json file, which contains the configuration for your project.

Following is an example of a Changesets configuration file for a project that is hosted on GitHub at https://github.com/lirantal/astro-keyboard-controls:

{
  "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
  "changelog": [
    "@changesets/changelog-github",
      {
        "repo": "lirantal/astro-keyboard-controls"
      }
  ],
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

Creating Changesets

You create a changeset when you want to release a new version of your package. This might be after several pull requests have been made to the main branch, or within a pull request itself. Either way though, the changeset needs to be created.

To create a changeset, use the following command:

npx changeset

This command will open an interactive prompt that guides you through the process of defining changes for each package in your monorepo. You can select the packages, specify the type of change (patch, minor, or major), and provide a summary of the changes.

Versioning and Publishing

Once you have defined your changesets, it’s time to determine the new versions for your packages and publish them. Changesets offers a command that automatically calculates the appropriate versions and prepares your packages for release. This “versioning resolution” process is entirely automated by Changesets and doesn’t require you to manually intervene.

Use the following command to version and publish your packages:

npx changeset version

This command will calculate the new versions based on your changesets and update the package.json files of the affected packages. Additionally, it will create new Git tags for the releases.

To publish your packages on npm, run the following command:

npx changeset publish

Automating Changesets and Package Releases with GitHub Actions CI/CD

The manual part with using the Changesets paradigm is that you need to create a changeset for each release. However, once created, the versioning and release processes can be entirely automated.

In the following example, we’ll use GitHub Actions to automate the package versioning and publishing. We’ll use the official changesets/action GitHub Action, which basically does the following:

  • Detects if there are any changesets to be published (that are stored in the .changeset directory of the repository. Yes, they need to be committed to the repository in-case you missed that).
  • If there are changesets, it will run the version command to update the semantic version on packages in this repository, altering their package.json field and next continue to run the publish command which will publish the packages to the configured package registry (e.g. npm).

Here’s the YAML configuration for the above described GitHub Actions workflow:

name: release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

permissions: {}
jobs:
  release:
    permissions:
      contents: write       # to create release (changesets/action)
      issues: write         # to post issue comments (changesets/action)
      pull-requests: write  # to create pull request (changesets/action)
    timeout-minutes: 20
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18.x
      - name: install dependencies
        run: npm ci
      - name: Create Release Pull Request or Publish to npm
        uses: changesets/action@v1
        with:
          publish: npm run release
          version: npm run version
          commit: "chore: new release"
          title: "chore: new release candidate"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Don’t forget to create a granular npm token for this package to be published via CI/CD and update the repository settings with the NPM_TOKEN secret. Also update your npm run scripts in package.json to include the changeset commands:

{
  "scripts": {
    "version": "changeset version",
    "release": "changeset publish"
  }
}

How does it work in terms of processes and workflows?

  • The above workflow creates a pull request for each release. You can see an example of a pull request that was created by the workflow here in my lockfile-lint repository
  • The GitHub Action that initiated the pull request determined the version, updated the package.json files accordingly, and also removed the changeset file itself from the repository (this is configurable, and I opted out of keeping them in the repository).
  • Once this pull request is merged, the same GitHub Action workflow is triggered again, but now it determines that package.json files have changed (also known as a version drift) from what appears in the npm registry and it publishes the new versions and creates a GitHub release tag. You can see an example of this workflow here.

Summary

Changesets provides an elegant solution for managing project releases in monorepo architectures. By embracing semantic versioning principles and offering an intuitive command-line interface, Changesets simplifies the process of versioning and ensures consistent release management across your packages.

In this blog post, we explored what Changesets is, its common use cases, and how it can benefit your project. We also provided a step-by-step guide to getting started with Changesets, from installation to creating changesets and publishing new versions.

If you’re working with monorepos or collaborating on large-scale projects, give Changesets a try. It will save you time and effort in managing releases and help you maintain a robust versioning workflow for your packages.

Happy publishing!