releases
Releases and changelogs
A release names a point in a package's history as a version, for good, as on Unison Share. Its notes are markdown kept in the codebase, and the part that lists what changed is written by tcm from the codebase, so it can't leave out a change to the API, or list one that didn't happen (behaviour isn't checked: a change that keeps the API is a patch; see limitations). The version is decided the same way: semver checks, in the manner of cargo-semver-checks, find what changed in the package's API since its last release and what that calls for. 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 a package at the version its changes call for. The package's name can be left out when the codebase has one. Its first release is0.1.0.tcm release <package>@<version>releases at a version further than that,1.0.0say, to declare an API stable. A version smaller than the changes call for is refused (--forcereleases anyway).tcm release check [package]shows what the next release would be and why: each finding, the lint it trips, and what it means for the code using the package; and, if releasing it would be refused as it stands, why (exiting 1).- A release is refused while the package has work left in
tcm todo: conflicts, or definitions that stopped typechecking; or, for a tcm library, while it uses definitions outside its namespace (--forcereleases anyway). - Releases don't change. A version names one release; making it again is refused, and so is syncing with a codebase that has a different one under the same version.
- Releases sync with
tcm pushandtcm pull, 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 it keeps the branch's history, even with
--squash(history), so the next release after it follows it there too. tcm diffandtcm resettake a release (tcm diff geo@0.1.0) wherever they take a history entry, andtcm pack geo@0.2.0packs one (publishing);tcm pack geoon a branch exactly at a release packs it as 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 there, saying which namespace hash the version is and
where it can be fetched from. Your repository signs its records, so a release is known to be 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 says the package and version; the namespace's whole hash, as released (what
tcm lib addmounts for that release, libraries); the history entry it names, and the hash format the hashes are in; the notes (at most 100,000 bytes: a release with more is refused); the sources; and when the release was made. Its key is<package>:<version>, so a version is one record. Its lexicon, and the attestations' and stars', are in therecordslibrary (libraries), tcm.codes being their authority. - Sources are servers people fetch it from (
tcm serve, sync), in order:--source <url>(up to 16, each at most 2048 characters) is kept, for that package's next announces; without it, the ones kept, elseorigin'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 said (it's announced anyway): people can fetch it from there once it's pushed. What sources send is re-hashed as it arrives, so they need no trust. - Signing in is AT Protocol OAuth, in your browser. tcm is a loopback client: it 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 attestwrites: what people say). It takes only its own sign-in's answer (its state, from the server it asked, which must say so: RFC 9207), and the server's endpoints must be https. The first time,--as <handle>(or a DID) says which account; the sign-in is then kept for you in~/.config/tcm/atproto.json($XDG_CONFIG_HOME/tcm), readable only by you (a file that's a link, or someone else's, isn't read; one others could read is made private), and its tokens are refreshed as they expire, so the next announce doesn't ask. Without--as, it's the account used last. Deleting the file signs out.$BROWSER, if set, opens the page. - Sources and the sign-in are settled before the release is made. If writing the record fails after that, the
release stays made, and
tcm release geo@1.2.0 --announceannounces it. - An existing version (
tcm release geo@1.1.0 --announce) is announced as it is: one made before, or one whose announce failed. A record that already says the same is left as it is; one whose sources differ gets the new sources; one naming another namespace for the version is refused, since a version names one release. - People add it by your handle:
tcm lib add @alice.dev/geo@1.2.0, and upgrade to what you announce next (announced releases). $TCM_ATPROTO_DIRECTORYlooks identities up in a PLC directory that also resolves handles, instead of DNS, HTTPS and plc.directory: for a private network, or a test's. Such a network's servers are usually local and plain http, which tcm refuses unless$TCM_ATPROTO_LOCAL=1too: otherwise a PDS must be an https server's address alone, and the servers a DID or a handle names are asked only at public addresses, not following redirects.
Releases kept in git
A codebase that mirrors a directory in a git repository can be made again from its
files, so its releases are kept with them. Releasing records the release in tcm-releases.json in the mirrored
directory (its version, notes, author and date, and the commit holding the files as released), and is refused
while the mirror has changes git doesn't have yet. Commit the record with the files.
A codebase linked to the mirror afresh (tcm import <dir> --into ns --mirror) makes the recorded releases again
before importing the files as they are: each one's commit is taken from git and imported, in order, and the
release is put at the history entry that makes, with its notes as they were. Hashes don't depend on where or when
code is imported, so the releases name the same definitions they did, and the next release's semver checks
compare against them.
Release notes
A release's notes are a heading, a summary people 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 documentation. A definition that only changed because something it uses did (re-hashed, code untouched) isn't listed again.
- Contributions: the contributions merged since the last release that changed this
package. They're read from this codebase and from
origin(or--remote <name>), wherever they were merged.
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
release-geo.md is a markdown scratch file: write above the marker, and tcm release geo takes your words from
it (and deletes it; they're in the release now). Everything below the marker is written again from the codebase
when the release is made, so editing it changes nothing, and changes made since the draft are in. --notes <file>
takes the summary from another file.
Changelogs
tcm changelog [package] prints a package's releases' notes, newest first, as markdown; --out CHANGELOG.md
writes it to a file. tcm pack puts it in the package as CHANGELOG.md. The changelog is made from the releases,
so it's never edited by hand and never out of date. tcm releases lists every package and its releases.
Semver checks
The checks compare each changed definition the package exports, as it was at the last release and as it is now, with the TypeScript checker, from the side of the code that uses it: every use that typechecked against the old version must still. A function that gains an optional parameter is extended (old calls still work); one that gains a required parameter, or accepts less for a parameter, or can return more, is breaking. Types are compared by their cases and properties, and as a whole.
The version they call for follows semver, as npm and Cargo read it: from 1.0.0, a major release for breaking
changes, a minor one for additions and extensions, and a patch otherwise. Before 1.0.0, a minor release for
breaking changes and a patch for anything else.
| Lint | Calls for | Finding | What it means for code using it |
|---|---|---|---|
definition_removed |
major | removed | code that uses it no longer compiles |
definition_renamed |
major | renamed | code that imports it by its old name no longer compiles |
export_moved |
major | is exported from a different subpath | imports of it from the old one no longer compile |
bin_removed |
major | command removed | scripts that run it fail |
bin_added |
minor | command added | new API |
kind_changed |
major | is now a type rather than a value, or the other way around | code that uses it the old way no longer compiles |
function_became_value |
major | can't be called any more | calls to it no longer compile |
function_required_parameter_added |
major | takes a required parameter it didn't | calls written for the old version are missing an argument |
function_parameter_removed |
major | takes fewer parameters | calls that pass the removed argument no longer compile |
function_parameter_made_required |
major | requires a parameter that was optional | calls that leave it out no longer compile |
function_parameter_type_narrowed |
major | accepts less for a parameter | arguments the old version accepted may not be accepted |
function_return_type_widened |
major | can return more kinds of value | callers may get a value they don't handle |
function_type_parameter_count_changed |
major | has a different number of type parameters | explicit type arguments no longer match |
function_signature_incompatible |
major | has a signature old calls don't fit | some calls written for the old version no longer compile |
constructor_incompatible |
major | is constructed differently | new written for the old version no longer compiles |
value_type_widened |
major | can hold more kinds of value | code reading it may get a value it doesn't handle |
type_property_removed |
major | lost a property | code reading the property no longer compiles |
type_required_property_added |
major | has a new required property | values written for the old type lack it |
type_property_made_optional |
major | has a property that may now be missing | code reading it may get undefined |
type_property_made_required |
major | requires a property that was optional | values written for the old type may lack it |
type_property_made_readonly |
major | has a property that's now readonly | code assigning to it no longer compiles |
type_property_type_changed |
major | has a property of a different type | code reading or writing the property may no longer compile |
type_union_member_added |
major | has a new case | code handling every case no longer does |
type_union_member_removed |
major | lost a case | values of the removed case no longer fit |
type_type_parameter_count_changed |
major | has a different number of type parameters | uses with type arguments no longer match |
type_changed |
major | changed | code using it may no longer compile |
definition_added |
minor | added | new API |
function_optional_parameter_added |
minor | takes a new optional parameter | old calls still work |
function_parameter_made_optional |
minor | no longer requires a parameter | old calls still work |
function_parameter_type_widened |
minor | accepts more for a parameter | old calls still work |
function_return_type_narrowed |
minor | returns a narrower type | what old callers expect still fits |
function_signature_extended |
minor | accepts more (a new overload, say) | old calls still work |
value_type_narrowed |
minor | holds a narrower type | what old readers expect still fits |
type_optional_property_added |
minor | has a new optional property | old values still fit |
type_property_made_writable |
minor | has a property that's no longer readonly | old uses still work |
implementation_changed |
patch | changed, with the same API | uses still compile; behaviour may differ |
A change to a type the API mentions shows where the type is, and isn't repeated at each definition that uses it.
Behaviour isn't checked: a definition whose code changed with the same API is a patch, implementation_changed.
Release at a higher version when a change in behaviour deserves one.