Skip to content

v0.130.0

Choose a tag to compare

@smm-h smm-h released this 29 Sep 21:02
· 782 commits to main since this release

Forks, rlsbl release abandon, every check as an option, nested workspace members, a refusal of uploads carrying private paths, releases run in a detached checkout of the committed commit (--allow-dirty removed), and rlsbl secrets sync-npm-token with the npm-token-synced check.

Context

Every release now runs in a release checkout, a detached checkout of the
release branch's committed tip, so nothing it builds or tests reads the working
tree; --allow-dirty had nothing left to waive and was removed. Nested workspace
members are supported across ownership, tags, CI filters, test runners, uploads
and Go requires. A registry keeps every upload permanently, so a release now
refuses an upload carrying planning notes, release metadata, agent files or
secrets, and scaffold writes each ecosystem's exclusions. An expired npm token
broke CI publishes after tagging, so rlsbl secrets sync-npm-token copies the
~/.npmrc token into NPM_TOKEN secrets and the npm-token-synced check blocks a
release whose secret predates the token. rlsbl now requires strictcli 0.44.0
and strictspec 0.5.0.

Breaking

  • rlsbl claim-name --force-publish is removed. claim-name always respects its availability check: a name the check reports as taken exits 1, and a check that ended in an error or returned a status it does not define exits 2, without publishing. Passing --force-publish is now refused as an unknown flag.
  • Every check is an option, rlsbl:<check name>. An error check ranks error > warn > off and a warning check warn > off, defaulting to its registered severity, and the checks strictcli registers into rlsbl are options too. rlsbl options set <id> --current <value> --ideal <value> --reason <text> [--scope <member>] writes the entry into .strictmetadata/options/<subject>.toml at the git root (creating the directory and its strictspec-owned manifest.toml), validates it with strictspec before writing, and commits it with the Autogenerated trailer; rlsbl options registry prints the registry, as data with --json. Every check run and every release apply the entries -- off does not run the check, warn reports without blocking -- and rlsbl check --list shows each check's value and where it came from. Invalid entries stop every check run and every release. dep-floors, lint, format, type-check, and strictspec-certificate-gate stay off until their options are switched on, and rlsbl:test-sandbox decides whether the sandboxed test runner is rendered. Declaring internal_dep_floors, a checks.<name> block, strictspec_gate, or test_sandbox no longer turns anything on: each setting is required while its option is on and refused by config-schema and every release while it is off, and the refusal names both fixes, including the exact rlsbl options set command. These options take a path scope, so a workspace can switch one on for a single member (--scope cli); the scope guards skip while their check is off, and testisolation-floor counts the floor as adopted when rlsbl:test-sandbox is on or the testisolation plugin is declared. To keep what a repository runs today, write the entry for every such setting its config carries. changelog_format_version_enforced is retired, and the changelog-format-version check is removed. The changelog-format-version-gate check runs on every repository, and its option rlsbl:changelog-format-version-gate decides whether a line without format_version blocks: error by default, and at warn the check still runs and reports. config-schema and every release refuse a config still carrying the key; delete it, and when the repository wants the gate off, also write the entry the refusal names: rlsbl options set rlsbl:changelog-format-version-gate --current off --ideal error --reason "...".
  • The pre-push hook runs rlsbl failing-checks --hook pre-push, and blocks only on error-level failures. rlsbl's checks.toml declares [hooks.pre-push] (the prepush tag) and [hooks.pre-release] (the preflight tag), so the installed hook names only its hook and a check an options entry softened to warn no longer blocks a push; rlsbl check stays the full report and still fails on warnings. rlsbl check --hook <name> and rlsbl failing-checks (same selection flags, including --releasable) run a declared hook's selection. Run rlsbl scaffold to upgrade an installed hook: it recognizes the previous rlsbl check --tag prepush hook and rewrites it; a hand-written hook is left alone and must be changed by hand.
  • A release blocks only on error-level check failures, for every bump type. A check at warn, registered that way or softened by an options entry, is printed as a WARN line and no longer stops a patch, minor, major, or prerelease release. The at-least-one-user-facing-entry rule is now release validation: every release except infra refuses to run without a user-facing changelog entry, naming bump = "infra" for a release with only infrastructure changes.
  • The pgdesign target reads pgdesign check --tag validation --json instead of passing --ignore-warnings. A check with status fail fails the release and its errors are named; warnings are printed and never block. The output must be strictcli's machine envelope, which pgdesign 0.27.1 prints: an older pgdesign whose --json prints a bare result array, a validation tag that selects no check, or a nonzero exit with no failure or warning to show fails the build.
  • --allow-dirty is removed from rlsbl release run and rlsbl monorepo release run: every release runs in a detached checkout of the committed commit. The release checks the release branch's tip out into .git/rlsbl/release-checkout (reused across releases, submodules initialized) and runs the producers, tests, hooks, local builds, version bump, and release commits there, so uncommitted work never reaches a release. The branch advances only by compare-and-swap from where the release left it -- a branch another session moved is refused, naming the commits -- and only the files the release's own commits change are written into the working tree. An uncommitted change to a path the release writes (its release state, version files, changelog, or a file it generates) refuses the release, naming the path; every other uncommitted change is listed and left alone, and --dry-run reports both instead of refusing. A release that fails before its candidate push leaves the branch and the working tree as they were: there is no git reset --hard anymore, and a refused candidate push takes its branch advance back the same way. Drop --allow-dirty/--no-allow-dirty from every invocation.
  • Workspace member paths are accepted in one spelling only. A member path in workspace.toml is repository-relative and /-separated, with no ., .., or empty segment, no leading or trailing /, and no backslash, and the repository root is .. Any other spelling (draw/cmd/, ./draw, draw//cmd) used to be tidied at load, and some spellings owned no files at all; it is now refused at load and by rlsbl monorepo add and rlsbl monorepo absorb. The load refusal lists every such path at once, each with the line to change in .rlsbl-monorepo/workspace.toml (path = "python/" becomes path = "python"), so a workspace written with trailing slashes is fixed in one pass.
  • Two new release-blocking Go tag checks. path-tag-format-go-member refuses a releasable whose tag_format is a Go path tag (<path>/v{version}) naming a path none of its own Go members lives at: such a tag would permanently publish a version of another module, or of none. It names {name}@v{version} and each Go member's own path as the formats to write. go-module-major-suffix refuses a go.mod module path ending in /v2 or a higher major, naming the rlsbl rewrite go-module-path invocation that drops the suffix. Both run in a release's preflight, so an affected workspace stops releasing until it is fixed.
  • workspace-unregistered finds an undeclared project at any depth. It looked only at top-level directories, so a go.mod or package.json nested inside a member (tools/extra/go.mod) passed, and its files silently fell to the enclosing member. Every manifest git lists is now read. Manifests under a tests/, testdata/, or fixtures/ folder and inside a scratch directory at a member's root are skipped, and a package.json marked private, or the undeclared parent directory of a declared member, is no longer exempt. Declare each flagged directory as a member, or remove its manifest.
  • rlsbl rewrite go-module-path renames only the module it names. Renaming M/draw also renamed M/draw/cmd, a module nested under it with a go.mod of its own, including when run as the fix rlsbl monorepo extract prints. A nested module's path and its packages' imports are now left alone (its references to the renamed module still move), the plan names every nested module it leaves, and each is renamed by an invocation of its own.
  • A release hook may not run inside a member nested in the member that declares it. A config-driven hook's dir pointing into a nested member (sdk's hook with "dir": "python", where sdk/python is a member) is refused when it would run, naming the member that owns the directory and the config file to declare the hook in instead. A releasable-level hook may still name any of its own releasable's members, and a dir with a .. segment or an absolute path is refused.
  • Go modules in one workspace must require each other's released versions, and may not use replace. rlsbl never raised a Go module's require line after a sibling module released, and go install <module>@<version> rejects a module carrying a replace, so a released module could ship pinned to a superseded sibling or unusable from outside. go-workspace-require-current refuses a require below the sibling's latest release and prints the go get <module>@v<version> to run; go-workspace-replace refuses a releasable member's replace into the workspace and prints the migration to a committed go.work (go work init, go work use, go mod edit -dropreplace). Both run in a release's preflight.
  • The release writes a strictcli project's .strictcli/schema.json from <app> help --json. strictcli replaced --dump-schema with help --json, which prints the help document on stdout; the release runs it (uv run <script>, go run <main package>, or node <bin>), writes the document byte for byte, and stamps the release version into it. A program on a strictcli without a help command (Python strictcli 0.43.0, Go strictcli v0.36.0, TypeScript strictcli 0.42.0, or earlier) stops the release before anything changes, naming the upgrade for its language; rlsbl never runs --dump-schema and never falls back to it. A command that exits 0 without printing a help document is refused too.
  • A releasable root member gets its config where rlsbl reads it. rlsbl monorepo init --root-releasable now requires --publish-mode and writes .rlsbl-monorepo/releasables/<name>/config.json, with the root's targets, default pipelines, and a required --publish-gate-check-regex when the root publishes. A release, rlsbl check, and the root-rlsbl-conflict finding name that file for the root member instead of a root .rlsbl/config.json, which the workspace refuses. A failed commit now stops monorepo init and removes what it wrote.

