repomatic.github.releases module

GitHub Releases API client.

The single home for reading GitHub Releases: raw cached API access (tags, versions), tag-to-version extraction, tag-to-SHA resolution, and the range-to-release-notes fetch every dependency report shares (fetch_github_release_notes()). The repomatic.deps.pins adapters and the repomatic.deps.report release-notes helper build on top of these reads, and so do the review steps of the prepare-release checklist (release_review_steps()).

The write helpers live here too, so every release writer carries the same arguments and failure contract: edit_release_notes(), the shared gh release edit path behind the dev pre-release sync (repomatic.github.dev_release) and the changelog-to-release-notes sync (repomatic.github.release_sync), create_release() behind the release draft, and upload_release_files() behind every asset upload.

repomatic.github.releases.GITHUB_API_RELEASES_URL = 'https://api.github.com/repos/{owner}/{repo}/releases'

GitHub API URL for fetching all releases for a repository.

repomatic.github.releases.GITHUB_API_TAG_REF_URL = 'https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag}'

GitHub API URL for resolving a tag name to its git object.

repomatic.github.releases.GITHUB_API_TAG_OBJECT_URL = 'https://api.github.com/repos/{owner}/{repo}/git/tags/{sha}'

GitHub API URL for dereferencing an annotated tag object to its commit.

repomatic.github.releases.RELEASES_PAGE_SIZE = 100

Releases per page of a listing, the most the REST API serves.

repomatic.github.releases.RELEASE_TAGS_CACHE = 'github-release-tags'

HTTP-cache namespace of the release map get_release_tags() reads.

repomatic.github.releases.RELEASE_NOT_FOUND_MARKER = 'release not found'

What gh release edit writes to its error output for a tag with no release.

exception repomatic.github.releases.GitHubReleasesUnavailable[source]

Bases: RuntimeError

Raised when the GitHub Releases API call could not complete cleanly.

Signals a transient failure (network error, timeout, JSON parse error, or pagination breaking mid-stream) where the result cannot safely be treated as “no releases.”

Callers that drive destructive operations (rewriting the changelog, deleting tags, etc.) must catch this and refuse to act, rather than silently rewriting state with a corrupted or empty view of release history.

class repomatic.github.releases.GitHubRelease(date, body)[source]

Bases: NamedTuple

Release metadata for a single version from GitHub.

Create new instance of GitHubRelease(date, body)

date: str

Publication date in YYYY-MM-DD format.

body: str

Release description body (markdown).

class repomatic.github.releases.ReleaseAsset(name, size, sha256, download_url)[source]

Bases: NamedTuple

A single downloadable asset attached to a GitHub release.

Create new instance of ReleaseAsset(name, size, sha256, download_url)

name: str

Asset filename.

size: int

Asset size in bytes.

sha256: str

SHA-256 hex digest from the API’s digest field.

Empty for assets uploaded before GitHub started recording digests (mid-2025), where the API returns digest: null.

download_url: str

Public browser download URL.

class repomatic.github.releases.ReleaseWithAssets(tag, date, draft, prerelease, assets, html_url='')[source]

Bases: NamedTuple

Full release metadata including its assets and visibility flags.

Create new instance of ReleaseWithAssets(tag, date, draft, prerelease, assets, html_url)

tag: str

Raw tag name (e.g. v1.2.3).

date: str

Publication date in YYYY-MM-DD format.

draft: bool

True for draft releases, which are only visible to maintainers.

prerelease: bool

True for releases marked as pre-release.

assets: tuple[ReleaseAsset, ...]

Assets attached to the release, in API order.

html_url: str

Browser URL of the release page.

For a draft release this is the only resolvable link: drafts have no public releases/tag/<tag> URL, so GitHub serves them at an unguessable releases/tag/untagged-<hash> path exposed only in this field. The prepare-release PR body links the rolling dev pre-release through it (see dev_release_url_and_previous_version()).

repomatic.github.releases.get_release_tags(repo_url, *, force_refresh=False)[source]

Get every published release, keyed by its raw, unstripped tag name.

Fetches every release page on a cache miss, and caches a non-empty map under RELEASE_TAGS_CACHE. Every tag scheme is kept, so a caller extracts the version with its own pattern: lychee tags lychee-v…, biome tags @biomejs/biome@…. Uses published_at (falling back to created_at) for the date.

A draft is left out. It is visible to a maintainer’s token alone, so a map holding it would answer differently per token, and a draft has no published notes to compare or link to.

