Skip to content

v0.120.0

Choose a tag to compare

@smm-h smm-h released this 05 Sep 11:49
· 1730 commits to main since this release

The version-fate model (recorded / unrecoverable / never-released), the release backfill promoted to a first-class command, operator-declared transition facts, and fleet-wide strictness.

Context

This release ships one campaign: every version number or tag a repository
carries now has exactly one declared fate, and every surface that reads or
repairs release history answers from the same authorities.

The release archives gained a three-state fate model -- recorded (the
release commit and trees), unrecoverable, or never released -- with a new
shipped_as field carrying the historical tag spelling a version actually
shipped under. The old backfill maintenance script is retired in favor of
rlsbl release backfill (consequential, preview/apply, with a
description-recovery chain and a hard refusal on unexplained tags), and
rlsbl transition record is the typed door for the two operator-declared
history facts. changelog remove completes the changelog surface.

Strictness grew in the same direction: unknown keys on the policed
configuration surfaces are refused at load, the dev_node alias is gone,
explicitly-empty flag values are refused centrally, a present git stash
blocks the guarded operations, and consequential classification now means
exactly one thing -- only a human may authorize the act.

Three banned vocabularies were renamed throughout: the release record
(formerly "ledger"), the transition record (formerly "lineage"), and the
release commit family (formerly "anchor"), including the archive marker
key rename to unrecoverable.

Breaking

  • Renamed: the release ledger is now the release record. The rlsbl.ledger module is now rlsbl.release_record, and every error message, check result and doc page that said "ledger" now says "the release record" or "the release archives".
  • Renamed: lineage records are now transition records. The committed repository-surgery log is written to transitions.jsonl (was lineage.jsonl), the rlsbl.lineage and rlsbl.lineage_followup modules are now rlsbl.transition_record and rlsbl.transition_record_followup, and the mirror reconciler's separate git-ancestry verdict is now ancestry-undetermined (was lineage-undetermined).
  • Renamed: the release anchor is now the release commit. Every message, check result, JSON field and doc that spoke of a release "anchor" now says "release commit": rlsbl status --json and rlsbl unreleased --json return nearest_release_commit_tag / nearest_release_commit_version (were range_anchor_tag / range_anchor_version), an archive is "recorded" or "unrecoverable" rather than "anchored" or "unanchorable", the rlsbl.anchor_remap module is now rlsbl.release_commit_remap, and the mirror's per-version tag verdict unanchored is now underivable. The key inside archived release files was renamed too -- unanchorable became unrecoverable -- as part of the version-fate work shipping in this same release.
  • Version fate is now a three-state model. An archived release file records exactly one of: the release commit it shipped from, unrecoverable = true (it shipped, from a commit nothing can name), or the new never_released = true (the version number exists but no release ever used it). The new optional shipped_as records the historical tag spelling a version actually shipped under when it differs from the scheme in effect today. Breaking: the marker key is spelled unrecoverable; the old unanchorable spelling is refused as an unknown key, so an archive carrying it must be renamed.
  • The transition record gains two event kinds, and its rewrite kind is renamed. release-history-closed records that a member's or releasable's release history is deliberately closed, and non-version-tag records one tag deliberately outside the version model; both carry an operator reason. Breaking: the rewrite event's kind literal is release-commit-remap, and the old anchor-remap spelling is refused as an unknown kind.
  • The two backfill scripts are retired. scripts/backfill_release_anchors.py and scripts/set_archived_descriptions.py are removal stubs that exit 1 naming rlsbl release backfill (and its --overrides file, which replaces the JSON description map). Every error that used to print a hand-written multi-step archive-recovery procedure now names the command instead.
  • The remote-release dispatch feature is removed. rlsbl scaffold no longer renders a release-dispatch.yml workflow, and the remote_release config key does nothing. Delete the workflow file from any repo that has one.
  • Unknown keys are refused at load. A key rlsbl does not know on a workspace.toml member table, releasable table or top level -- or in a standalone repo's .rlsbl/releasable.toml -- is now a hard error naming the surface, the key and the file, instead of a line that was written and never read. The retired dev_node member key is deleted: declare a dev node with dev_only = true plus releasable = false, which its refusal message spells out.
  • An empty flag value is refused instead of read as absence. Passing --name "" (or any named string flag with an empty or blank value) is now a hard error naming the flag. Most consequentially, rlsbl monorepo release init --releasables "" used to scaffold a section for EVERY releasable; it now refuses. Omitting a flag is unchanged.
  • Two managed-repo states are now refused. A repository with a git stash is a hard error on release run, release resume, release reconcile --apply and release backfill --apply (--allow-dirty does not waive it), and the new stash-free project check reports it earlier; drop the stash after landing its work. A workspace member outside every releasable that carries its own .rlsbl/changes/ is a hard error wherever that directory would be read -- changelog-directory enumeration, rlsbl monorepo status, and pre-push coverage.
  • rlsbl monorepo rename-releasable now asks before it runs. The rename declares a boundary-alias event in the releasable's transition record, pushes an alias tag to origin, and changes the tag scheme every future release of that releasable uses -- decisions only a human may authorize. The command is now consequential: an interactive run prompts, and a non-interactive one must pass --approve-consequential.

