Releases and changelogs — tcm-ts

Releases and changelogs

A release names a point in a package's history as a version, for good. tcm picks the version and writes the list of changes from the codebase: semver checks, in the manner of cargo-semver-checks, compare the package's API with its last release. So the notes can't miss an API change or list one that didn't happen. Behaviour isn't checked: a change that keeps the API is a patch (limitations). A codebase can hold any number of packages, each with its own releases.

$ tcm release check geo
geo's next release: 0.2.0 (after 0.1.0): breaking changes

  major  geo.Point has a new required property: label
         type_required_property_added: values written for the old type lack it

  minor  geo.distance takes a new optional parameter: digits
         function_optional_parameter_added: old calls still work

$ tcm release geo
released geo@0.2.0 as #nbbd6qqu, after 0.1.0: breaking changes

## 0.2.0 (2026-10-29)

### Breaking changes

- `geo.Point` has a new required property: `label`

### Extended

- `geo.distance` takes a new optional parameter: `digits`

Making a release

tcm release [package]
releases at the version its changes call for. The package can be left out when there's one. The first release is 0.1.0
tcm release <package>@<version>
releases at a higher version, to declare an API stable (1.0.0, say). A smaller version than the changes call for is refused
tcm release check [package]
shows the next release and why: each finding, the lint it trips, what it means for code using the package, and why releasing would be refused, if it would (exits 1)
tcm release view <package>@<version>
prints a release's notes (@<version> alone with one package)
tcm releases
lists every package and its releases
  • Refused while there's work to do: conflicts or broken definitions in tcm todo, or, for a tcm library, definitions it uses from outside its namespace. --force releases anyway (also past a too-small version).
  • Releases don't change. Making a version again is refused, and so is syncing with a codebase that has a different release under the same version.
  • Releases sync with tcm push and tcm pull, along with the history they name.
  • A release follows the one in its branch's history. One made on a branch is that branch's until it lands. Landing keeps the branch's history, even with --squash (history), so the next release follows it there too.
  • Releases work as history entries: tcm diff geo@0.1.0, tcm reset geo@0.1.0. tcm pack geo@0.2.0 packs one (publishing); tcm pack geo on a branch exactly at a release packs that release.

Announcing a release

tcm release --announce announces a release on the AT Protocol (the network Bluesky runs on): a codes.tcm.release record in your own repository, naming the version's namespace hash and where to fetch it. Your repository signs its records, so the release is provably yours (by your DID), and anyone can find it without a registry.

