Skip to content
rlsbl.commands.release_scrub
On this page

Wraps safegit scrub with in-history JSONL hash remapping, CHANGELOG verification, tag re-pointing, and GitHub Release documents rewritten in place.

#rlsbl.commands.release_scrub

#rlsbl.commands.release_scrub

Release scrub command: wraps safegit scrub with in-history JSONL hash remapping (--remap-shas-in), CHANGELOG verification, tag updates, and GitHub Release updates.

#_save_step

python
def _save_step(path, data, step_name)

Record a completed step in the scrub result file.

#_select_and_validate_mode

python
def _select_and_validate_mode(flags)

Determine the scrub mode from flags and validate the per-mode contract.

safegit's actual CLI contracts (verified against safegit source):

  • scrub match --pattern <re> with --replace/--mangle and

--from/--entire-history.

  • scrub file <path> takes a POSITIONAL path and only supports

--from (required) and --reason. There is no --file, --replace, --mangle, or --entire-history flag; strictcli-go hard-errors on unknown flags.

  • scrub run <recipe.toml> takes a POSITIONAL recipe path with

--from/--entire-history and --reason; per-operation pattern/replace/mangle live inside the recipe file.

--reason is not checked here: it is a required flag, and a supplied empty value is refused at the CLI boundary (rlsbl._refuse_empty_flags), which is the one place in rlsbl that decides what an explicitly-empty value means.

Returns the mode string: "match", "file", or "recipe".

#_remap_glob_args

python
def _remap_glob_args(remap_globs)

Repeatable --remap-shas-in flag pairs for the safegit invocation.

#_parse_safegit_envelope

python
def _parse_safegit_envelope(output)

Parse safegit's --json stdout as the framework's machine-mode envelope.

In machine mode stdout carries exactly ONE document (strictcli effects contract 19.1): the envelope, whose payload member is safegit's own data and whose preview member carries the recorded effects of a dry run. The whole stream is therefore parsed with a plain json.loads -- there is no trailing would-do log to tolerate any more, and no partial decode that could silently swallow a second document.

The envelope this flow reads is version 2 (contract 19.2): the member set is interface_version, app, app_version, command, exit_code, payload, dry_run, writes, preview, preview_error, diagnostics. writes is null on a scrub command (it names the write set of an update command, contract 27.5) and this flow does not read it.

Anything that is not an envelope is refused by name: the pre-0.27.0 shape was safegit's own bare JSON object, and reading it as a payload would produce a scrub state file missing every key this flow needs. An envelope declaring any other interface_version -- version 1, the pre-0.28.0 safegit -- is refused the same way, by name and with the floor to install.

#_build_safegit_args

python
def _build_safegit_args(flags, mode, remap_globs)

Build the safegit scrub argument list for the selected mode.

remap_globs (from changelog_remap_globs) is passed as repeatable --remap-shas-in flags in every mode: safegit rewrites full 40-hex commit hashes inside the glob-matched changelog files at EVERY commit of the rewritten history, so all historical versions -- including HEAD -- stay self-consistent.

--approve-consequential is explicit in every mode: safegit declares all three scrub modes consequential, so each prompts before dispatch and --json does not answer that prompt. Running this command IS the consent; the force-push that follows is confirmed separately.

The flag is placed BEFORE the command tokens. Anywhere-in-argv recognition is the current contract, but it is a recent amendment and the Go implementation acquired it later than the Python one; the pre-command position is the one every implementation and every version has always recognized, so it cannot break on a callee whose framework build lags.

#_build_scrub_archive

python
def _build_scrub_archive(scrub_data, mode, reason)

Build the committed audit archive from the working scrub state.

HARD SCHEMA RULE: the archive is committed to the repo, so it must never re-introduce what was scrubbed. Fields are WHITELISTED explicitly -- commit SHAs, tag refnames, reason, mode, and the step list only. No patterns, no replacement strings, no file paths, no matched content, and nothing that arrives unexpectedly in safegit's JSON or rlsbl's state.

#_get_archive_path

python
def _get_archive_path(scrub_result_path, new_head)

Archive location: a scrubs/ dir sibling to the releases/ state dir (so releasable-mode archives live under the releasable directory).

#_print_dry_run_summary

python
def _print_dry_run_summary(mode, data)

Print a per-mode dry-run preview from safegit's preview payload.

safegit has one result type per command now; the preview-only members are present exactly when they were measured, so a dry run's payload carries the match counts and none of the rewrite counts.

objects_matched is the count of distinct objects the pattern matched -- NOT objects_scanned, which counts what the scan walked. safegit reported only the latter under a name that read like the former until 0.27.0.

