Skip to content

v0.111.0

Choose a tag to compare

@smm-h smm-h released this 05 Aug 14:26
· 3239 commits to main since this release

A release can no longer burn a version number: main is the candidate, CI gates the tag, and a drift guard refuses to ship commits the release did not create -- alongside the effects regime, release reconcile, and the removal of dev-branch releases, rlsbl push, the watch daemon, cargo publishing, changeset-file mode and every timeout environment variable

Context

The centrepiece is a reordering. rlsbl used to bump the version, commit, tag, push and create the GitHub Release, and only then find out whether CI agreed. When it did not, the version number was spent: a tag and a Release existed for a commit that could not publish, and the only way out was to skip a number and try again. The order is now candidate-first. The version-bump commit goes to the release branch untagged, rlsbl waits for CI on exactly that commit, and only a green verdict produces the tag, the Release and the changelog finalization. A red CI leaves you with an ordinary branch you can fix forward on, at the same version, as many times as it takes. Batch releases work the same way with one wait for the whole batch -- and one push, because the CI router filters paths per push, so a push per member would leave every other member's job skipped on the commit its tag was about to land on.

Two gates that were supposed to answer the same question, and did not, are now one predicate. The release gate judged the workflow RUN; the publish gate judged the releasing project's own CHECK RUNS. In a monorepo those come apart whenever the paths filter finds no change under a project: its job concludes skipped, the run still concludes success, so the release tagged and the publish gate then refused the same commit. Both sides now ask "did this project's CI pass here?", only success passes, and a skipped or absent check is a hard error that names the filter mismatch.

The forward drift guard is the other half of the rollback guard that already existed. A release pins HEAD when it starts and re-checks the branch at four points; any commit in that window it did not create -- a concurrent session sharing the worktree, most likely -- aborts the release by name instead of silently shipping under the tag.

A lot was deleted, and the deletions share a rationale: every one of them was a second way to do something rlsbl already did once. Dev-branch releases and rlsbl push are gone, so a release starts from a release branch or it does not start. RLSBL_RELEASE_PUSH is gone, replaced by an internal --no-verify on release pushes and a pre-push hook that knows which ref namespaces it owns. The timeout environment variables are gone in favour of config keys and explicit flags -- an env var that silently changes how long a release waits is exactly the kind of invisible state this tool exists to remove. The detached watch daemon is gone; the CI wait is in-process now, which is what the new ordering needed anyway. Cargo publishing and the crates.io launcher are gone because nothing in the fleet ships a crate. Changeset-file coverage mode is gone because no configuration ever selected it.

rlsbl also moved onto strictcli's effects regime as its reference consumer. Every subprocess launch, filesystem mutation and network call now routes through one internal module with a test that fails if a new one appears outside it, and every command is classified, so --dry-run records instead of acting. Confirmation is declared per command rather than inferred from "this mutates something", and the flag that skips it is --approve-consequential; --yes is gone. rlsbl monorepo snapshot --check split into snapshot-check for the same reason: one command cannot be both a pure read and a write.

Finally, rlsbl release reconcile closes the loop on history rewrites. safegit stopped requiring a handshake before rewriting a release-managed repository, so rlsbl detects the damage afterwards instead -- the changelog's hash resolution fails loudly, changelog remap --from-journal repairs the entries from safegit's journal, and release reconcile re-pushes the moved tags and recreates their Releases. It is fail-closed: a tag whose divergence the journal cannot explain is an error, never a force-push. This is why the release now requires safegit 0.25.0 or newer.