Features

  • rlsbl release abandon. Records an abandoned release attempt's version as never released (its release archive with never_released = true), deletes the in-progress state file, and commits the archive, as one consequential operation. The next release bumps from that version and never releases under it again.
  • rlsbl release abandon finishes an interrupted run and names the right fix when it refuses. Re-running it after an abandon that stopped once its archive was written commits that archive if needed and deletes the leftover in-progress state file. A version below the latest release is refused with the same explanation rlsbl release run gives, and a tag for the version with no archive (locally or on origin) now points at rlsbl release backfill, which adopts the tag as the release it is evidence of, instead of rlsbl release undo.
  • rlsbl rewrite uv-path-sources and rlsbl monorepo extract switch rlsbl:dep-floors on wherever they write internal_dep_floors and the option is off: an entry with current and ideal error, scoped to the member in a workspace, whose reason names the command.
  • A private repository's PyPI publish workflow is scaffolded with attestations: false, and the new private-repo-publishing check reports publishing that needs a public repository. PyPI attestations record the repository's name, workflow, and commit in a public transparency log; rlsbl scaffold and rlsbl monorepo sync now switch them off when the repository is private and keep the default for a public one. The check (tag release) names npm build provenance, an attesting PyPI publish step, and a Go pipeline that reaches the Go module proxy, each with its fix, when the repository is private.
  • Forks are supported: a fork declares its upstream, rlsbl upstream adopt-tags moves the tags it inherited, and changelog coverage leaves upstream's history out. A fork declares its upstream in .strictmetadata/upstream/upstream.toml (strictspec's upstream schema: host, owner, repo, and branch); nothing is inferred from a git remote. rlsbl upstream adopt-tags moves every tag upstream has at the same name and object to refs/tags-of/<host>/<owner>/<repo>/<tag>, keeping its object, pushes those refs to origin, and deletes the tags from refs/tags on origin and locally; a same-name tag at another object refuses the run, named. It is consequential, supports --dry-run, and a re-run finishes an interrupted one. In a fork, the changelog checks, the pre-push coverage, rlsbl unreleased, and rlsbl status leave out every commit reachable from refs/upstream/<host>/<owner>/<repo>/<branch> or a kept tag, and a fork missing that ref is refused with the git fetch --no-tags commands that restore it. In a fork, the refusal of an empty release record beside version tags names rlsbl upstream adopt-tags before rlsbl release backfill.
  • A member's test runner skips the members nested inside it. A Python member running pytest in its own directory also collected a nested member's tests and failed on that member's dependencies. rlsbl scaffold now writes a path-exact --ignore=<path> for each nested member into [tool.pytest.ini_options] addopts (and each path into a Deno member's exclude), and the new nested-member-runner-exclusion check, which also runs in a release's preflight, fails while one is missing. pytest resolves --ignore against the directory it starts in, so a member's tests run from its own directory, as rlsbl and CI already do.
  • A member's published upload may not carry the files of members nested inside it. npm pack of a parent listed a nested member's files, a Python parent's sdist carried its nested members in full, and a Go module zip carried every nested member that is not a Go module itself, so a nested member's change altered the parent's published package with no parent changelog entry. The new nested-member-upload-contents check lists an npm package (npm pack --dry-run --ignore-scripts --offline) and a Go module zip offline, in a release's preflight, naming each file and the member that owns it. A Python upload can only be listed by building it, so the pypi CI workflow of a member with nested members now builds its upload and fails on such a file; run rlsbl scaffold to add that step.
  • The new nested-member-uv-sources check names where a nested Python member's workspace sources belong. uv refuses a { workspace = true } entry in the [tool.uv.sources] of a member nested inside another member, reporting that the package "is not a workspace member", while the same entry in the uv workspace root's pyproject.toml resolves. The check, which also runs in a release's preflight, refuses the nested declaration and names the root file to move it to.
  • The monorepo docs describe nested members. A member may sit inside another member's directory, and a new Nested members section in the monorepo guide says what every part of rlsbl does about it: file ownership, tags, CI filters, test runners, published uploads, uv sources, hooks, scaffold, extract, and mirror, plus the Go rules for a shared releasable and for modules that depend on each other. The canonical member-path spelling, the nested CI-filter excludes, the nested test-runner exclusions, and the extract refusal are documented where each applies.
  • A release refuses an upload that carries a private path. Planning notes (todo/), release and family metadata (.rlsbl/, .rlsbl-monorepo/, .strictmetadata/, stricttools/, .stricttools/), tool state (.selfdoc/, .strictcli/), agent instructions (CLAUDE.md, AGENTS.md, .claude/), scratch output (experiments/, screenshots/), environment files (.env*), and *.local-only files had reached PyPI, npm, and the Go module proxy, which keep every upload permanently. The new upload-private-paths check lists the npm package, the Go module zip, and the Docker build context offline before a release mutates anything, and the pypi CI template now builds the Python upload on the candidate commit and refuses the same paths in the sdist and wheel. Each refusal names the file and the fix for its ecosystem: a hatch or setuptools sdist exclusion, an .npmignore entry or a narrower files field, a stub go.mod in the private directory, or a .dockerignore entry. Re-run rlsbl scaffold to add the CI step.
  • Scaffold keeps the private paths out of every new upload. rlsbl scaffold merges the private-path entries into exclude under [tool.hatch.build.targets.sdist] of a hatchling pyproject.toml, renders them into a new .npmignore, writes a stub go.mod into .rlsbl/ and into each other private directory a Go module tracks files in, and creates a .dockerignore for a Docker project, so a newly scaffolded project passes the private-path refusal without hand edits. .npmignore and .dockerignore are created once and never rewritten, so an existing project adds the entries the refusal names by hand.
  • New rlsbl secrets sync-npm-token copies the npm token in ~/.npmrc into the NPM_TOKEN Actions secret after checking that npm accepts it (it prints the npm user). Without --all it sets the current repository's secret; with --all it sets every repository the gh account can see that already has an NPM_TOKEN secret, naming and skipping archived ones. The secret is never created, the token goes to gh secret set on stdin and is never printed, and the command is consequential.
  • New check npm-token-synced (tags release and preflight) refuses a release when npm does not accept the token in ~/.npmrc, or when the repository's NPM_TOKEN secret was last set before that token was created (from npm token list --json), so CI would publish with an older token that fails after the release has tagged. It applies when a CI publish pipeline authenticates with NPM_TOKEN, treats an undeterminable time as an error, and names rlsbl secrets sync-npm-token as the fix.