#_load_rewrite_journal

python
def _load_rewrite_journal()

Load the LAST rewrite group from safegit's persisted rewrite journal.

.git/safegit/rewrite-maps.jsonl holds up to three phase records per rewrite (start/refs/complete) sharing one id. The start record carries the full old-to-new commit map and is written BEFORE any refs move, so even a crashed rewrite leaves its mapping recoverable.

Lines can be MB-scale (full commit maps), so the file is streamed one json.loads per line with no line-length assumptions. Multiple rewrite ids are tolerated: the group whose start record appears last wins. A corrupt line is a hard error -- recovering with a partial map would silently mis-repair changelogs.

Returns {"id", "op", "reason", "created_at", "commit_map", "complete", "path"} or None when no journal (or no start record) exists.

#_recover_from_rewrite_journal

python
def _recover_from_rewrite_journal(all_changes_dirs, failures, scrub_data)

Repair dangling changelog hashes from the persisted rewrite journal.

Fallback for a scrub whose in-history remap did not cover the working tree -- e.g. a scrub interrupted after safegit finished but before rlsbl's steps completed, or a scrub someone ran orchestrated but outside rlsbl release scrub (without --remap-shas-in). Applies ONLY when the journal's commit map can actually fix at least one dangling hash; otherwise returns False and leaves every file untouched.

Repaired file paths are recorded in scrub_data["remapped_files"] so the commit step includes them (and a resumed run still commits them).

#heal_release_commits_from_journal

python
def heal_release_commits_from_journal(project_root, workspace_root, workspace_projects, repo_root)

Move stale recorded release commits through safegit's persisted rewrite journal.

The release commit half of detect-and-heal, and the counterpart to :func:_recover_from_rewrite_journal, which does the changelog half. It covers a rewrite that happened OUTSIDE this flow -- a raw safegit scrub, a git filter-repo run, a scrub interrupted before rlsbl's own steps -- where the archives still name commits that no longer exist and every guarded release record read therefore refuses.

Returns the repo paths a commit must carry, empty when there is no journal or nothing moved.

This is the entry point that reads the journal, and the scrub's no-match path is its caller. rlsbl release reconcile reaches the same repair through :func:rlsbl.release_commit_remap.repair_release_commits -- the shared core both go through -- but drives it from its own MERGED commit map (the journal plus the transition record release-commit-remap events plus the committed scrub archives), so it can still heal in a fresh clone, where the journal under .git is not there to read.

#_no_match_validate_and_repair

python
def _no_match_validate_and_repair(project_root, workspace_root, workspace_projects)

Changelog and release record repair for a scrub that found NOTHING to rewrite.

A no-match scrub is the one moment damage from a PRIOR crashed or direct scrub is still cheaply repairable: the safegit rewrite journal is at hand and the repair path is wired up right here. Exiting "nothing to do" without validating would let dangling hashes go unnoticed until a later rlsbl check, when the journal recovery is unreachable and the operator is pointed at manual amends.

No rewrite happened on this run, so there is nothing to force-push: validate, repair the changelog hashes AND the recorded release commits from the journal when possible, COMMIT the repaired files, and hard-error naming anything that remains dangling.

#_require_cleanup_ok

python
def _require_cleanup_ok(scrub_data, scrub_result_path)

Hard gate on safegit's machine-readable post-rewrite cleanup status.

The hash validation gate silently DEPENDS on old objects being pruned: a dangling changelog hash is only detectable because the pre-rewrite object is gone. When safegit reports cleanup_ok: false the old objects may still resolve, validation would falsely pass, and the flow would push a repository whose next prune breaks the changelog -- so the scrub stops here, BEFORE the commit step, with resume state intact.

Remediation re-check: on a resumed run after the operator completed the prune manually, the recorded flag is stale. The gate re-checks REALITY (does any pre-rewrite object still exist?) and proceeds -- updating the persisted state -- when the prune is confirmed done.

#_read_file_bytes

python
def _read_file_bytes(path)

File content as bytes, or None when the file does not exist.

#_regenerate_and_assert_unchanged

python
def _regenerate_and_assert_unchanged(proj_path, scrub_result_path)

Regenerate the changelog and assert it is byte-identical to disk.

With in-history hash remapping, HEAD's JSONL already carries the new SHAs when safegit returns, so regeneration must be a no-op. A diff means something ELSE is wrong (hand-edited CHANGELOG.md, generation drift, inconsistent JSONL) -- hard error with the diff shown, originals restored, and resume state intact. Nothing is committed when unchanged.

#run_cmd

python
def run_cmd(flags, *, ctx)

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search