Recommended resources
The freely available resources this lesson will direct you to.
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:
- Adopt the Conventional Commits specification, a widely-used standard for writing meaningful Git commit messages.
- Configure
release-it, a powerful CLI tool that will read your commit history to automatically determine the correct version bump, generate aCHANGELOG.mdfile, 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.
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 aBREAKING 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.
- You fixed a memory leak in the
Subscriptionclass. - You added a new
distinctUntilChangedoperator. - You refactored the internal structure of the
Subjectclass, which changes its public API for creating instances.
Show answer
fix(subscription): resolve memory leak on teardowntype:fix, because it's a bug fix.scope:subscription, to specify which part of the codebase is affected.
feat(operators): add distinctUntilChanged operatortype:feat, because it's a new feature.scope:operators, as you're adding to the collection of operators.
refactor(subject)!: overhaul internal state managementtype: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 howrelease-itinteracts with Git. We've set a conventional commit message for the release commit itself.github: Tellsrelease-itto 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-changelogplugin.preset.name: We use theconventionalcommitspreset, which understands thefeat/fix/!mapping to SemVer.preset.types: This array configures which commit types appear in the changelog. We've included sections forFeatures,Bug Fixes,Performance Improvements, andCode Refactoring. Other common types likedocsandtestare set tohidden: trueto 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:
- Determines the new version number based on your commits.
- Updates the
versionfield inpackage.json. - Generates or updates
CHANGELOG.mdwith the new version and its corresponding commits, grouped by section. - Commits
package.jsonandCHANGELOG.mdwith the messagechore(release): vX.Y.Z. - Creates a Git tag for the new version (e.g.,
vX.Y.Z). - Pushes the commit and the tag to your remote repository.
- 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-itis 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.jsonfile, where you can define Git behavior and configure plugins like@release-it/conventional-changelog. - Using
--dry-runis 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.