Parameters:
  • repo_url (str) – Repository URL (like https://github.com/user/repo).

  • force_refresh (bool) – Ignore any cached map and re-fetch. A cached map predating a release reports it as absent, so callers acting on an absence around release time should re-confirm live.

Return type:

dict[str, GitHubRelease]

Returns:

Dict mapping raw tag names to GitHubRelease tuples. Empty only when the repository has no published release or repo_url does not parse to an owner/repo pair.

Raises:

GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON. An empty return value means “the repo has no releases”; a raised exception means “the answer is unknown.”

repomatic.github.releases.get_github_releases(repo_url, *, force_refresh=False)[source]

Get the published releases tagged vX.Y.Z, keyed by bare version.

The get_release_tags() map, narrowed to the v-prefixed tags with the prefix stripped.

Parameters:
  • repo_url (str) – Repository URL (like https://github.com/user/repo).

  • force_refresh (bool) – Ignore any cached map and re-fetch: see get_release_tags().

Return type:

dict[str, GitHubRelease]

Returns:

Dict mapping version strings to GitHubRelease tuples.

Raises:

GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON.

repomatic.github.releases.get_releases_with_assets(repo_url)[source]

Get every release with its assets, visibility flags, and digests.

Deliberately uncached, unlike get_github_releases(): the main consumer is sync-binaries, which runs minutes after a release is published and must see the assets that were just uploaded. A cached view would regenerate the binaries page from a pre-release snapshot.

Parameters:

repo_url (str) – Repository URL (e.g. https://github.com/user/repo).

Return type:

list[ReleaseWithAssets]

Returns:

One ReleaseWithAssets per release (drafts and pre-releases included, for the caller to filter), in API order (newest first). Empty when the repository has no releases or repo_url does not parse to an owner/repo pair.

Raises:

GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON.

repomatic.github.releases.dev_release_url_and_previous_version(repo_url, version)[source]

Look up the two release references the prepare-release PR body links to.

A single get_releases_with_assets() fetch yields both:

  • Dev pre-release URL: the html_url of the draft pre-release whose version shares version’s release segment (the rolling v{version}.dev0 draft). Drafts are visible only to authenticated maintainers, so an unauthenticated or token-less caller gets None here even when the draft exists.

  • Previous version: the highest final release (draft, pre-release, and .dev tags excluded) already published. At prepare-release time the tag for version does not exist yet, so this is the release the new one supersedes, used for the v{previous}...main comparison link.

Parameters:
  • repo_url (str) – Repository URL (e.g. https://github.com/user/repo).

  • version (str) – The release version being prepared (e.g. 1.2.3), with the .dev suffix already stripped.

Return type:

tuple[str | None, str | None]

Returns:

An (dev_release_url, previous_version) pair. Either element is None when its release cannot be found or the API is unavailable, so the caller degrades each list item independently.

repomatic.github.releases.release_review_steps(repo_url, version)[source]

Build the two optional review steps of the prepare-release checklist.

The How-to release list opens with two review steps that render only when their GitHub data is reachable, so the checklist degrades to the bare merge instructions offline (or when the dev pre-release is disabled):

  • Dev pre-release review: links the rolling v{version}.dev0 draft through its html_url (drafts have no public tag URL). Omitted when no such draft is visible.

  • Full-changes review: links the v{previous}...main comparison. Omitted when no prior release exists to compare against.

Both come from a single dev_release_url_and_previous_version() lookup. Each returned string is a complete ordered-list line ending in a newline (or empty), written with a 1. marker so the surrounding lazily numbered list renumbers correctly however many steps survive.

Parameters:
  • repo_url (str | None) – Repository URL. Without one, both steps are empty.

  • version (str) – The release version being prepared (like 1.2.3).

Return type:

tuple[str, str]

Returns:

A (dev_release_review, changes_review) pair of list-item lines.

repomatic.github.releases.resolve_tag_to_sha(repo_url, tag)[source]

Resolve a release tag to its 40-character commit SHA.

Reads the tag’s git reference. An annotated tag points at an intermediate tag object, dereferenced one hop to the commit it targets; a lightweight tag points straight at the commit.

Parameters:
  • repo_url (str) – Repository URL.

  • tag (str) – The tag name to resolve (e.g. v1.2.3).

Return type:

str | None

Returns:

The commit SHA, or None when the tag cannot be resolved (network error, missing tag, or unexpected payload).

repomatic.github.releases.extract_version(tag, tag_pattern)[source]

Extract a version from a GitHub release tag.

Parameters:
  • tag (str) – The raw tag name.

  • tag_pattern (str | None) – A regex with a version named group, or None to strip a leading v (the common vX.Y.Z scheme).

Return type:

str | None

Returns:

The version string, or None when tag_pattern does not match.

repomatic.github.releases.edit_release_notes(tag, repository, body, *, title='')[source]

Edit a release’s notes (and optionally its title) in place.

The one gh release edit path shared by every release writer, so the dev pre-release sync and the changelog-to-release-notes sync carry the same arguments and failure contract. Assets are never touched.

Parameters:
  • tag (str) – Git tag name of the release (like v1.2.3).

  • repository (str) – GitHub repository in owner/name form.

  • body (str) – The new release body text.

  • title (str) – When non-empty, also replace the release title.

Return type:

bool

Returns:

True when the edit landed, False when no release carries tag.

Raises:

GhCommandError – When the edit failed for any other reason. A transient failure is not an absence: a caller reading it as one would create a second release for the same tag.

repomatic.github.releases.release_exists(tag, repository)[source]

Whether repository has a release for tag, draft or published.

Parameters:
  • tag (str) – Git tag name of the release (like v1.2.3).

  • repository (str) – GitHub repository in owner/name form.

Raises:

GhCommandError – When the lookup fails for another reason than an absent release. A transient failure is not an absence: a caller reading it as one would create a second release for the same tag.

Return type:

bool

repomatic.github.releases.create_release(tag, repository, *, target, notes, files=())[source]

Create the release draft of tag, unless a release already carries it.

The draft is titled after its tag, targets target, and starts with files attached. It stays a draft: assets can still be added to it, and publishing it is the last step of the release, after which an immutable release locks them.

Idempotent: a release that already exists, from an earlier attempt of the same run, is left as it is.

Parameters:
  • tag (str) – Git tag name of the release (like v1.2.3).

  • repository (str) – GitHub repository in owner/name form.

  • target (str) – The commit the tag points at, created with the release when the tag does not exist yet.

  • notes (str) – The release notes.

  • files (Sequence[Path]) – Assets to attach.

Return type:

bool

Returns:

True when the draft was created, False when a release already carried tag.

Raises:

GhCommandError – When the lookup or the creation fails.

repomatic.github.releases.upload_release_files(tag, repository, files)[source]

Attach files to the release of tag, replacing same-named assets.

The one upload path of every release writer: the release draft of the release workflow and the rolling dev pre-release alike. An upload to a published immutable release fails, so the release workflow attaches every asset while its release is still a draft.

Parameters:
  • tag (str) – Git tag name of the release (like v1.2.3).

  • repository (str) – GitHub repository in owner/name form.

  • files (Sequence[Path]) – The files to attach. Empty uploads nothing.

Raises:

GhCommandError – When the upload fails.

Return type:

None

class repomatic.github.releases.ReleaseNotesRequest(name, repo_url, old, new, tag_pattern=None, skip=frozenset({}))[source]

Bases: NamedTuple

One version bump whose GitHub release notes a report shows.

Create new instance of ReleaseNotesRequest(name, repo_url, old, new, tag_pattern, skip)

name: str

The name the report keys the notes by: a tool, a package, an action.

repo_url: str

The GitHub repository publishing the releases.

old: str

The bare version the bump leaves. Empty for a new dependency, whose notes are those of new alone.

new: str

The bare version the bump adopts.

tag_pattern: str | None

A regex with a version group, reading the version out of a tag. None for the vX.Y.Z scheme, which also passes a bare X.Y.Z tag through.

skip: frozenset[str]

Versions left out of the notes, like the releases a registry yanked: a pulled release has no notes worth showing.

repomatic.github.releases.in_bump_range(version, old, new)[source]

Whether the notes of a bump from old to new cover version.

The half-open range (old, new]. A pre-release inside it is left out, the way the sweep that picks new leaves it out. new itself is kept, even as a pre-release. With no old, the range is new alone.

Parameters:
  • version (Version) – The version of a release.

  • old (Version | None) – The version the bump leaves, or None for a new dependency.

  • new (Version) – The version the bump adopts.

Return type:

bool

repomatic.github.releases.fetch_github_release_notes(requests)[source]

Fetch the GitHub release notes of a batch of version bumps.

For each request, lists the releases of the repository once (a cached call, often warm from a prior candidate sweep) and keeps those in_bump_range() covers, oldest first. A repository costs one listing per page of RELEASES_PAGE_SIZE releases, however many versions the bump spans. Non-GitHub datasources (npm, PyPI workflow literals) make no request here and render no notes.

Parameters:

requests (Iterable[ReleaseNotesRequest]) – One request per bump.

Return type:

dict[str, tuple[str, list[tuple[str, str]]]]

Returns:

A dict mapping names to (repo_url, versions) tuples, where versions holds a (tag, body) pair per release. Only names with at least one non-empty release body are included.