Features

  • A version recorded never_released keeps its changelog section, annotated as never released. Such a version can carry finalized changelog files -- entries written and locked before the release was abandoned -- so the section is rendered in full with its own archived description, above a line stating that no release was ever published under that version number.
  • New command: rlsbl release backfill. It brings a repository's release archives into the three-fate model from its real history: it records each version's release commit from its tag (or from the historical spelling the archive names in shipped_as, or from the version-bump commit), completes an archive missing any required field, materializes an archive for a released version that never got one, and adopts a version tag no store records as the release it is evidence of. A reconstructed description comes from the first source that yields one -- an operator-reviewed --overrides file, the version's GitHub Release body, its CHANGELOG.md section, the commit subjects in its tag range -- and the archive names the source it came from. Tags the repository cannot account for are listed first and refuse the whole apply, with each of the three cheap resolutions spelled out.
  • docs/release-workflow.md documents the backfill as a command. The backfilling section now describes rlsbl release backfill: its five verdicts, the recovery chain for a reconstructed description, the one fate it will not derive (never_released, which you declare by writing the archive), and the unexplained-tag refusal with its three resolutions.
  • rlsbl release backfill --dry-run exits non-zero when it finds a tag the repository cannot account for, after rendering the whole plan -- the same shape rlsbl release reconcile --plan has. A preview that returned 0 would report a repository as accounted for when the apply would refuse it. A stash is reported by the preview and still refuses only the apply.
  • rlsbl transition record is the typed door onto the two transition-record facts an operator declares. --non-version-tag <tag> declares a tag that stands outside the version model; --release-history-closed <subject> declares a member's or releasable's release history deliberately over. Exactly one must be elected, --reason is required, the event is appended to the repository-scoped transition record and committed, and a second declaration of the same kind about the same subject is refused naming the one already recorded. rlsbl release backfill stops listing a declared tag as unexplained and rlsbl release reconcile stops owing a verdict on it.
  • A renamed project's past versions get their current-spelling tags back. The refs a released version owns now include the historical tag spelling its release archive records in shipped_as, alongside the aliases the transition record carries. rlsbl release reconcile therefore mints the current scheme's spelling at that version's release commit while the old-spelling tag stands explained, and the unpublished-refs check looks for both. When an alias event and a shipped_as field disagree about one version's spelling, the ref set is a hard error naming both sources -- rlsbl never guesses which record is right.
  • rlsbl monorepo list shows the member facts its help promised. Each row now carries the releasable a member is versioned under and the member flags it declares (library, dev-only, test-only), not just name and path.
  • rlsbl changelog remove. Deletes one changelog entry from unreleased.jsonl or a released version's JSONL, addressed by its ULID identifier or by the commits it covers. A released file is unlocked, rewritten, re-locked, and followed by a CHANGELOG.md regeneration and a GitHub Release notes sync. A selector matching several entries -- or none -- is refused with every match named, and nothing is written.
  • Longer help for four flags. release scrub's --pattern, --file and --from-commit, and rewrite go-module-path's --to-module, each carried a help line short enough that the documentation generator flagged it. All four now say what the value is and what happens to the repository when it is supplied.
  • Documentation for entry removal and the scope distinction. The JSONL changelog page gains a removal section and lists changelog remove and changelog remap in its command table; the check tables describe what the range and orphan checks now report; and the repair-surface lists that claim to be complete name rlsbl changelog remove.
  • releasable-residue now also reports release state on a member that releases nothing -- a dev node, or any member declared releasable = false -- covering release archives, a changelog directory, and version tags in the member's own scheme. A release-history-closed transition record event naming the member silences it, so a member that deliberately stopped releasing keeps the record of what it released.

