On this page
The one place a GitHub Release's notes, its rlsbl-ci-sha marker and its pre-release flag are decided, so the flow and the reconciler write one document.
#rlsbl.release_publication
#rlsbl.release_publication
The one place a GitHub Release's body, marker and pre-release flag are decided.
A GitHub Release is written by more than one part of rlsbl: the release flow creates it at step 21, and rlsbl release reconcile materializes or repairs it after a rewrite or a partial release. Before this module each site decided independently what a Release body looks like, which meant the reconcile path recreated Releases with notes only -- no rlsbl-ci-sha marker at all -- and never marked a pre-release version as a GitHub pre-release. A Release recreated that way is a Release the publish workflow cannot judge.
Three decisions live here, and nowhere else:
- The notes are the version's own CHANGELOG.md section, verbatim.
- The released-commit marker is
<!-- rlsbl-ci-sha: <40 hex> -->, and
the sha it carries is THE RELEASE RECORD'S RELEASE COMMIT for that version -- the candidate_sha the archive records. The marker is a projection of the release commit onto the forge, not an independent fact: the publish workflow reads it to learn which commit CI proved green, and the archive is what rlsbl itself reads for the same question. :func:release_commit_from_record is how a caller that does not already hold the release commit obtains it.
- The pre-release flag follows the version: a version carrying a
pre-release segment is a GitHub pre-release.
Everything that talks to gh is split into an argv builder and a thin action taking the caller's own runner, so the release flow (scoped run_gh), the reconciler (the same), and a caller naming another repository with --repo all compose the same document and differ only in how they reach the forge.
#ci_sha_marker
def ci_sha_marker(candidate_sha: str) -> strThe marker line for a released commit.
candidate_sha is the release record's release commit for the version -- the commit the archive records as the one CI verified.
#strip_ci_sha_marker
def strip_ci_sha_marker(body: str) -> strbody with any released-commit marker line removed.
#is_prerelease
def is_prerelease(version: str) -> boolDoes version carry a pre-release segment?
The whole rule: semver puts the pre-release channel after a hyphen, and rlsbl's version grammar has no other use for one.
#ReleasePublication
The full document one version's GitHub Release should carry.
Attributes:
tag: the git tag the Release is attached to.title: the Release title. Defaults to the tag, which is what the
release flow has always written.
notes: the version's CHANGELOG.md section, verbatim and without the
marker. Empty when the changelog has no section for the version; :attr:body then falls back to naming the version.
version: the version being published, which decides
:attr:prerelease.
candidate_sha: the release record's release commit for version.
#release_title
def release_title(self) -> str#prerelease
def prerelease(self) -> bool#marker
def marker(self) -> str#body
def body(self) -> strThe Release body: the notes, a blank line, then the marker.
#reconciled_body
def reconciled_body(self, existing: str) -> str | Noneexisting with this publication's marker on it, or None when correct.
Idempotent by construction: a body already carrying this exact marker answers None (nothing to write), and a body carrying a DIFFERENT marker has it replaced rather than a second one appended.
#publication
def publication(*, tag, version, candidate_sha, notes='', title=None)Build the :class:ReleasePublication for one version.
Raises ValueError when the release commit is missing: a Release written without the marker is one the publish workflow cannot judge, and silently omitting it is the failure this module exists to make impossible.
#release_commit_from_record
def release_commit_from_record(releases_dir: str, version: str) -> str | NoneThe recorded release commit for version, or None when there is none.
Read from the archive directly rather than through :func:rlsbl.release_record.read_entry: this is asked on repair paths, where the tag and the release commit are expected to disagree and the guarded read's DISAGREEMENT error would refuse to answer exactly when the answer is needed to end the disagreement.
Both commitless fates answer None, for different reasons: an unrecoverable archive records a version that SHIPPED from a commit nothing can name, and a never_released one records a version NUMBER no release ever used. A caller that must tell the two apart -- to say "the commit could not be derived" rather than "there was never a release here" -- asks :func:version_never_released as well.
#version_never_released
def version_never_released(releases_dir: str, version: str) -> boolDoes version's archive record that no release ever used the number?
The distinction :func:release_commit_from_record cannot draw, because it answers None for every commitless archive: a version that SHIPPED from a commit nothing can name, and a version NUMBER no release ever used, both have no commit to return. A caller deciding what to say about a version -- "the commit could not be derived" versus "there was never a release here" -- asks this first.
False for an absent archive: a version with no record at all is not a version recorded as never released.
#view_body_args
def view_body_args(tag, *, repo=None)argv reading one Release's body.
#create_args
def create_args(pub: ReleasePublication, notes_path, *, repo=None)argv creating the Release pub describes, notes read from a file.
#edit_notes_args
def edit_notes_args(tag, notes_path, *, repo=None)argv replacing one Release's notes from a file.
#edit_all_args
def edit_all_args(tag, notes_path, *, title=None, prerelease=False, repo=None)argv rewriting one Release's whole document: notes, title, flag.
The pre-release flag is stated in BOTH directions (--prerelease and --prerelease=false), never merely omitted: an edit that left it out would keep a Release wrongly marked pre-release marked that way, and the point of this argv is that what the forge ends up carrying is decided here rather than inherited from whatever was there before.
#delete_args
def delete_args(tag, *, repo=None)argv deleting one Release.
#notes_file
def notes_file(body, *, directory='.')Write body to a temporary notes file and yield its path.
Written-then-renamed like the release flow's own notes file, so gh never observes a partially written body, and removed on the way out even when the block raises.
#create_release
def create_release(pub: ReleasePublication, *, gh, config=None, repo=None, directory='.')Create the Release pub describes. Returns the argv that was run.
#update_release
def update_release(pub: ReleasePublication, *, gh, config=None, repo=None, directory='.')Rewrite an EXISTING Release to exactly the document pub describes.
The edit counterpart of :func:create_release, and the same document: a Release whose tag was moved by a rewrite keeps its name and its attachment, so only the body, the title and the pre-release flag have to be restated. Nothing is deleted, so a failure here leaves the old Release in place.
Returns the argv that was run.
#read_release_body
def read_release_body(tag, *, gh, config=None, repo=None) -> strThe existing Release's body, as gh reports it.
#ensure_marker
def ensure_marker(pub: ReleasePublication, *, gh, config=None, repo=None, directory='.')Put pub's marker onto an already-existing Release.
Returns True when the body was rewritten, False when it already carried exactly this marker. Exceptions from gh propagate: a Release whose marker could not be established is one the publish workflow would judge from $GITHUB_SHA instead, which is a verdict nobody established.