Skip to content

Authoring Changesets

martyy-code edited this page Aug 5, 2026 · 2 revisions

Last synced from docs/engineering/process/changesets.md § 4 on 2026-08-05. The repo file is the source of truth. If the two diverge, follow the repo.

This page explains how to write a Changeset for @deessejs/fp. It is the first thing to read before opening a pull request.

A Changeset is a small Markdown file under .changeset/ that tells the release tooling three things: which package is affected, what semver level the change implies, and a one-line summary that will appear in the changelog. Every PR on this repository must add one — see Contributing below.

Outline

  1. The shape of a Changeset
  2. Choosing the semver level
  3. Writing a good summary
  4. Multi-package changesets
  5. When a Changeset is empty
  6. Anti-patterns
  7. A worked example
  8. Contributing

1. The shape of a Changeset

A Changeset is a single Markdown file under .changeset/. The filename is a slug — the pnpm changeset CLI generates one for you when you run it interactively. The filename does not matter; the contents do.

The minimal shape:

---
"@deessejs/fp": minor
---

Add the `Result.tryCatch` and `Maybe.tryMaybe` families.

The first --- block is YAML frontmatter: a mapping from package name to semver level (major, minor, or patch). The body is the summary that will land in CHANGELOG.md and the GitHub Release notes. A single paragraph is the typical shape, but multi-line Markdown — code blocks, lists, links — is fine if the change warrants it.

2. Choosing the semver level

The semver level encodes the impact on the consumer of the package, not the size of the diff. A 200-line internal refactor is patch; a one-line rename of a public symbol is major. The decision tree:

  • major — anything that requires a consumer to change their code to keep working. A symbol renamed or removed. A signature change. A behaviour change that flips a default. If a user has to edit their code, it is major. Major bumps are rare and must be called out in the PR description.
  • minor — anything that adds a new capability without breaking existing code. A new exported function. A new method on Ok / Err / Some / None. A new option on an existing function. If the change is purely additive, it is minor.
  • patch — anything that fixes a bug, improves an internal detail, or changes documentation. A wrong type narrowing. A clearer error message. A perf tweak. A CI change. A typo in a docstring.

Cases that come up often and are easy to misclassify:

  • Refactor that exposes a new public export: usually minor, because the export is a new capability, even if the refactor itself was internal.
  • Bug fix that changes a default value: usually major, because consumers that depended on the old default will break.
  • Deprecating a symbol without removing it: minor with a clear note in the summary. Removal of the same symbol later is major.
  • Performance improvement with no API change: patch. Consumers do not need to change anything.
  • Type-only change that narrows or widens a return type: if a consumer's code stops compiling, it is major; if it now compiles better (more precise inference), it is minor or patch depending on whether the change is additive or corrective.

When in doubt, default to minor and call it out in the PR description. It is cheaper to ship a minor that turns out to be a patch than a patch that turns out to be a major.

3. Writing a good summary

The summary line is what shows up in CHANGELOG.md and the GitHub Release. It is written for the user upgrading the package, not the maintainer reviewing the PR. A good summary tells the reader, in one sentence, what they get or what they have to watch out for.

Conventions:

  • Past tense for completed changes: "Added Result.tryCatch", "Fixed incorrect narrowing in Maybe.map". The release has already happened by the time the user reads the changelog.
  • Lead with the user-visible effect, not the implementation: not "Extracted a helper for fold", but "Simplified error handling for nested Result chains".
  • Name the symbols involved: users grep their changelog for the API they care about. "Add Result.tryCatch" is searchable; "Improve error handling" is not.
  • One logical change per file: if a PR introduces two unrelated changes that warrant different semver levels, write two Changeset files. A single Changeset file can have multiple lines, but it should be one coherent change.
Good Bad Why
Added Result.tryCatch that wraps a throwing function into a Result. Add stuff The first names the symbol and the effect; the second is unsearchable.
Fixed incorrect narrowing when match returns a Result on the err branch. Fix bug The first is searchable and tells the user what was wrong; the second is noise.
Renamed Maybe.fromNullable to maybe for consistency with Result constructors. Update API The first tells the user which symbol moved; the second is a heading, not a sentence.
Deprecated errOr; it will be removed in 2.0. Use err instead. Deprecate The first tells the user what to do; the second is a label.
Bumped minimum Node version to 22.14 to match the engines field. Node bump The first explains the impact; the second is shorthand for maintainers.