Fixes

  • A version recorded never_released is no longer mistaken for a release. It is skipped by the latest-release fact, the unreleased range, and the checkout-contains-latest refusal. rlsbl release undo descends past it to the real latest release instead of selecting the phantom and dying on its commitless archive; the unpublished-refs check stops demanding refs and a GitHub Release for it; and rlsbl release reconcile skips it while claiming the refs it would have owned, so a tag carrying its name can no longer fire the tripwire. rlsbl status --json gains latest_release_state and never_released_versions.
  • A release archive that cannot be read now stops changelog generation instead of silently emptying a version's description. The archive read behind every regenerated section went through a raw parse that turned bad TOML, a missing format_version gate or a wrong-typed field into an empty description -- which rlsbl changelog generate then committed over the version's real prose in both CHANGELOG.md and the read-only per-version .md. It goes through the validating reader now and hard-errors naming the file. An absent archive is still empty metadata, as before.
  • Archive writers refuse a second version fate. Recording a release commit on an archive that already says unrecoverable or never_released -- or marking one unrecoverable that says never_released -- is now a hard error naming the file and the conflict, instead of silently producing a two-fate archive that every later read rejects.
  • The release-archive backfill no longer corrupts a never-released archive. rlsbl release backfill recognizes the never_released fate as settled and proposes nothing for it, instead of planning an unrecoverable marker beside it -- so a repository carrying a phantom version passes the backfill unchanged however many times it runs.
  • The mirror plan names a never-released version for what it is. A version whose archive records never_released now gets its own never-released verdict (nothing shipped under that number, so there is nothing to mirror) instead of underivable, which told the operator to go restore a release commit that never existed.
  • rlsbl release resume re-checks the release file. A hand-authored candidate_sha, tree_hashes, unrecoverable, never_released or shipped_as in unreleased.toml now aborts a resume before the mutating phase, the same way it aborts rlsbl release run -- previously a resume archived it as the version's permanent record.
  • Documentation: the release-workflow page describes the flow-owned field set by reference to its one authority (rather than as a closed pair), states that the backfill pass is idempotent over a never-released archive too, and the reconcile helper's docstring names all three version fates.
  • A never-released pre-release is annotated in CHANGELOG.md. When a stable version's section consolidates its pre-release cycle, a pre-release whose archive records never_released now carries the never-released note in its own sub-section, instead of reading as a release that happened.
  • The archived description for 0.3.1 (and the CHANGELOG.md section generated from it) says its archive records no release commit, in the current vocabulary, rather than naming the retired "anchor".
  • rlsbl release reconcile no longer fires its tripwire on a tag the transition record declares a non-version-tag. A nightly marker or an imported vendor tag recorded that way is an explained ref: it owns no verdict, and a divergence on it no longer aborts every reconcile on the repository.
  • Non-version tags recorded in a monorepo are now seen. A tag declared with rlsbl transition record --non-version-tag is written to the repository-scoped transition record; rlsbl release reconcile in a workspace read only the releasable's own record, so it kept firing its publication tripwire on the very tag the declaration silences.
  • rlsbl release backfill accounts for every spelling of a released version's refs. It asks expected_refs -- the single authority -- instead of rendering one tag format of its own, so a monorepo's Go companion tags and a second live spelling minted beside an archive's shipped_as are explained rather than refusing the whole apply.
  • rlsbl release backfill --overrides settles. An overrides file carrying a context planned a write on every run, even when the archive already recorded exactly that context; it is now compared like the description and left alone when it agrees.
  • A releasable's tagless version is no longer recorded unrecoverable. rlsbl release backfill looked for a version-bump commit under the standalone message shapes only, so a monorepo releasable's bump commit ({name}: release v{version}) was never found and the version got a permanent unrecoverable marker even though its commit was in the history.
  • rlsbl release reconcile states the refusal instead of crashing. When two records disagree about the tag spelling a version shipped under, the ref set cannot be derived; the refusal now arrives as the command's own error naming both records, where it used to escape as a traceback.
  • rlsbl release undo no longer deletes the tag a version shipped under. A version whose archive records shipped_as owns that historical spelling as a recorded alias; undo treated every non-primary ref as a companion of the release it was undoing and deleted it locally and on origin. The spelling consumers already resolve now stands where it is.
  • The backfill and reconcile docs describe what the commands now do. The tag-spelling derivation, the unexplained-tag rule, the scopes a backfill walks, the alias sources unpublished-refs renders, and what rlsbl release undo leaves alone are all restated against the shipped behavior.
  • Empty positional arguments are named as arguments. rlsbl monorepo add "" and the other positional-taking commands used to refuse with --path was given an empty value ... drop --path, naming a flag that does not exist. The refusal now names the argument and asks for a real value.
  • An explicitly-empty CLI value is never read as absence. Every remaining flag and argument that read a supplied-but-empty value as absence now refuses it -- most consequentially rlsbl release undo --version "", which used to undo the LATEST release, and rlsbl changelog edit --description "", which reported success having written nothing (the refusal names --unset-description for clearing the field).
  • A supplied-empty changelog flag is reported as empty. rlsbl changelog add --commits "", --description "" and the same flags on changelog amend said the value was missing; changelog remap --map-file "" said no map source had been named. All of them now name the empty value that was actually passed.
  • The monorepo guide's member-field table no longer instructs you to break your workspace. It documented a target key that workspace.toml's loader refuses, and omitted import_name, test_only and lint_allow. The table now matches the loader's key set exactly (a test binds the two), and the "What the loader refuses" section lists every refusal in the order the loader reports them.
  • The config-schema check's documented behavior matches what it does. The checks reference claimed it refuses unknown keys and wrong types in .rlsbl/config.json; it refuses the retired private key, an empty targets list and a release.mode key, and validates the pipelines section -- unknown keys there are not policed.
  • rlsbl monorepo extract no longer drops a member's own declarations. A multi-member extract wrote the destination workspace from a hand-listed key set, silently losing dev_only, description, test_only and lint_allow -- so an extracted member was lint-checked under rules it never declared. Every key the extract does not rewrite itself now travels with the member.
  • The documented workspace.toml examples are ones the loader accepts. The monorepo and conversions guides showed [[projects]] blocks carrying a target key, which workspace.toml's loader refuses -- copying either example produced a workspace that would not load.
  • Printed remedies are runnable. The changelog orphan finding named rlsbl changelog edit --commits <hash> --remove, a flag no command declares; it now names a real rlsbl changelog remove invocation carrying the entry's own identifier. The name-availability refusal's usage line named rlsbl check, which takes no package names, and now names rlsbl check-name. rlsbl status's commits-ahead hint named the bare rlsbl release group and now prints the whole invocation, required flags included.
  • Cross-filed changelog entries are named for what they are. In a workspace, an entry whose commit belongs to another releasable's territory was reported by the changelog-range check as "not in unreleased range" -- false, since the commit is in the range and only its owner is wrong. It now gets its own finding, naming the owning member and releasable and printing the rlsbl changelog remove plus rlsbl changelog add invocations that move the entry. The orphan check counts and names the same distinction.
  • rlsbl status at a workspace root says what to run instead. Standing at a workspace root whose root member declares no release target, the command reported "no package.json, pyproject.toml, or go.mod found" -- true of the directory and useless to the reader. It now says the root member declares no target and names rlsbl monorepo status. A root member that does declare one is a real project and still resolves.
  • dead-modules now analyses a Flutter app from lib/main.dart. A Flutter app has neither a lib/<package>.dart barrel nor bin/ scripts, so the analysis previously found no entry point, traversed nothing, and reported the project clean.
  • rlsbl changelog edit and rlsbl changelog remove no longer narrow a changelog file's permissions. Rewriting an entry pinned the file to owner-only 0600, so an ordinary 0644 unreleased.jsonl -- or an unlocked released file -- silently became unreadable to everyone else the first time an entry was edited or removed.
  • changelog-range no longer points at a directory that does not exist. When an entry names a commit whose every file is rlsbl's own bookkeeping, no member owns it, so the finding now says the files are tool-owned and need no coverage anywhere and prints only the removal -- instead of telling the reader to re-file the entry from the owning member's directory.
  • Rewritten files keep their permissions. Bumping a version no longer narrows .strictcli/schema.json, .rlsbl/config.json, a releasable's version file, selfdoc.json or an Xcode project file from 0644 to owner-only 0600.
  • changelog add no longer sends you to a directory that does not exist. When a refused commit touches only tool-owned files (a regenerated snapshot, a generated workflow), the error now says the commit is exempt from changelog coverage and needs no entry anywhere, instead of pointing at an owning member's directory that has no owner; when another member does own the files, the message names it.