Repository navigation
v0.130.0
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-publishis 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-publishis now refused as an unknown flag.- Every check is an option,
rlsbl:<check name>. An error check rankserror > warn > offand a warning checkwarn > 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>.tomlat the git root (creating the directory and its strictspec-ownedmanifest.toml), validates it with strictspec before writing, and commits it with the Autogenerated trailer;rlsbl options registryprints the registry, as data with--json. Every check run and every release apply the entries --offdoes not run the check,warnreports without blocking -- andrlsbl check --listshows each check's value and where it came from. Invalid entries stop every check run and every release.dep-floors,lint,format,type-check, andstrictspec-certificate-gatestay off until their options are switched on, andrlsbl:test-sandboxdecides whether the sandboxed test runner is rendered. Declaringinternal_dep_floors, achecks.<name>block,strictspec_gate, ortest_sandboxno longer turns anything on: each setting is required while its option is on and refused byconfig-schemaand every release while it is off, and the refusal names both fixes, including the exactrlsbl options setcommand. 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, andtestisolation-floorcounts the floor as adopted whenrlsbl:test-sandboxis 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_enforcedis retired, and thechangelog-format-versioncheck is removed. Thechangelog-format-version-gatecheck runs on every repository, and its optionrlsbl:changelog-format-version-gatedecides whether a line withoutformat_versionblocks:errorby default, and atwarnthe check still runs and reports.config-schemaand 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](theprepushtag) and[hooks.pre-release](thepreflighttag), so the installed hook names only its hook and a check an options entry softened towarnno longer blocks a push;rlsbl checkstays the full report and still fails on warnings.rlsbl check --hook <name>andrlsbl failing-checks(same selection flags, including--releasable) run a declared hook's selection. Runrlsbl scaffoldto upgrade an installed hook: it recognizes the previousrlsbl check --tag prepushhook 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 aWARNline 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 exceptinfrarefuses to run without a user-facing changelog entry, namingbump = "infra"for a release with only infrastructure changes. - The pgdesign target reads
pgdesign check --tag validation --jsoninstead of passing--ignore-warnings. A check with statusfailfails 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--jsonprints 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-dirtyis removed fromrlsbl release runandrlsbl 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-runreports both instead of refusing. A release that fails before its candidate push leaves the branch and the working tree as they were: there is nogit reset --hardanymore, and a refused candidate push takes its branch advance back the same way. Drop--allow-dirty/--no-allow-dirtyfrom every invocation.- Workspace member paths are accepted in one spelling only. A member path in
workspace.tomlis 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 byrlsbl monorepo addandrlsbl monorepo absorb. The load refusal lists every such path at once, each with the line to change in.rlsbl-monorepo/workspace.toml(path = "python/"becomespath = "python"), so a workspace written with trailing slashes is fixed in one pass. - Two new release-blocking Go tag checks.
path-tag-format-go-memberrefuses a releasable whosetag_formatis 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-suffixrefuses ago.modmodule path ending in/v2or a higher major, naming therlsbl rewrite go-module-pathinvocation that drops the suffix. Both run in a release's preflight, so an affected workspace stops releasing until it is fixed. workspace-unregisteredfinds an undeclared project at any depth. It looked only at top-level directories, so ago.modorpackage.jsonnested 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 atests/,testdata/, orfixtures/folder and inside a scratch directory at a member's root are skipped, and apackage.jsonmarkedprivate, 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-pathrenames only the module it names. RenamingM/drawalso renamedM/draw/cmd, a module nested under it with a go.mod of its own, including when run as the fixrlsbl monorepo extractprints. 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
dirpointing into a nested member (sdk's hook with"dir": "python", wheresdk/pythonis 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 adirwith 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'srequireline after a sibling module released, andgo install <module>@<version>rejects a module carrying areplace, so a released module could ship pinned to a superseded sibling or unusable from outside.go-workspace-require-currentrefuses a require below the sibling's latest release and prints thego get <module>@v<version>to run;go-workspace-replacerefuses a releasable member'sreplaceinto the workspace and prints the migration to a committedgo.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.jsonfrom<app> help --json. strictcli replaced--dump-schemawithhelp --json, which prints the help document on stdout; the release runs it (uv run <script>,go run <main package>, ornode <bin>), writes the document byte for byte, and stamps the release version into it. A program on a strictcli without ahelpcommand (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-schemaand 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-releasablenow requires--publish-modeand writes.rlsbl-monorepo/releasables/<name>/config.json, with the root's targets, default pipelines, and a required--publish-gate-check-regexwhen the root publishes. A release,rlsbl check, and theroot-rlsbl-conflictfinding name that file for the root member instead of a root.rlsbl/config.json, which the workspace refuses. A failed commit now stopsmonorepo initand removes what it wrote.
Features
rlsbl release abandon. Records an abandoned release attempt's version as never released (its release archive withnever_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 abandonfinishes 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 explanationrlsbl release rungives, and a tag for the version with no archive (locally or on origin) now points atrlsbl release backfill, which adopts the tag as the release it is evidence of, instead ofrlsbl release undo.rlsbl rewrite uv-path-sourcesandrlsbl monorepo extractswitchrlsbl:dep-floorson wherever they writeinternal_dep_floorsand the option is off: an entry with current and idealerror, 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 newprivate-repo-publishingcheck reports publishing that needs a public repository. PyPI attestations record the repository's name, workflow, and commit in a public transparency log;rlsbl scaffoldandrlsbl monorepo syncnow switch them off when the repository is private and keep the default for a public one. The check (tagrelease) 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-tagsmoves the tags it inherited, and changelog coverage leaves upstream's history out. A fork declares its upstream in.strictmetadata/upstream/upstream.toml(strictspec'supstreamschema:host,owner,repo, andbranch); nothing is inferred from a git remote.rlsbl upstream adopt-tagsmoves every tag upstream has at the same name and object torefs/tags-of/<host>/<owner>/<repo>/<tag>, keeping its object, pushes those refs to origin, and deletes the tags fromrefs/tagson 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, andrlsbl statusleave out every commit reachable fromrefs/upstream/<host>/<owner>/<repo>/<branch>or a kept tag, and a fork missing that ref is refused with thegit fetch --no-tagscommands that restore it. In a fork, the refusal of an empty release record beside version tags namesrlsbl upstream adopt-tagsbeforerlsbl release backfill. - A member's test runner skips the members nested inside it. A Python member running
pytestin its own directory also collected a nested member's tests and failed on that member's dependencies.rlsbl scaffoldnow writes a path-exact--ignore=<path>for each nested member into[tool.pytest.ini_options] addopts(and each path into a Deno member'sexclude), and the newnested-member-runner-exclusioncheck, which also runs in a release's preflight, fails while one is missing. pytest resolves--ignoreagainst 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 packof 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 newnested-member-upload-contentscheck 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; runrlsbl scaffoldto add that step. - The new
nested-member-uv-sourcescheck 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'spyproject.tomlresolves. 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-onlyfiles had reached PyPI, npm, and the Go module proxy, which keep every upload permanently. The newupload-private-pathscheck 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.npmignoreentry or a narrowerfilesfield, a stubgo.modin the private directory, or a.dockerignoreentry. Re-runrlsbl scaffoldto add the CI step. - Scaffold keeps the private paths out of every new upload.
rlsbl scaffoldmerges the private-path entries intoexcludeunder[tool.hatch.build.targets.sdist]of a hatchlingpyproject.toml, renders them into a new.npmignore, writes a stubgo.modinto.rlsbl/and into each other private directory a Go module tracks files in, and creates a.dockerignorefor a Docker project, so a newly scaffolded project passes the private-path refusal without hand edits..npmignoreand.dockerignoreare created once and never rewritten, so an existing project adds the entries the refusal names by hand. - New
rlsbl secrets sync-npm-tokencopies the npm token in~/.npmrcinto theNPM_TOKENActions secret after checking that npm accepts it (it prints the npm user). Without--allit sets the current repository's secret; with--allit sets every repository the gh account can see that already has anNPM_TOKENsecret, naming and skipping archived ones. The secret is never created, the token goes togh secret seton stdin and is never printed, and the command is consequential. - New check
npm-token-synced(tagsreleaseandpreflight) refuses a release when npm does not accept the token in~/.npmrc, or when the repository'sNPM_TOKENsecret was last set before that token was created (fromnpm 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 withNPM_TOKEN, treats an undeterminable time as an error, and namesrlsbl secrets sync-npm-tokenas 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 runused to release that number as-is, ignoring the declared bump; it now refuses before changing anything and namesrlsbl release abandon. Version files naming a version recorded never released are bumped from, a next version recorded never released is refused, andrlsbl rewrite project-namedecides its effective version the same way.rlsbl release undono longer reverts the release before an abandoned attempt when that attempt's version is recorded never released, and the in-progress refusals ofrelease 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 runand batch planning used to release 0.29.1 as a first release (andrlsbl rewrite project-namerecorded 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-namerecords the declared bump from such a version instead of refusing. - The
testblock of.rlsbl/config.jsonis called "test settings" everywhere. An unknown key in atest.<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 nowaccepts_test_settings). Thetestkey itself is unchanged. - The check reference lists
changelog-entry,go-companion-tags, andprepush-manual-warningat the error severity they run at, and says whatworkspace-targetsanddead-workspace-packagesverify. rlsbl changelog add,changelog amend, andchangelog editrefuse a repeated--commitsand name the one comma-separated flag that carries every value;--commits a --commits bused to record an entry coveringbalone.- 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 createfor a tag the remote does not have creates that tag at the default branch head and publishes the Release there, sorelease: publishedfires for a commit nobody released. The release flow,rlsbl release reconcile, the Release rewrite inrlsbl 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, andrlsbl monorepo mirrormaterializing a version create a Release with--latest=falseunless 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. Runrlsbl scaffold(orrlsbl monorepo syncin a workspace) to regenerate the publish workflow. - The scaffolded
deploy.ymlstarts when a GitHub Release is published, not when a tag is pushed. The tag-push trigger never matchedname@v1.2.3orpath/v1.2.3tags, fired nothing for a push carrying more than three tags or for a tag created with a workflow'sGITHUB_TOKEN, and had no publish gate. The workflow now starts onrelease: publishedplus manual dispatch at a tag, waits for CI through the publish gate, and runs once per tag. Runrlsbl scaffoldto regenerate it. rlsbl release yank,rlsbl release deprecate, andrlsbl watchread "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 onrelease: 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--watchand--no-watchalike, 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 andrlsbl release retry.rlsbl watchon a released commit and monorepo batch releases require the same, andrlsbl watchspells a monorepo member's tag under its own scheme. rlsbl scaffoldinstalls its git hooks where git runs them, and refuses acore.hooksPaththat 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 whencore.hooksPathmade git run hooks from another directory, and from a linked worktree it wrote them where git never reads hooks at all. Hooks now go wheregit rev-parse --git-path hookssays git looks; acore.hooksPathnaming any other directory refuses scaffold before it writes anything, naming the setting, the file that holds it, and thegit config --file <file> --unset core.hooksPaththat 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
--provenanceis 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 runpublishes it with its candidate push, instead of telling the reader to push the branch by hand. rlsbl release retryworks without a hand-writtenretry.toml, and dispatches only at the release tag. The file it writes when none exists had an emptyref, 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 itsref. Arefother 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, sorlsbl watchwould report that no run started and point at a second retry that publishes twice.- The scaffolded deploy workflow can succeed:
rlsbl deployaccepts a Release's tag checkout when an allowed branch reaches its commit. The workflow checks out the published Release's tag, a detached HEAD, whichrlsbl deployalways refused. On a detached HEAD the commit must now be reachable from one of the target'sonly_onbranches as origin has them. The scaffolded checkout fetches withfetch-depth: 0so the answer is there; re-runrlsbl scaffoldand commit the regenerateddeploy.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--forceflag the command does not have. rlsbl monorepo release runconfirms 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 watchon 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, namingrlsbl 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, andrlsbl monorepo initthere 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'sfunc main(), reported a nested module's.goreleaser.yml, and could "verify" an-Xflag 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
casebranch 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. Runrlsbl monorepo syncto regeneratepublish.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 claimedkernel/vulkan/v0.1.0, the tag of a member nested insidekernel, and the root'sv*claimed a top-level Go member'svendor/v0.1.0.rlsbl statusandrlsbl unreleasedthen refused with a release-record error whose printed fix could not clear it,monorepo extractof 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 barev0.xtags either. Runrlsbl monorepo syncto regeneratepublish.yml. rlsbl release scrubtakes 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'sCHANGELOG.mdhappened 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 namesrlsbl 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_formatisgfx/v{version}got only its primary tag, so a nested Go module in the same releasable (gfx/shader) never gotgfx/shader/v0.2.0and 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, sounpublished-refsandrelease reconcilenever ask for a tag at a commit that predates the member. rlsbl scaffoldin 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 addno 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'sVERSION) seeded that file's merge base without committing it, leaving.rlsbl-monorepo/releasables/<name>/bases/<member>/VERSIONuntracked;rlsbl scaffoldnow 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-configalso guards members nested under a member with aconftest.py. It considered only aconftest.pyat the workspace root, so a nested member whose tests would load an enclosing member'sconftest.pypassed. It now checks every enclosing member, and its finding names the member'spyproject.tomlby path (it used to print<name>/pyproject.toml, which is wrong for a nested member) and theconftest.pythe 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 indraw/cmdrandraw'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-freshfails in a workspace with nested members untilrlsbl monorepo syncregeneratesci-router.yml. rlsbl monorepo extractno longer deletes a member nested inside the releasable it extracts. Extractingtoolsplanned to delete its whole directory, includingtools/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 byrlsbl monorepo mirrorand byrlsbl release run, before anything is written, because the mirror would publish the nested member's files.go-companion-tagsasks 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 version0.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-symbolno 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.ymlkept under.rlsbl-monorepo/as live configuration and resolved theirmain: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/gotogithub.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-tagspushes 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 addstops when scaffolding the member or syncing the workspace fails, andrlsbl monorepo removerefuses a path no member has. Both add failures were swallowed: the command exited 0 with the member registered and committed inworkspace.tomlbut never scaffolded, or missing from the CI router. It now exits 1 naming the step that failed, withworkspace.tomlrestored 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.removeof 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 asworkspace.tomlwrites it, soweb/no longer removes the member atweb.rlsbl release scrubno longer fails in a repository that has released. The release checkout a release leaves at.git/rlsbl/release-checkoutkept the history before the rewrite reachable, so safegit's verification found the scrubbed content still present and the scrub stopped withmainrewritten 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 mirrorworks for a member the monorepo scaffolded. The member's.rlsbl/managed-files.jsonreached 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, namingrlsbl scaffoldin the member as the fix.- A failed
rlsbl monorepo extractno longer strands the source repository. When the source-sidemonorepo syncfailed, 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 tidyto refreshgo.sum; whengo.modwas not already tidy, that rewrote it, and the release stopped after its tests with "Unexpected modified files detected (possible concurrent change)".rlsbl release runandrlsbl release resumenow check every Go module whosego.sumthey refresh withgo mod tidy -diffbefore anything mutates, and refuse with the diff, naming the fix: rungo mod tidyin the module, commitgo.modandgo.sum, and re-run. Checking needs Go 1.23 or later. - Nothing tells the workspace root member to run
rlsbl scaffoldany more. The workspace root is not scaffolded, so that advice did nothing: a root member missing itsVERSION(orversion.json) was told to runrlsbl scaffoldfirst, andrlsbl scaffoldat the root asked to be run again before skipping. The missing version file now names the hand edit that creates it, andrlsbl scaffoldat the root says it does not scaffold the root. rlsbl monorepo addstops when committing the registration fails. The commit of the newworkspace.tomlentry only warned when it failed, so the add exited 0 with the member registered but uncommitted. It now exits 1 naming the failed commit, withworkspace.tomlrestored 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 scrubfinishes 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.0besidedraw/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 scrubthat 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 savedscrub-result.jsonas stale, because the scrub commit had movedHEADpast 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_regexbefore it releases. The root releasable'srlsbl release runandrlsbl monorepo addwent through without it, and the nextrlsbl 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'sconfig.jsonand the key to add. - A failed commit stops
rlsbl monorepo release runinstead 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 scaffoldcommits therlsblkeyword it adds topackage.jsonorpyproject.toml. The keyword edit was left uncommitted, also afterrlsbl 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, sochangelog-coveragelisted commits that belong to other members, whichrlsbl changelog addrightly 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 scaffolddoes not run at the workspace root, so the root.gitignorenever listed rlsbl's locks, in-progress release state, and scrub results, and a scrub leftscrub-result.jsonuntracked at the root.rlsbl monorepo syncnow adds the missing entries to the root.gitignore, keeping the lines already there. - A release refuses a Go workspace whose
go work syncwould change a module'sgo.mod, before changing anything. When two workspace modules required one dependency at different versions, the release'sgo work syncraised 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: rungo work sync, commit, and re-run. rlsbl release scrubrefuses 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 withrlsbl release resume, or give it up withrlsbl release abandon, then re-run the scrub.rlsbl release scrubno 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 extractleaves 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 addleaves 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 memberworkspace.tomlno 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-commitprints thesafegit committhat takes what the add wrote. - A failed
rlsbl monorepo initno longer deletes a file it found in place. When its commit failed, the init removed every file it had written, including a root releasableconfig.jsonthat 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 runfailed, 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 scruborrlsbl release reconcile --applyis 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 -difforgo work syncguard 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 withgo mod download allor point GOPROXY at a reachable proxy when GOPROXY isoffor 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 whengocannot 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-onlyoruv lockfailed 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-runningrlsbl release run, orrlsbl release resumefor a resumed release, continues it. rlsbl release scrubkeeps 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.gitignoreline 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 namedgo get github.com/stricttools/strictcli/go@latestandgo 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, thengo mod edit -droprequire=github.com/stricttools/strictcli/go,go get github.com/stricttools/strictcli/go@latest, andgo 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 asexperiments/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 newtest_sandbox.carry_ignoredkey. Re-runrlsbl scaffoldto 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 runorrlsbl 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 resumeandrlsbl monorepo release runrefused 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 thatrlsbl changelog addrefuses to write. The resume now asks only for the entries thechangelog-coveragecheck asks for. - The "Next steps"
rlsbl scaffoldprints now name the current release flow (rlsbl release init, set the bump type in.rlsbl/releases/unreleased.toml, thenrlsbl release run --watch) instead of the retiredrlsbl 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-pathsrefusal names selfdoc's migration for a CLAUDE.md selfdoc generated. When a Go module'sCLAUDE.mdcarries selfdoc's generated-file header, the named fix is to upgrade selfdoc, runselfdoc layout migrate --dry-runand thenselfdoc layout migrate, which moves it to.claude/CLAUDE.mdand commits, and thenrlsbl scaffoldfor the stubgo.modin.claude/; moving it by hand left the nextselfdoc genwriting it back at the root. A hand-writtenCLAUDE.mdkeeps the manual move.