If a change needs more than a sentence to explain, multi-line Markdown is fine. Common patterns: a short paragraph followed by a code block showing the new API, a short paragraph followed by a list of related changes, or a short paragraph that references a longer doc page.

4. Multi-package changesets

When a single PR affects more than one package in the monorepo, list all affected packages in the same frontmatter, each with its own semver level. The tool rolls them up independently at release time.

---
"@deessejs/fp": minor
"@deessejs/errors": patch
---

Add `Result.tryCatch` that returns typed errors from `@deessejs/errors` shapes.

A few rules:

  • Each package gets the semver level that matches its impact, not the highest level across the PR. If @deessejs/fp adds a function but @deessejs/errors only adjusts an internal helper, the levels differ.
  • If the changes in two packages are unrelated (different concerns bundled in one PR), do not bundle them. Split into two Changeset files.
  • The summary should describe the user-facing effect of the bundle. If the changes are conceptually one feature, one summary is fine. If they are two features that happen to share a PR, prefer two Changeset files.

5. When a Changeset is empty

For changes with no user-visible effect at all — a pure internal refactor, a CI workflow change, a dependency bump with no behaviour change, a docstring typo — the body of the Changeset is intentionally short and the semver level is patch. The Changesets CLI ships an --empty flag for this case:

pnpm changeset --empty -- --patch

This produces a Changeset file with the right frontmatter and an empty body. The audit trail is preserved (the file exists, the level is recorded), but the CHANGELOG entry is a one-liner with no body. Prefer --empty over skipping the Changeset file entirely, because every PR on this repository must add one (see § 8).

--empty is not an exemption. It is a form of Changeset, with the body left intentionally empty because the changelog does not need to know.

6. Anti-patterns

What not to put in a Changeset. The list is short because most of these are caught by the § 3 good vs bad table, but they recur often enough to be worth calling out explicitly.

  • Implementation chatter. "Extracted the fold helper into its own module", "Refactored the type definitions to use a conditional type", "Added a private utility". The user does not care how you wrote it; they care what they get.
  • Internal cross-references. "See PR #388", "Linked to the design doc in docs/internal/", "Companion to the changes in packages/internal". These rot as soon as the PR is renumbered or the doc moves.
  • Marketing. "We're excited to announce", "This release brings powerful new capabilities". Releases are not press releases.
  • Multi-line apology. "Sorry for the previous breaking change, this reverts..." is fine once; repeated in every release it becomes noise. State the change, not the history.
  • Bullet-point changelog when one sentence suffices. A one-line summary that says "Fixed narrowing in Maybe.flatMap" is better than three bullets about the same fix. Reserve bullets for genuinely independent changes.
  • Empty body without --empty. A Changeset file with frontmatter and no body looks like an authoring mistake. Use pnpm changeset --empty so the intent is explicit and the CLI does not prompt for a body.

7. A worked example

A PR adds a new Result.tryCatch and fixes a typing bug in Maybe.flatMap. Two unrelated changes, two Changeset files:

.changeset/cool-otters-dance.md
---
"@deessejs/fp": minor
---

Added `Result.tryCatch` that wraps a throwing function into a `Result<T, unknown>`,
narrowing to typed errors via the optional second argument.
.changeset/heavy-lion-sing.md
---
"@deessejs/fp": patch
---

Fixed `Maybe.flatMap` losing the narrowed type when the callback returns `None`.

Note the two different semver levels, the two user-facing summaries, and the named symbols. Both files exist; both will be consumed at the next pnpm changeset version.

8. Contributing

Every pull request on this repository must add a Changeset file under .changeset/. The rule is enforced by CI on every PR that targets staging: a dedicated changeset-check job fails the build when no Changeset is present, and the merge button stays disabled until one is added. There are no exemptions by category — not for refactors, not for CI, not for docs. For changes with no user-visible effect, use pnpm changeset --empty (see § 5); that produces a Changeset file with an empty body, which keeps the audit trail intact without inflating the changelog.

For the full operational details — branch flow, the Version Packages PR, the double-merge trap, and how Changesets interact with the release pipeline — see Release Process.