Breaking

  • Removed the detached watch daemon. --watch-async (on release run, release resume, release retry, and monorepo release run) and rlsbl watch --stop are gone, along with the internal --as-daemon-child mode and the per-commit pidfile/log files under .rlsbl/. CI watching is now always in-process: pass --watch to block, or --no-watch and run rlsbl watch <sha> yourself later.
  • Dev-branch releases and rlsbl push removed. A release can only start from a release branch — rlsbl release run and rlsbl monorepo release run now hard-error anywhere else instead of fast-forward merging a dev branch into main. The rlsbl push command is gone (pre-push-check stays). Pushing a release branch by hand is now a hard error rather than a warning, and the RLSBL_RELEASE_PUSH bypass variable is deleted: release-internal pushes use git push --no-verify. The generated pre-push hook is namespace-aware — it enforces on refs/heads/* and exits 0 for refs/tags/* and refs/backups/*; re-run rlsbl scaffold to pick it up.
  • Timeout environment variables removed. RLSBL_PUSH_TIMEOUT, RLSBL_CHECK_TIMEOUT, RLSBL_HOOK_TIMEOUT, RLSBL_BUILD_TIMEOUT and per-target RLSBL_BUILD_TIMEOUT_<TARGET> no longer do anything. Declare timeouts with the push_timeout, check_timeout, hook_timeout, and build_timeout keys in .rlsbl/config.json, or pass the new --push-timeout flag to release run, release resume, and monorepo release run. Shipped defaults are raised: push 120s to 300s, check 120s to 900s.
  • Cargo publishing target and crates.io launcher removed. The cargo release target and cargo publish pipeline no longer exist, along with the crates-wrapper launcher for Go binaries, the crates.io branches of check-name, claim-name, and release yank, and the cargo CI/publish templates. Rust projects are no longer an rlsbl release target; a Cargo.toml in the repo is still recognized by the plain target's manifest detection.
  • Changeset-file coverage mode removed. The coverage_unit config key is deleted along with the changeset-file mode it selected: there are no pending changelog files, changelog add always appends to unreleased.jsonl, and release finalization, undo, the pre-push coverage check, and the changelog checks are single-mode. Remove coverage_unit from .rlsbl/config.json -- it is now a dead key.
  • Releases now gate on CI before anything irreversible happens. rlsbl release run pushes the version-bump commit to the release branch untagged, waits for the repository's own CI to go green on exactly that commit, and only then finalizes the changelog, tags the verified commit, pushes the tag and creates the GitHub Release. A red CI leaves no tag, no GitHub Release and no finalized changelog behind: the version number is not burnt, and the fix lands forward on the same version with rlsbl release resume. Repositories with no push-triggered workflow proceed without a gate; a repository that declares push-triggered CI but produces no runs for the candidate is a hard error.
  • Batch releases gate on one CI wait for the whole batch. rlsbl monorepo release run pushes every member's version-bump commit untagged first, waits once for CI on the resulting branch tip, and only then finalizes, tags and releases each member — so every member tag points at the same CI-verified commit. A red batch leaves no tag, no GitHub Release and no finalized changelog for any member, so no version number is burnt anywhere in the batch.
  • rlsbl release scrub now requires safegit >= 0.25.0. It no longer sets the RLSBL_SCRUB_ORCHESTRATED handshake (safegit 0.25.0 stopped requiring it) and passes --approve-consequential explicitly to safegit's three scrub modes, whose destructive confirmation is no longer answered by --json. A history rewrite performed outside release scrub is now caught by the changelog hash checks and repaired with rlsbl changelog remap --from-journal and rlsbl release reconcile instead of being blocked up front.
  • Breaking: rlsbl runs on strictcli's effects regime. --dry-run, --approve-consequential, --quiet and the new --verbose are framework-owned flags now — rlsbl no longer declares them, and none of them has a short form, so -y is gone (there is no --yes on a strictcli app at all). Every command declares whether it is read_only or mutating, and that classification decides one thing only: whether --dry-run records its effects instead of performing them. Confirmation is a separate, per-command declaration — a command prompts before it runs only if it declares itself consequential, and --approve-consequential skips that prompt. A consequential command exits 1 on a non-interactive stdin unless the flag is passed, so a script, hook or CI step that drives one of rlsbl's consequential commands must pass --approve-consequential; everything else runs uninterrupted. Under --dry-run mutations are recorded rather than performed and printed as a numbered would-do log — including subprocesses: a preview no longer runs your release hooks, your tests, or any child command.
  • Breaking: rlsbl monorepo snapshot --check is now rlsbl monorepo snapshot-check. One command could not be both: verifying the artifact is a pure read, regenerating it writes and commits. The writer previews completely under --dry-run — it decides staleness before writing, so the preview names both the write and the commit — and the checker never writes anything at all. CI steps using --check must switch to the new command.
  • Breaking: three commands stopped asking their own confirmation questions. claim-name, release deprecate and release undo each prompted on top of the framework's confirmation, in different words, with their own non-interactive error text. There is one confirmation per consequential command now, and the flag that skips it is --approve-consequential. release undo also asked a second question halfway through a rollback — "Push changes to remote?" — whose default left the remote holding the release it had just undone locally; it now pushes as part of the rollback it was already authorized to perform.
  • Only genuinely dangerous commands ask for confirmation now. strictcli replaced the inferred "every mutating command prompts" rule with a declared consequential, so 15 rlsbl commands ask before running -- claim-name, deploy, release run/resume/retry/undo/deprecate/yank/scrub/reconcile, monorepo release run/mirror/absorb/extract/extract-releasable -- and everything else (commit, scaffold, changelog add, monorepo sync, dev install, ...) runs without interruption. The skip flag is --approve-consequential; --yes no longer exists. claim-name gains --force-publish for the separate decision of publishing despite a taken-name check.

Features

  • npm launcher first-run download mode. Launcher pipelines now take a required download key ("first-run" or "postinstall"). In first-run mode the npm wrapper does zero network I/O at install time and lazily downloads its checksum-verified binary on the first CLI invocation, caching it under a platform cache dir; postinstall keeps the install-time download. PyPI launchers may only use first-run.
  • strictspec certificate deploy gate. A new opt-in preflight check, strictspec-certificate-gate, consumes a strictspec strictspec diff certificate as a format_version deploy gate. Enable it by adding a strictspec_gate section (a certificate path, and optionally an adjudication file) to .rlsbl/config.json; projects without the section are unaffected. A violated claim blocks the release, corpus-supported/proven claims pass, and any otherwise-unsupported claim must be discharged by a committed adjudication file. A configured-but-missing or malformed certificate or adjudication file is a hard error.
  • Release files are validated with strictspec. .rlsbl/releases/unreleased.toml is now checked against a strictspec schema before parsing, so a malformed file, an invalid bump/mode enum value, an unknown key, or overlapping include/exclude targets is rejected with precise, field-level diagnostics.
  • External checks now receive the release context. Both freeform and structured external_checks subprocesses run with RLSBL_PROJECT_ROOT, RLSBL_LAST_TAG (empty string on a first release) and RLSBL_UNRELEASED_RANGE in their environment, resolved through the project's own monorepo tag glob and computed once per check run. Release-diff gates no longer need their own git describe. See the availability matrix in the configuration docs.
  • The pre-release channel is documented. The release-workflow docs now cover preid end to end: entering a channel with a normal bump, advancing and promoting with bump = "prerelease", stabilizing with preid = "stable", the alpha < beta < rc < stable ordering rules, and what changes downstream (GitHub pre-release flag, npm dist-tag, changelog file naming). rlsbl release init scaffolds the key with an explanation.
  • Changelog lines carry a format_version. Every line rlsbl changelog add writes now records a format_version and is validated against a strictspec schema. Enforcement is opt-in per repo: while changelog_format_version_enforced is absent from .rlsbl/config.json the changelog-format-version check warns; set it to true and changelog-format-version-gate hard-fails any changelog line missing the field, or to false to stay in legacy (mixed-format-tolerant) mode deliberately.
  • The documentation site is complete. Every public module in the package now carries a docstring, so the generated API reference documents 100% of symbols instead of leaving pages blank, and the scaffold, release-workflow, and other guide pages gained worked end-to-end usage examples.
  • --ci-timeout, --check-timeout and --hook-timeout are now flags. rlsbl release run, rlsbl release resume and rlsbl monorepo release run accept all four release timeouts on the command line, matching the existing --push-timeout pattern: pass the flag to override for one invocation, or set the matching config key in .rlsbl/config.json. Previously only --push-timeout had a flag, so a slow preflight suite or release hook could only be accommodated by editing the config file.
  • A release refuses to ship commits it did not create. rlsbl release run pins HEAD when it starts and checks the branch against its own commit trail at four points — entry, candidate push, right after the CI gate, and the final push. A commit that lands on the release branch mid-flight (a concurrent session, an editor auto-commit) aborts the release by name, showing each foreign SHA and its subject, instead of silently joining the release under its changelog. Nothing is rolled back: the foreign work is preserved, and the version survives for a clean re-run.
  • New command: rlsbl release reconcile. Repairs the release metadata a history rewrite invalidates but leaves behind — the remote's tags and the GitHub Releases attached to them. It reads safegit's rewrite journal to work out exactly which tags moved, force-pushes them with explicit leases, and recreates their Releases from the changelog. Usable after ANY out-of-band rewrite (a raw safegit scrub, git filter-repo, someone else's rewrite), not just one driven by rlsbl release scrub. Fail-closed: a tag whose divergence the journal cannot explain is a hard error rather than a force-push. --dry-run prints the plan.
  • The changelog docs explain the auto-commit's HEAD move. rlsbl changelog add commits unreleased.jsonl, which moves HEAD onto the changelog commit — so a following git commit --amend amends the wrong thing. The changelog reference now names the hazard, shows the exact sequence that produces it, and gives both ways out: record the entry last, or defer the commit with --no-commit.
  • Sandboxed test runners are now scaffolded. Declare a test_sandbox section in .rlsbl/config.json and rlsbl scaffold emits an executable bubblewrap runner script: the repo bound read-only, the suite in a throwaway writable copy, no network, and only the toolchain caches you list. It exports STRICTTEST_SANDBOX=1 for the stricttest floor.
  • New check: stricttest-floor. Once a repo adopts the sandboxed test runner (or the stricttest plugin), rlsbl check and the release preflight fail on a missing or non-executable runner, an incomplete test_sandbox section, or a CI workflow that claims to use the runner but does not. Repos that have not adopted it see a visible skip.
  • Docs: the sandboxed test runner and the checks page. docs/configuration.md documents the new test_sandbox section field by field, and the checks reference now lists the five project checks it had been missing.
  • Documentation corrections: the release workflow page now describes main-as-candidate ordering (candidate push, CI gate, fix-forward recovery with rlsbl release resume) instead of the old commit-tag-push flow; ci_timeout and the --ci-timeout/--check-timeout/--hook-timeout overrides are documented; the changelog and scaffold pages name the real --no-auto-commit flag; and the checks reference lists every check with correct per-tag counts.

Fixes

  • workspace-unbuildable no longer false-fails polyglot monorepos. When the repo root is not a uv workspace (no root pyproject.toml), the check now runs uv sync in each pypi project's own directory instead of a root-level uv sync --all-packages that died with "No pyproject.toml found". Repos that do have a root uv workspace are unchanged.
  • npm publish workflows now install dependencies first. All four npm publish templates (npm, launcher, pnpm, yarn) run a lockfile-strict install before publishing, so a package whose prepack script builds no longer fails at publish time with a missing dev toolchain. Re-run rlsbl scaffold (or rlsbl monorepo sync) to pick it up; the package must have a committed lockfile.
  • First monorepo releases no longer deadlock the publish gate. Each releasable member's CI router paths filter now includes the releasable's CHANGELOG.md, so a release commit that touches only releasable metadata still triggers the project's CI and the publish gate sees a real conclusion instead of a skipped check. Re-run rlsbl monorepo sync to regenerate the router.
  • Go releases and launcher shims now target the prefixed monorepo tag. The generated Go publish job builds with goreleaser and uploads the archives and checksums.txt to the real release tag itself, instead of delegating to goreleaser's publisher (which cannot address a <name>@vX.Y.Z tag and attached assets to a bare-vX.Y.Z Release nobody reads). Launcher shims gain a tagPrefix template var, so their download URLs resolve in prefixed-tag repos. Re-run rlsbl scaffold to regenerate.
  • The publish gate's CI-SHA marker is always present. The rlsbl-ci-sha marker is now written into the GitHub Release notes even when the Release already exists (a resumed release, or one created out of band): it is added, or an outdated one replaced, instead of being skipped. The gate no longer falls back to gating on an arbitrary commit.
  • Renaming a releasable no longer trips the pre-push hook. The boundary alias tag pushed by rlsbl monorepo rename-releasable now uses --no-verify (like every other tool-driven push) and honours the project's configured push_timeout instead of a hardcoded 120 seconds.
  • The launcher publish probe checks the URL users actually download. The generated npm/PyPI launcher publish workflow built its asset-verification URL from the repo basename and a bare v strip, so in a prefixed-tag monorepo (<name>@v1.2.3) it probed a URL nobody fetches — passing while every install 404s. It now bakes in the wrapped producer's assetProject and tagPrefix, the same values the shim downloads with. Re-run rlsbl scaffold to regenerate.
  • A fresh rlsbl scaffold now passes its own checks. The auto-generated .rlsbl/config.json pipeline entry was missing the mandatory target link, so rlsbl check --name config-schema failed on the tool's own output — and, because publish-template resolution matches on that link, a Go library repo was handed the binary/goreleaser publish workflow (the second scaffold pass then reported "unchanged", so the documented scaffold-review-scaffold flow never healed it). Scaffold now emits the link, library repos get the library publish workflow on the first pass, and their next-steps no longer advertise a goreleaser build that never runs.
  • A repo's first commit is no longer invisible to changelog scope matching. git diff-tree prints nothing for a parentless commit without --root, so the initial commit — the one that creates an entire project — looked like it touched no files: rlsbl changelog add refused it as out of scope for its own project or releasable, and the coverage check silently skipped it. It is now matched like every other commit.
  • The docs no longer describe features that were removed. Generated and hand-written pages still advertised the deleted cargo/crates.io target across the README, the CLI and API reference, and the pipelines page (which counted 10 pipeline types instead of 9 and never documented the mandatory pipeline target key). The dev-workflow page's editable-install table, --venv support list, CI-retry description, pre-push severities, and pre-push-check status were all out of date, the last of these claiming a removed command "still works with a deprecation warning" when it hard-errors and blocks the push.
  • The publish gate's failure guidance is followable again. When CI is red on a released commit the gate no longer tells you to "re-run CI to green on this exact commit" (impossible when the failure is in the code there). It explains that rlsbl verifies CI before tagging, that re-dispatching the publish cannot change the answer, and points at the real remedy: fix forward, cut a new release, then rlsbl release deprecate the old one. rlsbl release resume also regenerates CHANGELOG.md before finalization, so a fix-forward commit's changelog entry reaches readers.
  • Generated docs are no longer one release stale. The release now passes the version it is about to publish to selfdoc gen and selfdoc check as --version-override. Previously selfdoc ran before the version bump was written to disk, so every version-bearing generated line (the CLI index's Version line, root-file version directives) was written for the previous version — and that churn tripped the doc-staleness check on the next release, aborting it until the operator committed the release's own regeneration artifacts and baseline-accepted. Requires a selfdoc that supports --version-override.
  • Two command help strings now tell the truth. rlsbl pre-push-check --help said it verified CHANGELOG.md; it is a retired stub that always exits 1 and points at rlsbl scaffold. rlsbl dev install --help claimed a system-wide install across seven targets; it now names which targets support --global, --venv, and --uninstall separately.
  • --check-timeout now governs config-declared external checks; previously they always ran on the check_timeout config key or the shipped default regardless of the flag.
  • A release CI wait that runs out of time is now reported as an unresolved timeout with its own remediation (check the runs, resume when green, raise --ci-timeout) instead of being misreported as a CI failure. The run-discovery grace is also budgeted inside --ci-timeout, so a short budget no longer collapses the completion wait to one second.
  • rlsbl monorepo release run now refuses to gate, tag and release a batch whose branch tip carries commits the batch did not create (a concurrent session's work riding in between the last candidate push and the CI gate). The abort names every foreign commit and rolls nothing back.
  • rlsbl release --help now lists all 10 release subcommands (it omitted reconcile and claimed 9), and rlsbl monorepo --help lists all 18 monorepo subcommands (it claimed 16).
  • An invalid --check-timeout or --hook-timeout value is now reported against the flag instead of as "Invalid check_timeout in .rlsbl/config.json".
  • Sandbox test runner reclaims its own leaked scratch. A killed scripts/test.sh used to leave a full copy of the repo under TMPDIR and a clone of the uv cache behind; enough of them filled a tmpfs /tmp. Every run now sweeps orphaned scratch dirs it owns before allocating new ones, skipping any whose owner process is still alive. scripts/test.sh --sweep-only runs just the sweep.
  • Scrub documentation corrected. The release-workflow page said safegit blocks direct destructive scrubs in rlsbl-managed repositories; that guard no longer exists. It now describes what a direct scrub leaves behind and points at rlsbl release reconcile, and states the real safegit floor (0.25.0).
  • Sandbox test runner no longer leaks a repo copy on every run. scripts/test.sh ended in exec bwrap, which replaced the shell and discarded the EXIT trap that removes the throwaway tree copy and the uv cache clone -- so successful runs leaked too, not just killed ones. bwrap now runs as a child and its exit status is forwarded after cleanup.
  • Project tests no longer break on a stdlib-shadowing module. rlsbl runs a project's pytest as python -m pytest, which puts the project directory first on sys.path; a flat-layout module there with a stdlib name (an html.py, say) then shadowed the real one and broke pytest's own plugin loading. The invocation now passes -P (PYTHONSAFEPATH), which suppresses that injection without changing anything else.
  • Batch releases no longer half-publish. rlsbl monorepo release run pushed one candidate per member, so the CI router — which filters paths per push — skipped every other member's CI job on the commit all the tags landed on, and those members' publish gates then refused to publish. The batch now publishes every member in a single push and gates that one commit, so each member's own CI runs on the commit its tag points at.
  • rlsbl release resume no longer breaks three ways. (1) --dry-run performed the entire release for real — commits, tag, push to the release branch, GitHub Release, publish dispatches — because the dry-run gate existed only on the fresh-release path; it now previews the remaining steps and changes nothing. (2) A resume tagged whatever the branch tip happened to be and labelled it (CI-verified), because re-entering the release erased the recorded CI-verified commit; the tag now always lands on that commit, and a candidate that cannot be established is a hard error rather than a silent fall back to HEAD. (3) A concurrent session's commit could ride into a resumed release and reach the release branch under its version; once CI has passed, the release is sealed and any commit outside its own trail aborts by name, while a deliberate fix-forward before the CI gate is still adopted and re-gated.
  • rlsbl monorepo snapshot --dry-run no longer writes and commits. The global --dry-run flag was accepted and dropped, so the command regenerated snapshot.json and committed it anyway. It now reports whether the artifact would change and writes nothing.
  • Batch-released versions keep their description and context. A member released via rlsbl monorepo release run takes its release description from the batch file, which was never archived per member — so the next changelog regeneration silently stripped the description and context from that version's .md and its CHANGELOG.md section. The metadata is now archived alongside the version like a standalone release's, and survives every regeneration.
  • rlsbl release run --dry-run no longer deletes a leftover release state file. When a previous run finished but crashed before clearing in-progress.json, the auto-clear ran during previews too. A dry run now reports what it would clear and leaves the file in place.
  • rlsbl <command> --dry-run works without an argv rewrite. rlsbl carried a shim that moved --dry-run, --yes, --quiet and --verbose in front of the command name because strictcli only recognized them there. strictcli 0.35.3 recognizes them anywhere in argv, so the shim is gone and the flags are handled by the framework exactly where you type them. rlsbl now requires strictcli >= 0.35.3.
  • Releases of selfdoc-using projects work again. selfdoc's gen, check and deploy became mutating commands under strictcli's effects regime, so the release flow's subprocess calls hit the confirm protocol: they hard-errored on non-interactive stdin and prompted mid-release from a terminal. All three now pass --yes.
  • Releases of selfdoc-using projects work again. strictcli's confirm protocol now keys on a declared consequential rather than on mutating, and the --yes flag no longer exists on a strictcli app. The release flow's selfdoc gen / selfdoc check / selfdoc deploy subprocesses no longer pass it; none of those commands is consequential, so none of them prompts.
  • rlsbl commit and rlsbl changelog add work again. strictcli replaced the inferred "mutating means confirm" rule with a declared consequential, and --yes no longer exists on a strictcli app. rlsbl was passing --yes to safegit commit, so rlsbl commit failed outright and rlsbl changelog add's auto-commit left the entry uncommitted. release scrub now passes --approve-consequential to safegit's three scrub modes.
  • The cloudflare-pages pipeline can deploy again. selfdoc deploy now declares itself consequential -- it makes a Cloudflare Pages deployment live, or force-pushes gh-pages -- so the bare invocation the pipeline used would hard-error on the release runner's non-interactive stdin, aborting the publish step. The pipeline passes --approve-consequential; the approval was already taken by rlsbl release run one level up. selfdoc gen and selfdoc check are not consequential and stay bare.
  • A private monorepo can no longer publish to a public registry. rlsbl monorepo sync inlined every member's publish workflow into the root publish router purely on that file's existence, ignoring the member's publish_mode. A member declaring publish_mode: "none" that still carried a stale publish workflow got its jobs inlined, so the next <name>@v* release tag would have run a real registry publish. Suppressed members are now excluded, their stale publish workflows are deleted, and a generated router left with nothing to publish is removed.
  • rlsbl scaffold --target <t> no longer deletes the other targets' files. On a multi-target repo the flag routed to the single-target scaffold path, so every file belonging to an unlisted target -- ci-<other>.yml, .goreleaser.yml, VERSION, .npmignore -- fell out of the plan and was orphan-deleted along with its stored merge base, and the merged publish.yml was rewritten with only one target's jobs. --target now DECLARES a target (for ones auto-detection cannot find, such as plain): it is added to the project's target set and the scaffold always covers every target.
  • Re-scaffolding a project with CI service containers or a subdirectory target no longer crashes or invents merge conflicts. Scaffold applied its CI-workflow YAML rewrites (service-container injection, subdirectory working-directory injection) after the three-way merge and stored the rewritten text as the merge base while the template side stayed raw -- so base and template differed by the whole rewrite on every run, any local edit in that region conflicted, and scaffold then parsed the conflict-marked output as YAML and died with a bare while scanning a simple key error naming no file. The rewrites now run before the merge, and conflict-marked text is reported with the file name and the conflicting line ranges.
  • A Go project declaring artifact: "library" no longer gets goreleaser files. Scaffold classified Go projects by scanning for package main anywhere in the tree, so a declared library that ships a dev helper under scripts/ got a .goreleaser.yml aimed at that helper plus a version.go written into it -- regenerated on every scaffold -- even though its publish workflow correctly ran no goreleaser job. The pipeline's declared artifact is now authoritative; detection only applies when nothing is declared yet.
  • Dev nodes no longer get a publish workflow. A monorepo project marked dev_node = true (or releasable = false) cannot be released -- rlsbl release run hard-errors on it -- yet scaffold still wrote a publish.yml for it whenever its publish_mode was the ordinary default "ci". Publish scaffolding is now derived from the project's position in the workspace graph, and an existing publish.yml on such a project is swept on the next scaffold.
  • A Go target living in a subdirectory gets the right artifact in its generated pipeline config. Scaffold auto-detected the artifact kind at the project root, where a subdirectory Go target has no go.mod; detection failed and fell back to "binary", so a Go library under go/ was declared a binary. Detection now runs in the target's own directory.
  • A release can no longer tag a version that could never publish. The release CI gate judged the workflow RUN, while the publish gate judged the releasing project's own CHECK RUNS. In a monorepo those disagree whenever the CI router's paths filter finds no change under a project: its job concludes skipped, the router's run still concludes success, so the release tagged and created a GitHub Release -- and the publish gate then correctly refused the same skipped check, leaving a tag and a Release for a version with no path to any registry and no re-run that could fix it. Both gates now decide "did this project's CI pass on this commit?" with one predicate built from the publish gate's own check-name matcher, in which only success passes. A skipped or absent check is a hard error that names the filter mismatch and the remedy, and no version number is burnt.
  • Monorepo publishes are no longer blocked by a publish gate that could not find the release commit. The gate resolves the commit CI ran on from the rlsbl-ci-sha marker in the GitHub Release body, but the gate job had neither contents:read nor a repository context for gh release view, so the marker read failed silently and the gate fell back to the tag commit -- whose path-filtered CI is skipped in a monorepo, blocking every publish and every retry. Separately, the gate's check-name pattern dropped the per-target suffix for workspace projects, so a project whose CI comes from ci-<target>.yml produced a regex that never matched its real check run. All three are fixed, so a monorepo release publishes on the first attempt.