rlsbl v0.113.0 /rlsbl.commands.release_scrub
On this page

Release scrub command: wraps safegit scrub with JSONL hash remapping, CHANGELOG regeneration, tag updates, and GitHub Release recreation.

#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 recreation.

#_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.

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_json

python
def _parse_safegit_json(output)

Parse safegit's --json stdout, tolerating the dry-run would-do log.

Under --dry-run the framework appends its would-do log to STDOUT after the command's own JSON, so json.loads on the whole stream fails with "Extra data". Read exactly one JSON document and require that whatever follows is only that log: genuine garbage after the JSON is still a hard error, not something silently swallowed.

#_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 REAL dry-run JSON.

Dry-run schemas differ per mode and have NO rewrites/tags keys:

  • match: ScrubMatchDryRunResult (total_matches, estimated_commits, ...)
  • file: ScrubFileDryRunResult (commit_count, mode, file)

#_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).

#_no_match_validate_and_repair

python
def _no_match_validate_and_repair(project_root, workspace_root, workspace_projects)

Changelog hash validation 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 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)
Search