Fixes

  • An abandoned release attempt is no longer released as a first release. With version files bumped past the latest release and no archive or tag for the new number, rlsbl release run used to release that number as-is, ignoring the declared bump; it now refuses before changing anything and names rlsbl release abandon. Version files naming a version recorded never released are bumped from, a next version recorded never released is refused, and rlsbl rewrite project-name decides its effective version the same way. rlsbl release undo no longer reverts the release before an abandoned attempt when that attempt's version is recorded never released, and the in-progress refusals of release run, release undo, and the executor name the command that abandons an attempt.
  • Version files behind the latest release are refused. With 0.29.3 released and the version files at an unrecorded 0.29.1, rlsbl release run and batch planning used to release 0.29.1 as a first release (and rlsbl rewrite project-name recorded 0.29.1 as the effective version); they now refuse before changing anything, naming both numbers and saying the version files must name at least 0.29.3. Version files naming an unrecoverable version whose tag is gone are still refused by the release, and the refusal now says the archive cannot name the commit and asks for the tag to be restored at the commit it shipped from; rlsbl rewrite project-name records the declared bump from such a version instead of refusing.
  • The test block of .rlsbl/config.json is called "test settings" everywhere. An unknown key in a test.<target> block is now refused as "is not a recognized setting. Valid settings: ...", and the configuration reference and the support matrix say "test settings" (the matrix axis is now accepts_test_settings). The test key itself is unchanged.
  • The check reference lists changelog-entry, go-companion-tags, and prepush-manual-warning at the error severity they run at, and says what workspace-targets and dead-workspace-packages verify.
  • rlsbl changelog add, changelog amend, and changelog edit refuse a repeated --commits and name the one comma-separated flag that carries every value; --commits a --commits b used to record an entry covering b alone.
  • Every GitHub Release rlsbl creates passes --verify-tag, and repairs no longer move the "Latest" badge onto an old release. Without the flag, gh release create for a tag the remote does not have creates that tag at the default branch head and publishes the Release there, so release: published fires for a commit nobody released. The release flow, rlsbl release reconcile, the Release rewrite in rlsbl release scrub, and mirror publication now refuse instead, and the manual-creation command a failed release prints carries the flag too. Reconcile, the scrub's rewrite, and rlsbl monorepo mirror materializing a version create a Release with --latest=false unless it is newer than the repository's current Latest, so the repository keeps showing its newest release as Latest -- including a newest release whose own Release step failed and a repair created later; a real release keeps GitHub's default.
  • A release pushes each of its tags in a push of its own, the primary tag first. GitHub creates no events at all -- no workflow runs, no webhooks -- for a push that carries more than three tags, so a release with several Go module tags started nothing that listens for tag pushes. The retry after a failed tag push does the same.
  • The publish gate stops at its deadline when the GitHub checks API keeps failing. A failed checks request used to be retried without looking at the deadline, so a rate limit or an endpoint answering 404 held the gate job until GitHub's six-hour job limit, billed on private repositories. The gate now fails at its deadline naming the failing API, and every generated gate job carries its own timeout-minutes. Run rlsbl scaffold (or rlsbl monorepo sync in a workspace) to regenerate the publish workflow.
  • The scaffolded deploy.yml starts when a GitHub Release is published, not when a tag is pushed. The tag-push trigger never matched name@v1.2.3 or path/v1.2.3 tags, fired nothing for a push carrying more than three tags or for a tag created with a workflow's GITHUB_TOKEN, and had no publish gate. The workflow now starts on release: published plus manual dispatch at a tag, waits for CI through the publish gate, and runs once per tag. Run rlsbl scaffold to regenerate it.
  • rlsbl release yank, rlsbl release deprecate, and rlsbl watch read "the latest release" from the package's own release record. They used to ask GitHub for the repository's newest Release, which in a monorepo is whichever package released last: a package's own newest release could be yanked or deprecated instead of undone, and the watch notification linked another package's release.
  • A release refuses, before pushing anything, when its GitHub Release would start no publish workflow, and fails when the publish runs never started. GitHub starts nothing and reports no error when the tagged commit lacks the workflow file, the workflow or GitHub Actions is disabled, or the Release was created with a workflow's GITHUB_TOKEN; a release used to end in success anyway. Before pushing, a project that publishes from CI must carry a workflow that starts on release: published, GitHub Actions must be enabled, and every such workflow GitHub knows must be active, each refusal naming its fix. After the Release is created, on --watch and --no-watch alike, every such workflow at the tagged commit must show a run for the tag within five minutes, or the release exits 1 naming what it found and rlsbl release retry. rlsbl watch on a released commit and monorepo batch releases require the same, and rlsbl watch spells a monorepo member's tag under its own scheme.
  • rlsbl scaffold installs its git hooks where git runs them, and refuses a core.hooksPath that points elsewhere. It used to write the pre-push and post-rewrite hooks into the repository's own hooks directory and report them installed even when core.hooksPath made git run hooks from another directory, and from a linked worktree it wrote them where git never reads hooks at all. Hooks now go where git rev-parse --git-path hooks says git looks; a core.hooksPath naming any other directory refuses scaffold before it writes anything, naming the setting, the file that holds it, and the git config --file <file> --unset core.hooksPath that removes it.
  • A release with a customized pre-release hook resolves its external checks' option values for the member being released. It runs only the config-declared external checks then, and every value -- the external check's own and those of the checks it depends on -- came from the options of the directory the release stood in rather than from the member's.
  • A release from a private repository is refused before it pushes anything when its publishing needs a public repository. A Go library published from CI or any local Go pipeline would reach the Go module proxy, which cannot fetch a private module and caches every version it is asked about; a PyPI publish step with attestations on would record the repository's name, workflow, and commit in a public transparency log; npm build provenance needs a public source repository. The committed publish workflows count as well as the config: one that still asks the Go module proxy for the module or still publishes with --provenance is refused too. The refusal names each finding's fix -- "publish_mode": "none", rlsbl scaffold, or "provenance": false, with the regenerated workflow committed.
  • A release run from a workspace root runs the released member's external checks. It ran the external checks declared in the workspace root's config instead, because the check provider read the config of the current directory.
  • An unpushed batch finalize commit names the release that publishes it. When the push of a batch's finalize commit fails, the message says the next rlsbl monorepo release run publishes it with its candidate push, instead of telling the reader to push the branch by hand.
  • rlsbl release retry works without a hand-written retry.toml, and dispatches only at the release tag. The file it writes when none exists had an empty ref, so the command deleted it and exited, and the retry that a publish which never started names as its fix could not run. The file now carries the release tag as its ref. A ref other than the tag, such as a branch, is refused before anything is dispatched: a run dispatched at a branch cannot be tied to the release, so rlsbl watch would report that no run started and point at a second retry that publishes twice.
  • The scaffolded deploy workflow can succeed: rlsbl deploy accepts a Release's tag checkout when an allowed branch reaches its commit. The workflow checks out the published Release's tag, a detached HEAD, which rlsbl deploy always refused. On a detached HEAD the commit must now be reachable from one of the target's only_on branches as origin has them. The scaffolded checkout fetches with fetch-depth: 0 so the answer is there; re-run rlsbl scaffold and commit the regenerated deploy.yml. A commit no allowed branch reaches, and a checkout without the history to tell, are refused with the fix named. The deploy docs no longer describe a --force flag the command does not have.
  • rlsbl monorepo release run confirms that every member it releases started its publish runs. A member whose release completed inside its own first pass, and one whose release targets could not be resolved, were never checked, and a workspace below the git root looked for publish workflows in its own directory, found none, and confirmed nothing. Each released member is now confirmed from its release record, against the workflows at the git root.
  • rlsbl watch on a release older than the repository's Actions retention reports that its runs expired instead of failing. GitHub deletes workflow runs older than the retention setting, so the publish-start confirmation found none and failed, naming rlsbl release retry, which could publish a second time. When the retention or the Release's publish date cannot be read, the refusal names no retry.
  • A git repository nested inside another is no longer treated as part of the outer project. rlsbl looked for .rlsbl/ and .rlsbl-monorepo/ past the repository root, so a scratch repository under an rlsbl project took that project as its own, and rlsbl monorepo init there refused as "inside existing project". The search now stops at the enclosing repository's root.
  • A workspace member's checks no longer read the files of members nested inside it. library-lint, ruff-lint, ldflags-symbol, and the dependency checks walked every file under a member's directory, so a parent failed on a nested command's func main(), reported a nested module's .goreleaser.yml, and could "verify" an -X flag against a symbol in the nested module. The root member linted every other member the same way. Each member now reads only its own files, and a Go import of a nested module (kernel/vulkan) is attributed to that module rather than to the module enclosing it (kernel).
  • The monorepo publish router checks the CI of every member of a multi-member releasable. The generated router had one case branch per member, all with the releasable's tag prefix, so only the first member's CI was verified and a failing CI on any other member did not block the publish. Each prefix now has one branch whose check filter covers every member. Run rlsbl monorepo sync to regenerate publish.yml.
  • A tag belongs to a project only when that project's tag format renders it. Tags were matched by glob, so kernel/v* also claimed kernel/vulkan/v0.1.0, the tag of a member nested inside kernel, and the root's v* claimed a top-level Go member's vendor/v0.1.0. rlsbl status and rlsbl unreleased then refused with a release-record error whose printed fix could not clear it, monorepo extract of the parent planned to take the nested member's tags as its own, and the publish router ran the parent's publish jobs on the nested member's release. A dev-node root member with no release target no longer claims a converted repository's bare v0.x tags either. Run rlsbl monorepo sync to regenerate publish.yml.
  • rlsbl release scrub takes each rewritten Release's notes from the member that owns the tag. It matched tags to members by a {name}@ prefix, so a Go path tag (draw/cmd/v0.1.0) matched nobody and its notes came from whichever member's CHANGELOG.md happened to carry the version. Each member's tag format now decides; a tag no member's format renders is refused rather than guessed at, and the refusal names rlsbl transition record --non-version-tag, whose declaration makes the rewrite leave that tag alone.
  • A releasable tagged in the Go path form now tags every Go member. A releasable whose tag_format is gfx/v{version} got only its primary tag, so a nested Go module in the same releasable (gfx/shader) never got gfx/shader/v0.2.0 and its publish failed after tagging. Every publishing Go member now gets its own <path>/v<version> tag, pushed in a push of its own after the primary tag. A past release is owed only the tags of the members its release record lists, so unpublished-refs and release reconcile never ask for a tag at a commit that predates the member.
  • rlsbl scaffold in a member no longer deletes the merge bases of a member nested inside it. When a parent and its nested child belong to the same releasable, the child's scaffold merge bases live inside the parent's, and scaffolding the parent removed every one of them as orphans, leaving the tree dirty and the child's next scaffold without a merge base.
  • rlsbl monorepo add no longer leaves an untracked merge base, and resolves its path from the workspace root. Adding a member whose file already matched its template (a Go member's VERSION) seeded that file's merge base without committing it, leaving .rlsbl-monorepo/releasables/<name>/bases/<member>/VERSION untracked; rlsbl scaffold now commits every base it seeds. The path argument was also resolved against the current directory, so the command worked only from the workspace root; it is a repository-relative member path wherever the command runs.
  • member-pytest-config also guards members nested under a member with a conftest.py. It considered only a conftest.py at the workspace root, so a nested member whose tests would load an enclosing member's conftest.py passed. It now checks every enclosing member, and its finding names the member's pyproject.toml by path (it used to print <name>/pyproject.toml, which is wrong for a nested member) and the conftest.py the run would load.
  • A member's CI job no longer runs for changes inside the members nested in it, and never misses a change to a dependency. A parent's paths filter (draw/**) also matched every nested member's files, so a change in draw/cmd ran draw's CI. Each member's filter now excludes its nested members, except one whose directory holds a dependency of the member. The root member's filter follows the same rule: it used to exclude a member enclosing one of its dependencies, which silenced that dependency, because a paths-filter exclude cannot be taken back. router-filters-fresh fails in a workspace with nested members until rlsbl monorepo sync regenerates ci-router.yml.
  • rlsbl monorepo extract no longer deletes a member nested inside the releasable it extracts. Extracting tools planned to delete its whole directory, including tools/lint, a member of another releasable that stays, without naming it. The extract now refuses, naming the nested member and both ways out: extract that member first, or move it into the departing releasable. A mirrored releasable whose member encloses another member is refused the same way by rlsbl monorepo mirror and by rlsbl release run, before anything is written, because the mirror would publish the nested member's files.
  • go-companion-tags asks only about a version that was released. It read the releasable's version file, so a releasable that had not released yet was warned about missing companion tags for version 0.0.0. It now reads the latest released version from the release record, and expects a tag for each Go member that release's record lists.
  • ldflags-symbol no longer reads a workspace's scaffold base copies. In a workspace whose root member is a Go module, the check read the copies of members' .goreleaser.yml kept under .rlsbl-monorepo/ as live configuration and resolved their main: against that directory, so every release in the workspace failed on a symbol the real build declares.
  • A Go project requiring strictcli at its current module path now gets its schema regenerated at release. strictcli's Go module moved from github.com/smm-h/strictcli/go to github.com/stricttools/strictcli/go, and rlsbl recognized only the former, so a project requiring the new path was treated as not using strictcli and released with a stale .strictcli/schema.json. Both paths are now recognized.
  • rlsbl upstream adopt-tags pushes one ref per push. It moved every inherited tag in one atomic push that created every kept ref and deleted every tag from origin, and GitHub fires no events for a push deleting four or more tags, so the workflows and webhooks those deletions should start never ran. Each tag now moves on its own: its kept ref is pushed, then its tag is deleted from origin in a second push, then here. A push that fails stops the run with every earlier tag finished, and a re-run finishes the rest.
  • rlsbl monorepo add stops when scaffolding the member or syncing the workspace fails, and rlsbl monorepo remove refuses a path no member has. Both add failures were swallowed: the command exited 0 with the member registered and committed in workspace.toml but never scaffolded, or missing from the CI router. It now exits 1 naming the step that failed, with workspace.toml restored to what it was before the command, and commits the registration only after both steps succeed; fix what the step reported and re-run the add. remove of an unknown path printed a warning and exited 0, so a mistyped path looked like a successful removal; it now exits 1 naming every member's path, and matches a path as workspace.toml writes it, so web/ no longer removes the member at web.
  • rlsbl release scrub no longer fails in a repository that has released. The release checkout a release leaves at .git/rlsbl/release-checkout kept the history before the rewrite reachable, so safegit's verification found the scrubbed content still present and the scrub stopped with main rewritten locally and origin, its tags, and the release record untouched. The scrub now removes the checkout before rewriting, and the next release creates it afresh. When safegit fails after it has rewritten the local history, the rewrite is saved and nothing is pushed; remove what safegit reports still holds the old history, prune, and re-run the same command to finish the scrub.
  • rlsbl monorepo mirror works for a member the monorepo scaffolded. The member's .rlsbl/managed-files.json reached the mirror through the subtree split, but its merge bases live in its releasable's state directory, so the scaffold in the mirror refused the registry as a project scaffolded before merge-base tracking and the mirror never converged. The mirror now gives its scaffold the member's committed merge bases. A member whose registry has no committed bases is refused before anything is scaffolded, naming rlsbl scaffold in the member as the fix.
  • A failed rlsbl monorepo extract no longer strands the source repository. When the source-side monorepo sync failed, the departing members and their release state were already deleted and nothing was committed, and a re-run refused both the existing target and the dirty tree. The source step now deletes nothing until every other edit has succeeded, and a failure anywhere before its commit restores the source to its last commit; the error names the fix: fix the cause, delete the extracted repository, and re-run.
  • A release refuses an untidy Go module up front instead of aborting late with a misleading error. The release runs go mod tidy to refresh go.sum; when go.mod was not already tidy, that rewrote it, and the release stopped after its tests with "Unexpected modified files detected (possible concurrent change)". rlsbl release run and rlsbl release resume now check every Go module whose go.sum they refresh with go mod tidy -diff before anything mutates, and refuse with the diff, naming the fix: run go mod tidy in the module, commit go.mod and go.sum, and re-run. Checking needs Go 1.23 or later.
  • Nothing tells the workspace root member to run rlsbl scaffold any more. The workspace root is not scaffolded, so that advice did nothing: a root member missing its VERSION (or version.json) was told to run rlsbl scaffold first, and rlsbl scaffold at the root asked to be run again before skipping. The missing version file now names the hand edit that creates it, and rlsbl scaffold at the root says it does not scaffold the root.
  • rlsbl monorepo add stops when committing the registration fails. The commit of the new workspace.toml entry only warned when it failed, so the add exited 0 with the member registered but uncommitted. It now exits 1 naming the failed commit, with workspace.toml restored to what it was before the command, like a failed scaffold or sync; fix what refused the commit and re-run the add.
  • rlsbl release scrub finishes in a workspace whose releasable has nested Go modules. After re-pointing the tags, the scrub refused the tag it pushes for each nested Go module beside the releasable's own (draw/gpu/v0.1.0 beside draw/v0.1.0) as belonging to no releasable, and stopped before rewriting any GitHub Release. Those tags carry no GitHub Release, and the scrub now leaves them to the tag step.
  • A rlsbl release scrub that stopped after its own commit can be finished. When a push or a GitHub Release update failed after the scrub had committed its metadata, every re-run refused the saved scrub-result.json as stale, because the scrub commit had moved HEAD past the rewritten head, leaving origin, the tags, or the Releases unrepaired. Re-running the same command now recognizes the scrub's own commit and finishes from the last completed step.
  • A workspace root that publishes is asked for its publish_gate_check_regex before it releases. The root releasable's rlsbl release run and rlsbl monorepo add went through without it, and the next rlsbl monorepo sync, run by an extract or an add, refused the root publisher after it had shipped. Both now refuse before changing anything, naming the root releasable's config.json and the key to add.
  • A failed commit stops rlsbl monorepo release run instead of being reported as a warning. When committing the resolved batch plan or the archive of a finished batch failed, the batch went on or reported success with nothing recording the plan or the archive. It now stops, puts the files back as the branch has them, and names the re-run that finishes the batch.
  • rlsbl scaffold commits the rlsbl keyword it adds to package.json or pyproject.toml. The keyword edit was left uncommitted, also after rlsbl monorepo add, which scaffolds the member it adds. A manifest with no uncommitted changes of its own is now committed with the scaffold; a manifest with edits of its own is left for you to commit.
  • rlsbl check --releasable <name> checks only that releasable's changelog when it owns the workspace root. The changelog checks answered for every releasable in the workspace instead, so changelog-coverage listed commits that belong to other members, which rlsbl changelog add rightly refused for the named releasable, and printed a commit once per releasable on identical lines. Coverage lines from several changelogs now also name the releasable each is about.
  • A workspace root ignores rlsbl's run-state files. rlsbl scaffold does not run at the workspace root, so the root .gitignore never listed rlsbl's locks, in-progress release state, and scrub results, and a scrub left scrub-result.json untracked at the root. rlsbl monorepo sync now adds the missing entries to the root .gitignore, keeping the lines already there.
  • A release refuses a Go workspace whose go work sync would change a module's go.mod, before changing anything. When two workspace modules required one dependency at different versions, the release's go work sync raised the lower requirement and the release stopped late on unexpected modified files. The refusal now comes first, lists each requirement and the version it would become, and names the fix: run go work sync, commit, and re-run.
  • rlsbl release scrub refuses to run beside a release stopped mid-flight. It removed the release checkout and rewrote the history a stopped release's state records, stranding that release. It now refuses before changing anything, naming the state file and the fix: finish the release with rlsbl release resume, or give it up with rlsbl release abandon, then re-run the scrub.
  • rlsbl release scrub no longer strands a release that stops while the scrub waits for it, or finishes a stopped scrub for a different pattern. A release still running when the scrub started could stop while the scrub waited for its lock, and the scrub then removed its release checkout and rewrote its history; the stopped-release refusal is now asked again once the scrub holds the lock. A scrub stopped at its push was also finished by a re-run asking for another pattern or mode, which pushed the saved rewrite and reported the new scrub complete; that re-run is now refused, naming the arguments the saved scrub was started with.
  • rlsbl monorepo extract leaves edits made in the source while it runs alone. The extract checked the source clean before building the extracted repository, then edited the source without checking again: an edit another session made in between was committed with the extract, and when the source step failed, its rollback put that edit back as the last commit held it or deleted the new file. The source step now refuses such changes before writing anything in the source, naming each path and the fix: commit or set them aside, delete the extracted repository, and re-run.
  • A failed rlsbl monorepo add leaves the working tree and the history as it found them. The scaffold and the sync committed the new member's files, its releasable state, and a CI router that already ran its jobs, so a failed sync or registration commit left those commits behind for a member workspace.toml no longer declared. The add now commits what the registration, the scaffold, and the sync wrote as one commit, and a failure restores every path it changed and commits nothing. A file that had uncommitted changes before the add is left out of the commit and named, and --no-auto-commit prints the safegit commit that takes what the add wrote.
  • A failed rlsbl monorepo init no longer deletes a file it found in place. When its commit failed, the init removed every file it had written, including a root releasable config.json that was already there before the init overwrote it. It now puts the working tree back as it found it: what the init created is removed, and a file it overwrote gets its earlier contents back.
  • The re-run after a failed batch archive commit finishes the archive. When committing the archive of a finished rlsbl monorepo release run failed, the re-run the error named archived only the plan and left the batch file for you to delete, so the finished batch never got its archived batch file. A batch file unchanged since the plan was resolved from it is now archived by that re-run together with the plan, and the re-run exits 0; a batch file written or edited since is the next batch's and is left in place.
  • A push origin refuses during rlsbl release scrub or rlsbl release reconcile --apply is reported as a refusal, not as a moved remote. With the lease still holding, the error said the remote changed since the rewrite started, printing the same value as expected and current, and hid origin's own message. It now prints origin's refusal, says nothing moved the ref, and names the re-run that resumes from the push.
  • A Go release preflight that cannot run or fails names the fix for its cause. When the release's go mod tidy -diff or go work sync guard failed, the refusal printed go's error and no way out. It now names the fix go's output points to: warm the module cache with go mod download all or point GOPROXY at a reachable proxy when GOPROXY is off or the proxy is unreachable, release the sibling module first when a required version was never published, correct GOFLAGS when go cannot parse it, warm the cache after a timeout, put a working Go toolchain on PATH when go cannot start, and otherwise re-run the named go command to reproduce the error.
  • A failed lockfile re-lock stops the release before anything is pushed. When npm install --package-lock-only or uv lock failed after the version bump, the release printed a warning and pushed a candidate carrying a stale lockfile. It now refuses before the candidate push, with nothing pushed or tagged, and names the fix for the cause the tool printed: restore access to an unreachable registry, or release first the sibling whose required version was never published. Re-running rlsbl release run, or rlsbl release resume for a resumed release, continues it.
  • rlsbl release scrub keeps a release from starting between removing the release checkout and rewriting history. The scrub let go of the lock after removing the release checkout, so a release starting before safegit ran could re-create the checkout on the history being rewritten. The lock is now held from the removal until the scrub ends. Because the lock is a file in the working tree while held, a lock file git does not ignore is refused before anything is removed, naming the .gitignore line to add.
  • A Go program on strictcli's former module path is told how to migrate it. When the release's schema step met a Go program requiring github.com/smm-h/strictcli/go, the refusal named go get github.com/stricttools/strictcli/go@latest and go mod tidy, which does not move the program's imports. It now names the migration in order: rlsbl rewrite go-module-path --from-module github.com/smm-h/strictcli/go --to-module github.com/stricttools/strictcli/go, then go mod edit -droprequire=github.com/stricttools/strictcli/go, go get github.com/stricttools/strictcli/go@latest, and go mod tidy.
  • The sandboxed test runner copies only what git sees. scripts/test.sh (rendered from the test-sandbox template) copied the whole project directory, so gitignored bulk such as experiments/ could fill the temp filesystem before the suite started. It now copies tracked files, untracked files that are not ignored, and .git; a gitignored path the suite needs, such as a pre-warm stage directory, is declared in the new test_sandbox.carry_ignored key. Re-run rlsbl scaffold to pick it up.
  • A release refuses when a lockfile's tool is missing, instead of shipping the lockfile stale. A lockfile whose tool (uv, npm, go, or gradle) was not on PATH, or whose Gradle wrapper was missing, was skipped with a warning. The release now stops before anything is pushed, naming the tool to install and whether to re-run rlsbl release run or rlsbl release resume; a resumed release keeps its state.
  • rlsbl's own source distribution no longer ships its planning notes, release metadata, and agent instructions. The sdist carried todo/, .rlsbl/, stricttools/, CLAUDE.md, and other private paths; they are now excluded, and rlsbl's CI refuses the upload on the release commit if one returns.
  • A resumed workspace release adopts a commit that touches only a dev node. rlsbl release resume and rlsbl monorepo release run refused to finish a stopped release when a commit made after it stopped touched only a dev-node member (or anything else outside the releasable's territory), asking for a changelog entry that rlsbl changelog add refuses to write. The resume now asks only for the entries the changelog-coverage check asks for.
  • The "Next steps" rlsbl scaffold prints now name the current release flow (rlsbl release init, set the bump type in .rlsbl/releases/unreleased.toml, then rlsbl release run --watch) instead of the retired rlsbl release [patch|minor|major], no longer tell you to push to GitHub (the release pushes main itself), and on a multi-registry project list only the setup steps of the registries it actually publishes to
  • The upload-private-paths refusal names selfdoc's migration for a CLAUDE.md selfdoc generated. When a Go module's CLAUDE.md carries selfdoc's generated-file header, the named fix is to upgrade selfdoc, run selfdoc layout migrate --dry-run and then selfdoc layout migrate, which moves it to .claude/CLAUDE.md and commits, and then rlsbl scaffold for the stub go.mod in .claude/; moving it by hand left the next selfdoc gen writing it back at the root. A hand-written CLAUDE.md keeps the manual move.