$ tcm release geo --announce --as alice.dev --source https://tcm.alice.dev
signing in as alice.dev, in the browser (if it doesn't open: https://bsky.social/oauth/authorize?…)
released geo@1.2.0 as #nbbd6qqu, after 1.1.0: additions
…
announced geo@1.2.0 as @alice.dev: at://did:plc:…/codes.tcm.release/geo:1.2.0
  fetched from https://tcm.alice.dev
  https://tcm.alice.dev doesn't have it yet: `tcm push` sends it
{
  "$type": "codes.tcm.release",
  "package": "geo",
  "version": "1.2.0",
  "namespace": "…",
  "entry": "…",
  "hashFormat": 2,
  "notes": "## 1.2.0 (2026-11-03)\n…",
  "sources": ["https://tcm.alice.dev"],
  "createdAt": "2026-11-03T12:00:00.000Z"
}
  • The record holds the package and version; the namespace's whole hash as released (what tcm lib add mounts, libraries); the history entry it names and the hash format; the notes (at most 100,000 bytes, or the release is refused); the sources; and when it was made. Its key is <package>:<version>, so one record per version. The lexicons for it, the attestations and stars are in the records library (libraries), with tcm.codes as their authority.
  • Sources are tcm serve servers (sync) people fetch it from, in order. --source <url> (repeatable, up to 16, each at most 2048 characters) is kept for the package's next announces. Without it: the kept ones, else origin's URL if it's a server. With none, the announce is refused. A source that doesn't have the release yet, or doesn't answer, is reported but the announce goes ahead: tcm push sends it there. Sources need no trust: what they send is re-hashed.
  • Existing versions: tcm release geo@1.1.0 --announce announces one made earlier, or one whose announce failed. A record that already says the same is left alone; one with different sources gets the new ones; one naming another namespace for the version is refused.
  • If writing the record fails, the release still stands (sources and sign-in are settled first): tcm release geo@1.2.0 --announce retries.
  • People add it by your handle: tcm lib add @alice.dev/geo@1.2.0, and upgrade to what you announce next (announced releases).

Signing in

  • In your browser, with AT Protocol OAuth. tcm listens on 127.0.0.1 for the browser to come back, and asks only to write tcm's records (releases, and the reviews, advisories and yanks tcm lib attest writes: what people say). It accepts only its own sign-in's answer (RFC 9207), and the server's endpoints must be https. $BROWSER, if set, opens the page.
  • --as <handle> (or a DID) picks the account the first time; later, the last one used is the default.
  • The sign-in is kept in ~/.config/tcm/atproto.json ($XDG_CONFIG_HOME/tcm), readable only by you (a link or someone else's file isn't read; one others could read is made private). Tokens refresh as they expire. Delete the file to sign out.
  • Or with an app password, no browser (CI, a server over ssh). Make one in your account's settings (Bluesky: Settings, Privacy and security, App passwords) and give it once, with --as: TCM_ATPROTO_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx tcm announce --as alice.dev. tcm signs in with it (com.atproto.server.createSession), keeps it in the same file, and signs in again by itself when tokens run out, for every command that writes tcm's records. Revoking the app password signs it out everywhere.
  • $TCM_ATPROTO_DIRECTORY resolves identities in a PLC directory that also resolves handles, instead of DNS, HTTPS and plc.directory: for a private or test network. Such networks' servers are usually local and plain http, which tcm refuses unless $TCM_ATPROTO_LOCAL=1 too. Otherwise a PDS must be an https server's address alone, and servers a DID or handle names are asked only at public addresses, without following redirects.

Releases kept in git

A codebase that mirrors a directory in a git repository keeps its releases with the files. Releasing writes tcm-releases.json in the mirrored directory (version, notes, author, date, and the commit holding the files as released), and is refused while the mirror has changes git doesn't have. Commit the record with the files.

Linking a codebase to the mirror afresh (tcm import <dir> --into ns --mirror) makes the recorded releases again first: each one's commit is imported from git in order, and the release put at the history entry that makes, with its notes. Hashes don't depend on where or when code is imported, so the releases name the same definitions, and the next release's semver checks compare against them.

Release notes

A release's notes are a heading, a summary you write, and the list of changes tcm writes:

  • Breaking changes, Added, Extended, Changed: what the semver checks found, by name. An addition shows its signature and the first sentence of its docs. A definition only re-hashed because something it uses changed isn't listed.
  • Contributions: the contributions merged since the last release that changed this package, read from this codebase and from origin (or --remote <name>).

To write the summary, draft the release first:

$ tcm release geo --draft
wrote release-geo.md (geo's next release, 0.2.0 as things stand): write what it's about above the marker, then `tcm release geo`; the changes below the marker are regenerated

Write above the marker in release-geo.md; tcm release geo takes your words and deletes the file. Everything below the marker is regenerated when the release is made, so editing it does nothing, and changes since the draft are included. --notes <file> takes the summary from another file.

Changelogs

tcm changelog [package] prints a package's release notes, newest first, as markdown; --out CHANGELOG.md writes a file. tcm pack puts it in the package as CHANGELOG.md. It's made from the releases, so it's never hand-edited and never out of date.

Semver checks

Each changed definition the package exports is compared, as at the last release and as now, with the TypeScript checker, from the side of code using it: every use that typechecked against the old version must still. Gaining an optional parameter is an extension (old calls still work); gaining a required parameter, accepting less for a parameter, or returning more is breaking. Types are compared by their cases and properties, and as a whole.

The version follows semver as npm and Cargo read it. From 1.0.0: major for breaking changes, minor for additions and extensions, patch otherwise. Before 1.0.0: minor for breaking changes, patch for anything else.

LintCalls forFindingWhat it means for code using it
definition_removedmajorremovedcode that uses it no longer compiles
definition_renamedmajorrenamedcode that imports it by its old name no longer compiles
export_movedmajoris exported from a different subpathimports of it from the old one no longer compile
bin_removedmajorcommand removedscripts that run it fail
bin_addedminorcommand addednew API
kind_changedmajoris now a type rather than a value, or the other way aroundcode that uses it the old way no longer compiles
function_became_valuemajorcan't be called any morecalls to it no longer compile
function_required_parameter_addedmajortakes a required parameter it didn'tcalls written for the old version are missing an argument
function_parameter_removedmajortakes fewer parameterscalls that pass the removed argument no longer compile
function_parameter_made_requiredmajorrequires a parameter that was optionalcalls that leave it out no longer compile
function_parameter_type_narrowedmajoraccepts less for a parameterarguments the old version accepted may not be accepted
function_return_type_widenedmajorcan return more kinds of valuecallers may get a value they don't handle
function_type_parameter_count_changedmajorhas a different number of type parametersexplicit type arguments no longer match
function_signature_incompatiblemajorhas a signature old calls don't fitsome calls written for the old version no longer compile
constructor_incompatiblemajoris constructed differentlynew written for the old version no longer compiles
value_type_widenedmajorcan hold more kinds of valuecode reading it may get a value it doesn't handle
type_property_removedmajorlost a propertycode reading the property no longer compiles
type_required_property_addedmajorhas a new required propertyvalues written for the old type lack it
type_property_made_optionalmajorhas a property that may now be missingcode reading it may get undefined
type_property_made_requiredmajorrequires a property that was optionalvalues written for the old type may lack it
type_property_made_readonlymajorhas a property that's now readonlycode assigning to it no longer compiles
type_property_type_changedmajorhas a property of a different typecode reading or writing the property may no longer compile
type_union_member_addedmajorhas a new casecode handling every case no longer does
type_union_member_removedmajorlost a casevalues of the removed case no longer fit
type_type_parameter_count_changedmajorhas a different number of type parametersuses with type arguments no longer match
type_changedmajorchangedcode using it may no longer compile
definition_addedminoraddednew API
function_optional_parameter_addedminortakes a new optional parameterold calls still work
function_parameter_made_optionalminorno longer requires a parameterold calls still work
function_parameter_type_widenedminoraccepts more for a parameterold calls still work
function_return_type_narrowedminorreturns a narrower typewhat old callers expect still fits
function_signature_extendedminoraccepts more (a new overload, say)old calls still work
value_type_narrowedminorholds a narrower typewhat old readers expect still fits
type_optional_property_addedminorhas a new optional propertyold values still fit
type_property_made_writableminorhas a property that's no longer readonlyold uses still work
implementation_changedpatchchanged, with the same APIuses still compile; behaviour may differ

A change to a type the API mentions is shown where the type is, not repeated at each definition using it. When a change in behaviour deserves more than a patch, release at a higher version.

Built from c482a14e