Changesets, semver and independent releases in a monorepo
Without a structured versioning process, internal TypeScript packages end up either with a single global version number for the whole monorepo, or with hand written changelogs nobody keeps consistent. Changesets solves both problems with a declarative, review friendly workflow.
Table of Contents
- 1. Why a global version number fails in a monorepo
- 2. Enforcing semver discipline for internal packages
- 3. The Changesets workflow in detail
- 4. Fixed vs. independent versioning
- 5. Automatic changelog generation
- 6. CI integration: automatically creating release PRs
- 7. Versioning internal packages without npm publishing
- 8. Pre-releases and snapshot versions for test branches
- 9. Versioning strategies compared
- 10. Summary
- 11. FAQ
1. Why a global version number fails in a monorepo
A naive approach to versioning in a TypeScript monorepo is to give all packages the same global version number and bump it together on every release. This works in the short term, but quickly leads to unnecessary major version jumps for packages not affected by a change at all, and makes it impossible for consumers to tell whether a particular package actually changed, just because the version number went up.
The alternative, versioning every package completely independently by hand, fails on human consistency: developers forget to write a changelog entry, choose inconsistent semver levels for similar changes, or a breaking change accidentally gets published as a patch version. A growing TypeScript monorepo with many internal packages needs a process that enforces versioning decisions at the time of the code change, not only at release time.
Another downside of both naive approaches shows up during debugging: when an internal package in a TypeScript monorepo shows unexpected behavior, the first question is usually which version is actually installed and what changed since the last working version. Without structured version history and traceable changelogs, this actually simple question turns into time consuming archaeology through commit history and chat messages.
2. Enforcing semver discipline for internal packages
Semantic Versioning defines three levels: patch for backward compatible bug fixes, minor for backward compatible new features, and major for breaking changes. These rules sound simple but are regularly misapplied in practice, especially for internal packages in a TypeScript monorepo, where developers tend to treat versioning decisions as an afterthought because no external customer seems directly affected.
This assumption is dangerous, because internal consumers too, meaning other teams within the same organization, rely on correct semver signals to decide whether an update can be safely applied automatically. A breaking change incorrectly declared as minor breaks automated dependency updates just as it would for a public library. Changesets enforces this discipline by requiring the semver level to be explicitly stated with every change, instead of leaving it implicitly to the release owner.
An often overlooked aspect of semver discipline concerns internal type definitions themselves: if an exported TypeScript type in a package changes, for example a field turning from optional to required, that is a breaking change for every consumer, even if the package's runtime logic did not change. These purely type related breaking changes are especially often underestimated in a TypeScript monorepo, because they do not show up during manual testing of runtime functionality.
3. The Changesets workflow in detail
The core of Changesets is a simple idea: instead of changing version numbers directly, a developer creates a small markdown file in the .changeset directory for every relevant code change, describing which packages are affected, which semver level is appropriate, and what actually changed functionally. This file gets submitted together with the code changes in the same pull request and goes through the same review process.
The command npx changeset walks through this process interactively: it asks which packages in a TypeScript monorepo are affected by the current change, which semver level applies to each package, and lets the developer type a short description that later gets picked up automatically into the changelog. Because this file is part of the pull request, the reviewer sees the versioning decision directly in the context of the actual code change, instead of blindly trusting it later.
# Initialize Changesets in the monorepo
npx changeset init
# Create a new changeset for the current change
npx changeset add
# Apply all open changesets into real version bumps
npx changeset version
# Actually publish the affected packages
npx changeset publish
# Check the status of open changesets before building a release
npx changeset status --verbose
# Check whether the current change is missing a changeset at all
npx changeset status --since=main
In practice it pays off to add a CI check that flags a pull request when code files in a package changed but no matching changeset exists. This simple guard prevents the most common omission in the whole workflow: a functional change that slips through with no versioning decision at all and only gets caught, incorrectly, with the wrong level at the next release.
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"access": "restricted",
"baseBranch": "main",
"ignore": ["@myorg/internal-docs"]
}
4. Fixed vs. independent versioning
Changesets supports two fundamental versioning modes. In fixed mode, several packages are grouped together and receive the same version number on every release, similar to a classic framework with many tightly coupled core packages. In independent mode, the default behavior, every package in a TypeScript monorepo versions completely independently, according to the changesets actually submitted for that package.
For most TypeScript monorepos, independent versioning is the better fit, because it precisely reflects which packages actually changed and avoids unnecessary version jumps for unchanged packages. Fixed versioning, on the other hand, pays off for packages that are conceptually always meant to be released together as a unit, for example a core package and its official plugin packages that must always stay compatible with each other at the same version.
{
"fixed": [["@myorg/core", "@myorg/core-plugin-auth", "@myorg/core-plugin-cache"]],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
A mixed setup is also possible: most packages of a TypeScript monorepo run in independent mode, while only a small, clearly bounded group of tightly coupled core packages gets grouped through the fixed array. This combination covers most real world package structures in practice, without a team having to commit the entire repository to a single mode.
5. Automatic changelog generation
A frequently underestimated advantage of Changesets is automatic changelog generation. When running changeset version, all open changeset files are collected, grouped by affected package, and written into a formatted CHANGELOG.md file for each package in the TypeScript monorepo. The description texts come directly from the changeset files developers wrote when creating the change, keeping the changelog automatically up to date and consistently formatted.
This automation prevents the classic problem of hand maintained changelogs, where entries get forgotten or are hastily written together just before a release. With a customized changelog generator, the format can additionally be extended with links to pull requests, commit hashes or internal ticket references, which significantly improves traceability for other teams, especially with internal packages.
For a TypeScript monorepo with several consuming teams, it also pays off to extend the changelog format with a short migration notes section that gets automatically linked for major versions. That way, a team updating an internal package immediately finds the relevant guidance, without having to ask in chat what actually changed and which code adjustments are needed.
6. CI integration: automatically creating release PRs
In practice, the Changesets workflow runs through a GitHub Action called changesets/action, which checks on every merge into the main branch whether open changeset files exist. If any are present, the action automatically creates a release pull request that already contains the computed version changes and updated changelogs. Merging this special pull request then triggers the actual publish step for all affected packages in the TypeScript monorepo.
This two step process, first a collecting PR, then the actual publish, gives teams the ability to bundle several small changes into a single, manageable release, instead of publishing a new version immediately on every single merge. For internal packages that change frequently, this prevents unnecessary version inflation and makes it easier for consumers to apply updates in manageable, thematically coherent steps.
Teams that instead want to publish every change immediately, for example for very small, isolated utility packages in a TypeScript monorepo, can also skip the collecting step by having the CI pipeline run changeset version and changeset publish back to back right after every merge. This mode only fits, however, if releases are actually low risk enough to skip an additional human review step before publishing.
7. Versioning internal packages without npm publishing
A misconception that frequently comes up with Changesets: you do not need a public npm registry to benefit from it. For purely internal packages consumed only within the TypeScript monorepo via workspace symlinks, changeset publish can instead run against a private registry such as Verdaccio or GitHub Packages, or the publish step can be skipped entirely, keeping only the version bumps and changelogs.
In this restricted mode, Changesets serves purely as a structured process for versioning decisions and changelog maintenance, with no actual npm publish ever taking place. This is especially useful for organizations that never want to expose their internal TypeScript packages outside their own infrastructure, but still want to benefit from traceable version history and automatically generated changelogs.
If a private registry is used anyway, it pays off to add a CI step that checks after every publish whether the published version is actually installable, before the release gets marked as complete. This simple smoke test prevents a misconfigured exports field or a forgotten build file from silently ending up in a release marked as successful.
It always matters that the version number in package.json and the range referenced via the workspace: protocol in dependent packages stay consistent with each other. A major release that requires a narrower version range in workspace:^ should always ship together with the corresponding adjustments in dependent internal packages of the same TypeScript monorepo.
8. Pre-releases and snapshot versions for test branches
For feature branches that need testing before merging into the main branch, Changesets offers a pre release mode via changeset pre enter next. This mode appends a pre release tag like -next.0 to every computed version, so test installations stay clearly distinguishable from regular releases without polluting the regular version history of a package in the TypeScript monorepo.
For even shorter lived testing purposes, for example quickly verifying a single pull request change, the Changesets ecosystem additionally offers snapshot releases, which produce a unique version number tied to the commit without affecting the regular versioning flow or changelog history. These snapshot versions are excellent for previewing an internal package in another application before the actual change has even been merged.
It matters to never run pre release modes and snapshot versions in the same CI pipeline as regular releases without clearly separating them. A regular release accidentally tagged as a snapshot, or the reverse, quickly causes confusion in a TypeScript monorepo about which version actually counts as stable and is allowed to be used in production code.
9. Versioning strategies compared
Depending on the coupling degree and release frequency of packages in a TypeScript monorepo, a different versioning strategy fits better.
| Strategy | When it makes sense | Drawback | Tool |
|---|---|---|---|
| Global version number | Very tightly coupled packages | Unnecessary version jumps | Manual, no tooling needed |
| Independent versioning | Loosely coupled, many packages | More individual changelogs to maintain | Changesets in independent mode |
| Fixed groups | Core package plus official plugins | Version jumps even for unchanged plugins | Changesets fixed configuration |
| Snapshot releases | Short lived PR previews | Not meant for permanent use | changeset version --snapshot |
For most growing TypeScript monorepos with many loosely coupled internal packages, independent versioning with Changesets is the most robust choice, because it integrates versioning decisions directly into the code review process and keeps changelogs consistently up to date, without a release owner having to manually research afterward what actually changed in which package.
Switching between these strategies is also not an all or nothing decision for the entire lifetime of a TypeScript monorepo. It is common to start with independent versioning and only later, once clear, tightly coupled package families have emerged, define individual fixed groups deliberately, instead of committing to the versioning strategy fully from the very start.
Calendar based versioning, where all packages carry the current year and month in the version string, is deliberately not recommended, because it makes no statement about actual breaking changes and makes automated update decisions impossible for consumers. For internal TypeScript monorepo packages, real semantic versioning through Changesets therefore remains the more reliable choice over purely time based alternatives.
Mironsoft
TypeScript monorepo tooling, release automation and CI/CD
Chaotic versioning of internal packages?
We set up Changesets for your TypeScript monorepo, define fixed and independent groups matched to your package structure, and automate release PRs plus changelogs in the CI pipeline.
Changesets setup
Choose versioning mode and configuration matched to your package structure
Release automation
Automated release PRs and publish steps in GitLab CI or GitHub Actions
Semver training
Team workshops on correctly classifying internal changes by semver
10. Summary
Changesets solves the core problem of versioning internal packages in a TypeScript monorepo by making versioning decisions part of code review, instead of leaving them to the release owner afterward. Independent versioning precisely reflects actual changes, while fixed groups remain sensible for tightly coupled package families. Automatically generated changelogs prevent the classic problem of forgotten or inconsistent release notes.
CI integration through automated release pull requests makes the entire process reproducible and traceable, even for purely internal packages with no public npm publication. Teams that establish semver discipline and automated release processes early in a TypeScript monorepo avoid the versioning chaos that almost inevitably emerges in unstructured monorepos as the package count grows.
Getting started already pays off with a handful of internal packages, not only at fifty. The earlier Changesets becomes a fixed part of the pull request workflow, the more natural the habit becomes for new team members, and the rarer the typical versioning mistakes that, in a grown TypeScript monorepo, can only be fixed afterward with considerable effort.
In the end it comes down to a simple principle: versioning decisions belong exactly where the code actually changes, not in a separate, easily forgotten step right before the release.
This principle holds regardless of whether a package is ever published publicly or stays internal forever.
Versioning Internal TypeScript Packages — the key takeaways
Changesets in review
Versioning decisions become a markdown file that is part of the pull request and thus of code review.
Independent vs. fixed
Independent versioning for loosely coupled packages, fixed groups for tightly coupled core package families.
Automatic changelogs
Description texts from changeset files automatically become consistently formatted CHANGELOG.md entries.
CI automation
Release pull requests bundle several changesets into one clear, shared release.