repomatic package

Expose package-wide elements.

Subpackages

Submodules

repomatic.binary module

Binary build targets and verification utilities.

Defines the Nuitka compilation targets for all supported platforms and provides binary architecture verification using exiftool.

repomatic.binary.NUITKA_BUILD_TARGETS = {'linux-arm64': {'arch': 'arm64', 'extension': 'bin', 'os': 'ubuntu-24.04-arm', 'platform_id': 'linux'}, 'linux-x64': {'arch': 'x64', 'extension': 'bin', 'os': 'ubuntu-24.04', 'platform_id': 'linux'}, 'macos-arm64': {'arch': 'arm64', 'extension': 'bin', 'os': 'macos-26', 'platform_id': 'macos'}, 'macos-x64': {'arch': 'x64', 'extension': 'bin', 'os': 'macos-26-intel', 'platform_id': 'macos'}, 'windows-arm64': {'arch': 'arm64', 'extension': 'exe', 'os': 'windows-11-arm', 'platform_id': 'windows'}, 'windows-x64': {'arch': 'x64', 'extension': 'exe', 'os': 'windows-2025', 'platform_id': 'windows'}}

List of GitHub-hosted runners used for Nuitka builds.

The key of the dictionary is the target name, which is used as a short name for user-friendlyness. As such, it is used to name the compiled binary.

Values are dictionaries with the following keys:

repomatic.binary.FLAT_BUILD_TARGETS = [{'arch': 'arm64', 'extension': 'bin', 'os': 'ubuntu-24.04-arm', 'platform_id': 'linux', 'target': 'linux-arm64'}, {'arch': 'x64', 'extension': 'bin', 'os': 'ubuntu-24.04', 'platform_id': 'linux', 'target': 'linux-x64'}, {'arch': 'arm64', 'extension': 'bin', 'os': 'macos-26', 'platform_id': 'macos', 'target': 'macos-arm64'}, {'arch': 'x64', 'extension': 'bin', 'os': 'macos-26-intel', 'platform_id': 'macos', 'target': 'macos-x64'}, {'arch': 'arm64', 'extension': 'exe', 'os': 'windows-11-arm', 'platform_id': 'windows', 'target': 'windows-arm64'}, {'arch': 'x64', 'extension': 'exe', 'os': 'windows-2025', 'platform_id': 'windows', 'target': 'windows-x64'}]

List of build targets in a flat format, suitable for matrix inclusion.

repomatic.binary.BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = ('.github/workflows/release.yaml', 'pyproject.toml', 'tests/', 'uv.lock')

Path prefixes that always affect compiled binaries, regardless of the project.

Project-specific source directories (derived from [project.scripts] in pyproject.toml) are added dynamically by binary_affecting_paths.

repomatic.binary.SKIP_BINARY_BUILD_BRANCHES: Final[frozenset[str]] = frozenset({'format-images', 'format-json', 'format-markdown', 'format-shell', 'sync-gitignore', 'sync-mailmap', 'update-deps-graph'})

Branch names for which binary builds should be skipped.

These branches contain changes that do not affect compiled binaries:

  • .mailmap updates only affect contributor attribution

  • Documentation and image changes don’t affect code

  • .gitignore and JSON config changes don’t affect binaries

This allows workflows to skip expensive Nuitka compilation jobs for PRs that cannot possibly change the binary output.

repomatic.binary.BINARY_ARCH_MAPPINGS: Final[dict[str, tuple[str, str]]] = {'linux-arm64': ('CPUType', 'Arm 64-bits'), 'linux-x64': ('CPUType', 'AMD x86-64'), 'macos-arm64': ('CPUType', 'ARM 64-bit'), 'macos-x64': ('CPUType', 'x86 64-bit'), 'windows-arm64': ('MachineType', 'ARM64'), 'windows-x64': ('MachineType', 'AMD64')}

Mapping of build targets to (exiftool_field, expected_substring) tuples.

ABI signatures reported by file(1) for each compiled binary:

  • linux-arm64: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, stripped

  • linux-x64: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped

  • macos-arm64: Mach-O 64-bit executable arm64

  • macos-x64: Mach-O 64-bit executable x86_64

  • windows-arm64: PE32+ executable (console) Aarch64, for MS Windows

  • windows-x64: PE32+ executable (console) x86-64, for MS Windows

repomatic.binary.get_exiftool_command()[source]

Return the platform-appropriate exiftool command.

On Windows, exiftool is installed as exiftool.exe.

Return type:

str

repomatic.binary.run_exiftool(binary_path)[source]

Run exiftool on a binary and return parsed JSON output.

Parameters:

binary_path (Path) – Path to the binary file.

Return type:

dict[str, str]

Returns:

Dictionary of exiftool metadata.

Raises:
repomatic.binary.verify_binary_arch(target, binary_path)[source]

Verify that a binary matches the expected architecture for a target.

Parameters:
  • target (str) – Build target (e.g., ‘linux-arm64’, ‘macos-x64’).

  • binary_path (Path) – Path to the binary file.

Raises:
Return type:

None

repomatic.cache module

Global cache for downloaded tool executables, HTTP API responses, and generated tool configurations.

Three cache subtrees under the user-level cache directory:

Binary cache (bin/): platform-specific tool executables, keyed by {tool}/{version}/{platform}/{executable}. Each cached binary has a .sha256 sidecar written after a verified archive download. Cache hits verify the binary against this sidecar to detect local tampering.

HTTP response cache (http/): JSON API responses from PyPI and GitHub, keyed by {namespace}/{key}.json. Freshness is controlled by a per-caller TTL (seconds); stale entries remain on disk until auto-purge removes them.

Config cache (config/): generated tool configuration files, keyed by {tool}/{filename}. Overwritten on every invocation from the current [tool.X] section in pyproject.toml or bundled defaults. Passed to tools via explicit --config flags so repomatic never writes to the user’s repository.

Note

The cache module is intentionally a pure storage layer. It does not know about checksums, registries, API semantics, or tool specifications. All trust and freshness decisions belong to the caller.

class repomatic.cache.CacheEntry(tool, version, platform, executable, size, path, mtime)[source]

Bases: object

A single cached binary with its metadata.

tool: str

Tool name (registry key).

version: str

Pinned version string.

platform: str

Platform key (e.g., linux-x64, macos-arm64).

executable: str

Executable filename.

size: int

File size in bytes.

path: Path

Absolute path to the cached binary.

mtime: float

File modification time (seconds since epoch).

class repomatic.cache.HttpCacheEntry(namespace, key, size, path, mtime)[source]

Bases: object

A single cached HTTP response with its metadata.

namespace: str

Cache namespace (e.g., pypi, github-releases).

key: str

Cache key within the namespace (e.g., requests, astral-sh/ruff).

size: int

File size in bytes.

path: Path

Absolute path to the cached response file.

mtime: float

File modification time (seconds since epoch).

class repomatic.cache.ConfigCacheEntry(tool, filename, size, path, mtime)[source]

Bases: object

A single cached tool configuration file with its metadata.

tool: str

Tool name (registry key).

filename: str

Config filename (e.g., yamllint.yaml, biome.json).

size: int

File size in bytes.

path: Path

Absolute path to the cached config file.

mtime: float

File modification time (seconds since epoch).

repomatic.cache.cache_dir()[source]

Resolve the cache root directory.

Precedence (highest to lowest):

  1. REPOMATIC_CACHE_DIR environment variable.

  2. cache.dir in [tool.repomatic].

  3. Platform-specific default.

Return type:

Path

Returns:

Absolute path to the cache root (may not exist yet).

repomatic.cache.cached_binary_path(name, version, platform_key, executable)[source]

Construct the cache path for a binary (does not check existence).

Parameters:
  • name (str) – Tool name.

  • version (str) – Pinned version.

  • platform_key (str) – Platform key (e.g., linux-x64).

  • executable (str) – Executable filename.

Return type:

Path

Returns:

Absolute path where the binary would be cached.

repomatic.cache.get_cached_binary(name, version, platform_key, executable)[source]

Return the cached binary path if it exists and is executable.

Does not verify the checksum. The caller is responsible for integrity checks since it owns the checksum value and the skip_checksum flag.

Parameters:
  • name (str) – Tool name.

  • version (str) – Pinned version.

  • platform_key (str) – Platform key.

  • executable (str) – Executable filename.

Return type:

Path | None

Returns:

Path to the cached binary, or None if not cached.

repomatic.cache.store_binary(name, version, platform_key, source)[source]

Copy an extracted binary into the cache atomically.

Writes to a temporary file in the target directory, then renames to the final name. This is atomic on POSIX (same-filesystem rename) and safe on Windows (Path.replace overwrites atomically).

Triggers auto_purge() after a successful store.

Parameters:
  • name (str) – Tool name.

  • version (str) – Pinned version.

  • platform_key (str) – Platform key.

  • source (Path) – Path to the extracted binary to cache.

Return type:

Path

Returns:

Path to the cached binary.

repomatic.cache.cache_info()[source]

List all cached binaries.

Return type:

list[CacheEntry]

Returns:

List of CacheEntry instances, sorted by tool name then version.

repomatic.cache.clear_cache(tool=None, max_age_days=None)[source]

Remove cached binaries.

Parameters:
  • tool (str | None) – If set, only remove entries for this tool. Otherwise remove all cached binaries.

  • max_age_days (int | None) – If set, only remove entries with mtime older than this many days. Otherwise remove all matching entries.

Return type:

tuple[int, int]

Returns:

Tuple of (files_deleted, bytes_freed).

repomatic.cache.get_cached_response(namespace, key, max_age_seconds)[source]

Return a cached HTTP response if it exists and is fresh.

Parameters:
  • namespace (str) – Cache namespace (e.g., pypi, github-releases).

  • key (str) – Cache key, may contain / for nested paths.

  • max_age_seconds (int) – Maximum age in seconds. Entries with mtime older than this are considered stale and ignored. <= 0 disables the cache (always returns None).

Return type:

bytes | None

Returns:

Raw cached response bytes, or None if not cached or stale.

repomatic.cache.store_response(namespace, key, data)[source]

Store an HTTP response in the cache atomically.

Uses the same write-to-temp-then-rename pattern as store_binary(). Triggers auto_purge() after a successful store.

Parameters:
  • namespace (str) – Cache namespace.

  • key (str) – Cache key, may contain / for nested paths.

  • data (bytes) – Raw response bytes to cache.

Return type:

Path | None

Returns:

Path to the cached response file, or None if the write failed (permissions, read-only filesystem, sandbox restrictions).

repomatic.cache.http_cache_info()[source]

List all cached HTTP responses.

Return type:

list[HttpCacheEntry]

Returns:

List of HttpCacheEntry instances, sorted by namespace then key.

repomatic.cache.clear_http_cache(namespace=None, max_age_days=None)[source]

Remove cached HTTP responses.

Parameters:
  • namespace (str | None) – If set, only remove entries in this namespace. Otherwise remove all cached responses.

  • max_age_days (int | None) – If set, only remove entries with mtime older than this many days. Otherwise remove all matching entries.

Return type:

tuple[int, int]

Returns:

Tuple of (files_deleted, bytes_freed).

repomatic.cache.store_config(tool_name, filename, content)[source]

Store a generated tool config in the cache atomically.

Uses the same write-to-temp-then-rename pattern as store_response(). Does not trigger auto_purge(): config files are tiny and overwritten on every invocation, so age-based pruning is unnecessary.

Parameters:
  • tool_name (str) – Tool name (registry key).

  • filename (str) – Config filename (e.g., yamllint.yaml).

  • content (str) – Config file content as text.

Return type:

Path | None

Returns:

Path to the cached config file, or None if the write failed (permissions, read-only filesystem, sandbox restrictions).

repomatic.cache.config_cache_info()[source]

List all cached tool configurations.

Return type:

list[ConfigCacheEntry]

Returns:

List of ConfigCacheEntry instances, sorted by tool name.

repomatic.cache.clear_config_cache(tool=None)[source]

Remove cached tool configurations.

Parameters:

tool (str | None) – If set, only remove entries for this tool. Otherwise remove all cached configurations.

Return type:

tuple[int, int]

Returns:

Tuple of (files_deleted, bytes_freed).

repomatic.cache.auto_purge()[source]

Remove cached entries older than the configured TTL.

Called automatically after store_binary() and store_response(). Purges both binary and HTTP cache entries. Resolves the TTL from REPOMATIC_CACHE_MAX_AGE env var, then cache.max-age in [tool.repomatic], then the CacheConfig.max_age field default. Set to 0 to disable.

Return type:

None

repomatic.changelog module

Changelog parsing, updating, and release lifecycle management.

This module is the single source of truth for all changelog management decisions and operations. It handles two phases of the release cycle:

Post-release (unfreeze)Changelog.update():

Decomposes the latest release section via Changelog.decompose_version(), transforms the elements into an unreleased entry (date → unreleased, comparison URL → ...main, body → development warning), renders via the release-notes template, and prepends the result to the changelog.

Release preparation (freeze)Changelog.freeze():

Decomposes the current unreleased section, sets the release date, freezes the comparison URL to ...vX.Y.Z, clears the development warning, renders via the release-notes template, and replaces the section in place.

Both operations follow the same decompose → modify → render → replace pattern, with the release-notes.md template as the single source of truth for section layout. Both are idempotent: re-running them produces the same result. This is critical for CI workflows that may be retried.

Note

This is a custom implementation. After evaluating all major alternatives — towncrier, commitizen, python-semantic-release, generate-changelog, release-please, scriv, and git-changelog (see issue #94) — none were found to cover even half of the requirements.

Why not use an off-the-shelf tool?

Existing tools fall into two camps, neither of which fits:

Commit-driven tools (python-semantic-release, commitizen, generate-changelog, release-please) auto-generate changelogs from Git history. This conflicts with the project’s philosophy of hand-curated changelogs: entries are written for users, consolidated by hand, and summarize only changes worth knowing about. Auto-generated logs from developer commits are too noisy and don’t account for back-and-forth during development.

Fragment-driven tools (towncrier, scriv) avoid merge conflicts by using per-change files, but handle none of the release orchestration: comparison URL management, GFM warning lifecycle, workflow action reference freezing, or the two-commit freeze/unfreeze release cycle. The multiplication of files across the repo adds complexity, and there is no 1:1 mapping between fragments and changelog entries.

Specific gaps across all evaluated tools:

  • No comparison URL management. None generate GitHub v1.0.0...v1.1.0 diff links, or update them from ...main to ...vX.Y.Z at release time.

  • No unreleased section lifecycle. None manage the [!WARNING] GFM alert warning that the version is under active development, inserting it post-release and removing it at release time.

  • No workflow action reference freezing. None handle the freeze/unfreeze cycle for @main@vX.Y.Z references in workflow files.

  • No two-commit release workflow. None support the freeze commit ([changelog] Release vX.Y.Z) plus unfreeze commit ([changelog] Post-release bump) pattern that changelog.yaml uses.

  • No citation file integration. None update citation.cff release dates.

  • No version bump eligibility checks. None prevent double version increments by comparing the current version against the latest Git tag with a commit-message fallback.

The custom implementation in this module is tightly integrated with the release workflow. Adopting any external tool would require keeping most of this code and adding a new dependency — more complexity, not less.

repomatic.changelog.CHANGELOG_HEADER = '# Changelog\n'

Default changelog header for empty changelogs.

repomatic.changelog.SECTION_START = '##'

Markdown heading level for changelog version sections.

repomatic.changelog.DATE_PATTERN = re.compile('\\d{4}\\-\\d{2}\\-\\d{2}')

Pattern matching release dates in YYYY-MM-DD format.

repomatic.changelog.VERSION_COMPARE_PATTERN = re.compile('v(\\d+\\.\\d+\\.\\d+)\\.\\.\\.v(\\d+\\.\\d+\\.\\d+)')

Pattern matching GitHub comparison URLs like v1.0.0...v1.0.1.

repomatic.changelog.RELEASED_VERSION_PATTERN = re.compile('^##\\s*\\[`?(\\d+\\.\\d+\\.\\d+)`?\\s+\\((\\d{4}-\\d{2}-\\d{2})\\)\\]', re.MULTILINE)

Pattern matching released version headings with dates.

Captures version and date from headings like ## [`5.9.1` (2026-02-14)](...). Skips unreleased versions which use (unreleased) instead of a date. Backticks around the version are optional.

repomatic.changelog.HEADING_PARTS_PATTERN = re.compile('^##\\s*\\[`?(?P<version>\\d+\\.\\d+\\.\\d+(?:\\.\\w+)?)`?\\s+\\((?P<date>[^)]+)\\)\\]\\((?P<url>[^)]+)\\)', re.MULTILINE)

Pattern extracting version, date/label, and URL from a heading.

Used by Changelog.decompose_version() to populate the heading fields of VersionElements.

repomatic.changelog.AVAILABLE_VERB = 'is available on'

Verb phrase for versions present on a platform.

repomatic.changelog.FIRST_AVAILABLE_VERB = 'is the *first version* available on'

Verb phrase for the inaugural release on a platform.

repomatic.changelog.GITHUB_LABEL = '🐙 GitHub'

Display label for GitHub releases in admonitions.

repomatic.changelog.GITHUB_RELEASE_URL = '{repo_url}/releases/tag/v{version}'

GitHub release page URL for a specific version.

repomatic.changelog.NOT_AVAILABLE_VERB = 'is **not available** on'

Verb phrase for versions missing from a platform.

repomatic.changelog.YANKED_DEDUP_MARKER = 'yanked from PyPI'

Dedup marker for the yanked admonition to prevent duplicate insertion.

class repomatic.changelog.VersionElements(compare_url='', date='', version='', availability_admonition='', changes='', development_warning='', editorial_admonition='', yanked_admonition='')[source]

Bases: object

Discrete building blocks of a changelog version section.

Each field is a pre-formatted markdown block (or empty string when absent). Templates compose these elements into the final section layout. Empty variables produce empty strings, which render_template’s 3+ newline collapsing handles gracefully.

Heading fields (compare_url, date, version) are populated by Changelog.decompose_version() and used by the release-notes template to render the ## heading line. Body fields are unchanged.

compare_url: str = ''

GitHub comparison URL from the heading (e.g. repo/compare/vA...vB).

date: str = ''

Release date or unreleased label from the heading.

version: str = ''

Version string extracted from the heading (e.g. 1.2.3).

availability_admonition: str = ''

[!NOTE] or [!WARNING] block for platform availability.

changes: str = ''

Hand-written changelog entries (bullet points, prose).

development_warning: str = ''

[!WARNING] block for unreleased versions under active development.

editorial_admonition: str = ''

Hand-written GFM alert blocks not matching auto-generated patterns.

Multiple blocks are joined with double newlines.

yanked_admonition: str = ''

[!CAUTION] block for releases yanked from PyPI.

class repomatic.changelog.Changelog(initial_changelog=None, current_version=None)[source]

Bases: object

Helpers to manipulate changelog files written in Markdown.

update()[source]

Add a new unreleased entry at the top of the changelog.

Decomposes the current version section, transforms it into an unreleased entry (date set to unreleased, comparison URL retargeted to main, body replaced with the development warning), and prepends it to the changelog.

Idempotent: returns the current content unchanged if an unreleased entry already exists.

Return type:

str

freeze(release_date=None, default_branch='main')[source]

Freeze the current unreleased section for release.

Decomposes the current version section, sets the release date, freezes the comparison URL to the release tag, clears the development warning, and re-renders via the release-notes template.

Parameters:
  • release_date (str | None) – Date in YYYY-MM-DD format. Defaults to today (UTC).

  • default_branch (str) – Branch name for comparison URL.

Return type:

bool

Returns:

True if the content was modified.

classmethod freeze_file(path, version, release_date=None, default_branch='main')[source]

Freeze a changelog file in place.

Reads the file, applies all freeze operations via freeze(), and writes the result back.

Parameters:
  • path (Path) – Path to the changelog file.

  • version (str) – Current version string.

  • release_date (str | None) – Date in YYYY-MM-DD format. Defaults to today (UTC).

  • default_branch (str) – Branch name for comparison URL.

Return type:

bool

Returns:

True if the file was modified.

extract_repo_url()[source]

Extract the repository URL from changelog comparison links.

Parses the first ## [...](<repo_url>/compare/...) heading and returns the base repository URL (e.g. https://github.com/user/repo).

Return type:

str

Returns:

The repository URL, or empty string if not found.

extract_all_releases()[source]

Extract all released versions and their dates from the changelog.

Scans for headings matching ## [X.Y.Z (YYYY-MM-DD)](...). Unreleased versions (with (unreleased)) are skipped.

Return type:

list[tuple[str, str]]

Returns:

List of (version, date) tuples ordered as they appear in the changelog (newest first).

extract_all_version_headings()[source]

Extract all version strings from ## headings.

Includes both released and unreleased versions, so the caller can avoid false-positive orphan detection for the current development version.

Return type:

set[str]

Returns:

Set of version strings found in headings.

insert_version_section(version, date, repo_url, all_versions)[source]

Insert a placeholder section for a missing version.

The section is placed at the correct position in descending version order. The comparison URL points from the next-lower version to this one. After insertion, the next-higher version’s comparison URL base is updated to reference this version, keeping the timeline coherent.

Idempotent: returns False if the version heading already exists.

Parameters:
  • version (str) – Version string (e.g. 1.2.3).

  • date (str) – Release date in YYYY-MM-DD format.

  • repo_url (str) – Repository URL for comparison links.

  • all_versions (list[str]) – All known versions sorted descending.

Return type:

bool

Returns:

True if the content was modified.

update_comparison_base(version, new_base)[source]

Replace the base version in a version heading’s comparison URL.

Changes compare/vOLD...vX.Y.Z to compare/vNEW...vX.Y.Z in the heading for the given version.

Parameters:
  • version (str) – The version whose heading to update.

  • new_base (str) – New base version (without v prefix).

Return type:

bool

Returns:

True if the content was modified.

decompose_version(version)[source]

Decompose a version section into discrete elements.

Parses both the heading (version, date, URL) and the body (admonitions, changes).

Classifies each GFM alert block (consecutive > lines) as one of the auto-generated element types. Everything not classified as auto-generated is preserved as changes.

Parameters:

version (str) – Version string (e.g. 1.2.3).

Return type:

VersionElements

Returns:

A VersionElements with each field populated.

replace_section(version, new_section)[source]

Replace the entire section (heading + body) for a version.

Locates the version heading and replaces everything up to the next ## heading (or EOF) with new_section.

Parameters:
  • version (str) – Version string (e.g. 1.2.3).

  • new_section (str) – New section content including heading.

Return type:

bool

Returns:

True if the content was modified.

repomatic.changelog.build_release_admonition(version, *, pypi_url='', github_url='', first_on_all=False)[source]

Build a GFM release admonition with available distribution links.

Parameters:
  • version (str) – Version string (e.g. 1.2.3).

  • pypi_url (str) – PyPI project URL, or empty if not on PyPI.

  • github_url (str) – GitHub release URL, or empty if no release exists.

  • first_on_all (bool) – Whether every listed platform is a first appearance. When True, uses “is the first version available on” wording.

Return type:

str

Returns:

A > [!NOTE] admonition block, or empty string if neither URL is provided.

repomatic.changelog.build_unavailable_admonition(version, *, missing_pypi=False, missing_github=False)[source]

Build a GFM warning admonition for platforms missing a version.

Parameters:
  • version (str) – Version string (e.g. 1.2.3).

  • missing_pypi (bool) – Whether the version is missing from PyPI.

  • missing_github (bool) – Whether the version is missing from GitHub.

Return type:

str

Returns:

A > [!WARNING] admonition block, or empty string if neither platform is missing.

repomatic.changelog.lint_changelog_dates(changelog_path, package=None, *, fix=False, pypi_package_history=())[source]

Verify that changelog release dates match canonical release dates.

Uses PyPI upload dates as the canonical reference when the project is published to PyPI. Falls back to git tag dates for projects not on PyPI.

Versions older than the first PyPI release are expected to be absent and logged at info level. Versions newer than the first PyPI release but missing from PyPI are unexpected and logged as warnings.

Also detects orphaned versions: versions that exist as git tags, GitHub releases, or PyPI packages but have no corresponding changelog entry. Orphans are logged as warnings and cause a non-zero exit code.

When fix is enabled, date mismatches are corrected in-place and admonitions are added to the changelog:

  • A [!NOTE] admonition listing available distribution links (PyPI, GitHub) for each version. Links are conditional: only sources where the version exists are included.

  • A [!WARNING] admonition listing platforms where the version is not available (missing from PyPI, GitHub, or both).

  • A [!CAUTION] admonition for yanked releases.

Caution

The fix-changelog workflow job skips this function during the release cycle (when release_commits_matrix is non-empty). At that point the release pipeline hasn’t published to PyPI or created a GitHub release yet, so this function would incorrectly add “not available” admonitions to the freshly-released version.

  • Placeholder sections for orphaned versions, with comparison URLs linking to adjacent versions.

Parameters:
  • changelog_path (Path) – Path to the changelog file.

  • package (str | None) – PyPI package name. If None, auto-detected from pyproject.toml. If detection fails, falls back to git tags.

  • fix (bool) – If True, fix dates and add admonitions to the file.

  • pypi_package_history (Sequence[str]) – Former PyPI package names for renamed projects. Releases from each former name are merged into the lookup table so versions published under old names are recognized. The current package name wins on version collisions.

Return type:

int

Returns:

0 if all dates match or references are missing, 1 if any date mismatch or orphan is found.

repomatic.checksums module

Update SHA-256 checksums for binary downloads.

Two update modes:

  1. Workflow files — scans for GitHub release download URLs paired with sha256sum --check verification lines. Replaces stale hashes in-place.

  2. Tool registry — iterates TOOL_REGISTRY entries with binary specs, downloads each URL, and replaces stale hashes in tool_runner.py.

Designed to be called by Renovate postUpgradeTasks after version bumps, but also works standalone for manual checksum updates.

repomatic.checksums.update_checksums(file_path)[source]

Update SHA-256 checksums in a workflow file.

Parameters:

file_path (Path) – Path to the workflow YAML file.

Return type:

list[tuple[str, str, str]]

Returns:

List of (url, old_hash, new_hash) for each updated checksum. Empty if all checksums are already correct.

repomatic.checksums.update_registry_checksums(registry_path)[source]

Update SHA-256 checksums for binary tools in the tool runner registry.

Iterates all TOOL_REGISTRY entries with binary specs, downloads each URL, computes the SHA-256, and replaces stale hashes in-place in the Python source file.

Parameters:

registry_path (Path) – Path to tool_runner.py.

Return type:

list[tuple[str, str, str]]

Returns:

List of (url, old_hash, new_hash) for each updated checksum. Empty if all checksums are already correct.

repomatic.cli module

repomatic.cli.is_stdout(filepath)[source]

Check if a file path is set to stdout.

Prevents the creation of a - file in the current directory.

Return type:

bool

repomatic.cli.prep_path(filepath)[source]

Prepare the output file parameter for Click’s echo function.

Always returns a UTF-8 encoded file object, including for stdout. This avoids UnicodeEncodeError on Windows where the default stdout encoding is cp1252.

For non-stdout paths, parent directories are created automatically if they don’t exist. This absorbs the mkdir -p step that workflows previously had to do.

Return type:

IO

repomatic.cli.generate_header(ctx)[source]

Generate metadata to be left as comments to the top of a file generated by this CLI.

Return type:

str

repomatic.cli.remove_header(content)[source]

Return content without blank lines and header metadata from above.

Return type:

str

class repomatic.cli.ComponentSelector[source]

Bases: ParamType

Accepts bare component names or qualified component/file selectors.

Bare names (e.g., skills) select an entire component. Qualified entries (e.g., skills/repomatic-topics) select a single file within a component. The same syntax is used by the exclude config option in [tool.repomatic].

name: str = 'selector'

the descriptive name of this type

get_metavar(param, ctx=None)[source]

Returns the metavar default for this param if it provides one.

convert(value, param, ctx)[source]

Convert the value to the correct type. This is not called if the value is None (the missing value).

This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types.

The param and ctx arguments may be None in certain situations, such as when converting prompt input.

If the value cannot be converted, call fail() with a descriptive message.

Parameters:
  • value – The value to convert.

  • param – The parameter that is using this type to convert its value. May be None.

  • ctx – The current context that arrived at this value. May be None.

shell_complete(ctx, param, incomplete)[source]

Return a list of CompletionItem objects for the incomplete value. Most types do not provide completions, but some do, and this allows custom types to provide custom completions as well.

Parameters:
  • ctx – Invocation context for this command.

  • param – The parameter that is requesting completion.

  • incomplete – Value being completed. May be empty.

Added in version 8.0.

repomatic.cli.GITIGNORE_BASE_CATEGORIES: tuple[str, ...] = ('certificates', 'emacs', 'git', 'gpg', 'linux', 'macos', 'node', 'nohup', 'python', 'rust', 'ssh', 'vim', 'virtualenv', 'visualstudiocode', 'windows')

Base gitignore.io template categories included in every generated .gitignore.

These cover common development environments, operating systems, and tools. Downstream projects can add more via gitignore-extra-categories in [tool.repomatic].

repomatic.cli.GITIGNORE_IO_URL = 'https://www.toptal.com/developers/gitignore/api'

gitignore.io API endpoint for fetching .gitignore templates.

repomatic.cli.TOOL_LIST_HEADERS: tuple[str, ...] = ('Tool', 'Version', 'Config source')

Column headers for the repomatic run --list table.

repomatic.cli.CACHE_LIST_HEADERS: tuple[str, ...] = ('Type', 'Name', 'Detail', 'Size', 'Age')

Column headers for the repomatic cache show table.

repomatic.config module

Configuration schema and loading for [tool.repomatic] in pyproject.toml.

Defines the Config dataclass, its TOML serialization helpers, and the load_repomatic_config function that reads, validates, and returns a typed Config instance.

class repomatic.config.CacheConfig(dir='', github_release_ttl=604800, github_releases_ttl=86400, max_age=30, pypi_ttl=86400)[source]

Bases: object

Nested schema for [tool.repomatic.cache].

dir: str = ''

Override the binary cache directory path.

When empty (the default), the cache uses the platform convention: ~/Library/Caches/repomatic on macOS, $XDG_CACHE_HOME/repomatic or ~/.cache/repomatic on Linux, %LOCALAPPDATA%\repomatic\Cache on Windows. The REPOMATIC_CACHE_DIR environment variable takes precedence over this setting.

github_release_ttl: int = 604800

Freshness TTL for cached single-release bodies (seconds).

GitHub release bodies are immutable once published, so a long TTL (7 days) is safe. Set to 0 to disable caching for single-release lookups.

github_releases_ttl: int = 86400

Freshness TTL for cached all-releases responses (seconds).

New releases can appear at any time, so a shorter TTL (24 hours) balances freshness with API savings.

max_age: int = 30

Auto-purge cached entries older than this many days.

Set to 0 to disable auto-purge. The REPOMATIC_CACHE_MAX_AGE environment variable takes precedence over this setting.

pypi_ttl: int = 86400

Freshness TTL for cached PyPI metadata (seconds).

PyPI metadata changes when new versions are published. A 24-hour TTL avoids redundant API calls while keeping data reasonably current.

class repomatic.config.DependencyGraphConfig(all_extras=True, all_groups=True, level=None, no_extras=<factory>, no_groups=<factory>, output='./docs/assets/dependencies.mmd')[source]

Bases: object

Nested schema for [tool.repomatic.dependency-graph].

all_extras: bool = True

Whether to include all optional extras in the graph.

When True, the update-deps-graph command behaves as if --all-extras was passed.

all_groups: bool = True

Whether to include all dependency groups in the graph.

When True, the update-deps-graph command behaves as if --all-groups was passed. Projects that want to exclude development dependency groups (docs, test, typing) from their published graph can set this to false.

level: int | None = None

Maximum depth of the dependency graph.

None means unlimited. 1 = primary deps only, 2 = primary + their deps, etc. Equivalent to --level.

no_extras: list[str]

Optional extras to exclude from the graph.

Equivalent to passing --no-extra for each entry. Takes precedence over dependency-graph.all-extras.

no_groups: list[str]

Dependency groups to exclude from the graph.

Equivalent to passing --no-group for each entry. Takes precedence over dependency-graph.all-groups.

output: str = './docs/assets/dependencies.mmd'

Path where the dependency graph Mermaid diagram should be written.

The dependency graph visualizes the project’s dependency tree in Mermaid format.

class repomatic.config.DocsConfig(apidoc_exclude=<factory>, apidoc_extra_args=<factory>, update_script='./docs/docs_update.py')[source]

Bases: object

Nested schema for [tool.repomatic.docs].

apidoc_exclude: list[str]

Glob patterns for modules to exclude from sphinx-apidoc.

Passed as positional exclude arguments after the source directory (e.g., ["setup.py", "tests"]).

apidoc_extra_args: list[str]

Extra arguments appended to the sphinx-apidoc invocation.

The base flags --no-toc --module-first are always applied. Use this for project-specific options (e.g., ["--implicit-namespaces"]).

update_script: str = './docs/docs_update.py'

Path to a Python script run after sphinx-apidoc to generate dynamic content.

Resolved relative to the repository root. Must reside under the docs/ directory for security. Set to an empty string to disable.

class repomatic.config.GitignoreConfig(extra_categories=<factory>, extra_content=<factory>, location='./.gitignore', sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.gitignore].

extra_categories: list[str]

Additional gitignore template categories to fetch from gitignore.io.

List of template names (e.g., ["Python", "Node", "Terraform"]) to combine with the generated .gitignore content.

extra_content: str

Additional content to append at the end of the generated .gitignore file.

location: str = './.gitignore'

File path of the .gitignore to update, relative to the root of the repository.

sync: bool = True

Whether .gitignore sync is enabled for this project.

Projects that manage their own .gitignore and do not want the autofix job to overwrite it can set this to false.

class repomatic.config.LabelsConfig(extra_content_rules='', extra_file_rules='', extra_files=<factory>, sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.labels].

extra_content_rules: str = ''

Additional YAML rules appended to the content-based labeller configuration.

Appended to the bundled labeller-content-based.yaml during export.

extra_file_rules: str = ''

Additional YAML rules appended to the file-based labeller configuration.

Appended to the bundled labeller-file-based.yaml during export.

extra_files: list[str]

URLs of additional label definition files (JSON, JSON5, TOML, or YAML).

Each URL is downloaded and applied separately by labelmaker.

sync: bool = True

Whether label sync is enabled for this project.

Projects that manage their own repository labels and do not want the labels workflow to overwrite them can set this to false.

class repomatic.config.TestMatrixConfig(exclude=<factory>, include=<factory>, remove=<factory>, replace=<factory>, variations=<factory>)[source]

Bases: object

Nested schema for [tool.repomatic.test-matrix].

Keys inside replace and variations are GitHub Actions matrix identifiers (e.g., os, python-version) and must not be normalized to snake_case. Click Extra’s click_extra.normalize_keys = False metadata on the parent field prevents this.

exclude: list[dict[str, str]]

Extra exclude rules applied to both full and PR test matrices.

Each entry is a dict of GitHub Actions matrix keys (e.g., {"os": "windows-11-arm"}) that removes matching combinations. Additive to the upstream default excludes.

include: list[dict[str, str]]

Extra include directives applied to both full and PR test matrices.

Each entry is a dict of GitHub Actions matrix keys that adds or augments matrix combinations. Additive to the upstream default includes.

remove: dict[str, list[str]]

Per-axis value removals applied to both full and PR test matrices.

Outer key is the variation/axis ID (e.g., os, python-version). Inner list contains values to drop from that axis. Applied after replacements but before excludes, includes, and variations.

replace: dict[str, dict[str, str]]

Per-axis value replacements applied to both full and PR test matrices.

Outer key is the variation/axis ID (e.g., os, python-version). Inner dict maps old values to new values. Applied before removals, excludes, includes, and variations.

variations: dict[str, list[str]]

Extra matrix dimension values added to the full test matrix only.

Each key is a dimension ID (e.g., os, click-version) and its value is a list of additional entries. For existing dimensions, values are merged with the upstream defaults. For new dimension IDs, a new axis is created. Only affects the full matrix; the PR matrix stays a curated reduced set.

class repomatic.config.TestPlanConfig(file='./tests/cli-test-plan.yaml', inline=None, timeout=None)[source]

Bases: object

Nested schema for [tool.repomatic.test-plan].

file: str = './tests/cli-test-plan.yaml'

Path to the YAML test plan file for binary testing.

The test plan file defines a list of test cases to run against compiled binaries. Each test case specifies command-line arguments and expected output patterns.

inline: str | None = None

Inline YAML test plan for binaries.

Alternative to test_plan_file. Allows specifying the test plan directly in pyproject.toml instead of a separate file.

timeout: int | None = None

Timeout in seconds for each binary test.

If set, each test command will be terminated after this duration. None means no timeout (tests can run indefinitely).

class repomatic.config.WorkflowConfig(source_paths=None, sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.workflow].

source_paths: list[str] | None = None

Source code directory names for workflow trigger paths: filters.

When set, thin-caller and header-only workflows include paths: filters using these directory names (as name/** globs) alongside universal paths like pyproject.toml and uv.lock.

When None (default), source paths are auto-derived from [project.name] in pyproject.toml by replacing hyphens with underscores — the universal Python convention. For example, name = "extra-platforms" automatically uses ["extra_platforms"].

sync: bool = True

Whether workflow sync is enabled for this project.

Projects that manage their own workflow files and do not want the autofix job to sync thin callers or headers can set this to false.

class repomatic.config.Config(awesome_template_sync=True, bumpversion_sync=True, cache=<factory>, changelog_location='./changelog.md', dependency_graph=<factory>, dev_release_sync=True, docs=<factory>, exclude=<factory>, gitignore=<factory>, include=<factory>, labels=<factory>, mailmap_sync=True, notification_unsubscribe=False, nuitka_enabled=True, nuitka_entry_points=<factory>, nuitka_extra_args=<factory>, nuitka_unstable_targets=<factory>, pypi_package_history=<factory>, setup_guide=True, skills_location='./.claude/skills/', test_matrix=<factory>, test_plan=<factory>, uv_lock_sync=True, workflow=<factory>)[source]

Bases: object

Configuration schema for [tool.repomatic] in pyproject.toml.

This dataclass defines the structure and default values for repomatic configuration. Each field has a docstring explaining its purpose.

awesome_template_sync: bool = True

Whether awesome-template sync is enabled for this project.

Repositories whose name starts with awesome- get their boilerplate synced from files bundled in repomatic. Set to false to opt out.

bumpversion_sync: bool = True

Whether bumpversion config sync is enabled for this project.

Projects that manage their own [tool.bumpversion] section and do not want the autofix job to overwrite it can set this to false.

cache: CacheConfig

Binary cache configuration.

changelog_location: str = './changelog.md'

File path of the changelog, relative to the root of the repository.

dependency_graph: DependencyGraphConfig

Dependency graph generation configuration.

dev_release_sync: bool = True

Whether dev pre-release sync is enabled for this project.

Projects that do not want a rolling draft pre-release maintained on GitHub can set this to false.

docs: DocsConfig

Sphinx documentation generation configuration.

exclude: list[str]

Additional components and files to exclude from repomatic operations.

Additive to the default exclusions (labels, skills). Bare names exclude an entire component (e.g., "workflows"). Qualified component/identifier entries exclude a specific file within a component (e.g., "workflows/debug.yaml", "skills/repomatic-audit", "labels/labeller-content-based.yaml").

Affects repomatic init, workflow sync, and workflow create. Explicit CLI positional arguments override this list.

gitignore: GitignoreConfig

.gitignore sync configuration.

include: list[str]

Components and files to force-include, overriding default exclusions.

Use this to opt into components that are excluded by default (labels, skills). Each entry is subtracted from the effective exclude set (defaults + user exclude) and bypasses RepoScope filtering, so scope-restricted files (like awesome-only skills) are included regardless of repository type. Qualified entries (component/file) implicitly select the parent component. Same syntax as exclude.

labels: LabelsConfig

Repository label sync configuration.

mailmap_sync: bool = True

Whether .mailmap sync is enabled for this project.

Projects that manage their own .mailmap and do not want the autofix job to overwrite it can set this to false.

notification_unsubscribe: bool = False

Whether the unsubscribe-threads workflow is enabled.

Notifications are per-user across all repos. Enable on the single repo where you want scheduled cleanup of closed notification threads. Requires a classic PAT with notifications scope stored as REPOMATIC_NOTIFICATIONS_PAT.

nuitka_enabled: bool = True

Whether Nuitka binary compilation is enabled for this project.

Projects with [project.scripts] entries that are not intended to produce standalone binaries (e.g., libraries with convenience CLI wrappers) can set this to false to opt out of Nuitka compilation.

nuitka_entry_points: list[str]

Which [project.scripts] entry points produce Nuitka binaries.

List of CLI IDs (e.g., ["mpm"]) to compile. When empty (the default), deduplicates by callable target: keeps the first entry point for each unique module:callable pair. This avoids building duplicate binaries when a project declares alias entry points (like both mpm and meta-package-manager pointing to the same function).

nuitka_extra_args: list[str]

Extra Nuitka CLI arguments for binary compilation.

Project-specific flags (e.g., --include-data-files, --include-package-data) that are passed to the Nuitka build command.

nuitka_unstable_targets: list[str]

Nuitka build targets allowed to fail without blocking the release.

List of target names (e.g., ["linux-arm64", "windows-x64"]) that are marked as unstable. Jobs for these targets will be allowed to fail without preventing the release workflow from succeeding.

pypi_package_history: list[str]

Former PyPI package names for projects that were renamed.

When a project changes its PyPI name, older versions remain published under the previous name. List former names here so lint-changelog can fetch release metadata from all names and generate correct PyPI URLs.

setup_guide: bool = True

Whether the setup guide issue is enabled for this project.

Projects that do not need REPOMATIC_PAT or manage their own PAT setup can set this to false to suppress the setup guide issue.

skills_location: str = './.claude/skills/'

Directory prefix for Claude Code skill files, relative to the repository root.

Skill files are written as {skills_location}/{skill-id}/SKILL.md. Useful for repositories where .claude/ is not at the root (e.g., dotfiles repos that store configs under a subdirectory).

test_matrix: TestMatrixConfig

Per-project customizations for the GitHub Actions CI test matrix.

Keys inside this section are GitHub Actions matrix identifiers (e.g., os, python-version) and must not be normalized to snake_case.

test_plan: TestPlanConfig

Binary test plan configuration.

uv_lock_sync: bool = True

Whether uv.lock sync is enabled for this project.

Projects that manage their own lock file strategy and do not want the sync-uv-lock job to run uv lock --upgrade can set this to false.

workflow: WorkflowConfig

Workflow sync configuration.

repomatic.config.SUBCOMMAND_CONFIG_FIELDS: Final[frozenset[str]] = frozenset({'awesome_template_sync', 'bumpversion_sync', 'cache', 'changelog_location', 'dependency_graph', 'dev_release_sync', 'docs', 'exclude', 'gitignore', 'include', 'labels', 'mailmap_sync', 'notification_unsubscribe', 'pypi_package_history', 'setup_guide', 'skills_location', 'test_matrix', 'test_plan', 'uv_lock_sync', 'workflow'})

Config fields consumed directly by subcommands, not needed as metadata outputs.

The test-plan and deps-graph subcommands now read these values directly from [tool.repomatic] in pyproject.toml, so they no longer need to be passed through workflow metadata outputs.

repomatic.config.CONFIG_REFERENCE_HEADERS = ('Option', 'Type', 'Default', 'Description')

Column headers for the [tool.repomatic] configuration reference table.

repomatic.config.config_reference()[source]

Build the [tool.repomatic] configuration reference as table rows.

Introspects the Config dataclass fields, their type annotations, defaults, and attribute docstrings. Nested dataclass fields are expanded into individual rows with dotted keys. Returns a list of (option, type, default, description) tuples suitable for click_extra.table.print_table.

Return type:

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

repomatic.config.load_repomatic_config(pyproject_data=None)[source]

Load [tool.repomatic] config merged with Config defaults.

Delegates to click-extra’s schema-aware dataclass instantiation, which handles normalization, flattening, nested dataclasses, and opaque field extraction automatically based on field metadata and type hints.

Parameters:

pyproject_data (dict[str, Any] | None) – Pre-parsed pyproject.toml dict. If None, reads and parses pyproject.toml from the current working directory.

Return type:

Config

repomatic.deps_graph module

Generate Mermaid dependency graphs from uv lockfiles.

Note

Uses uv export --format cyclonedx1.5 which provides structured JSON with dependency relationships, replacing the need for pipdeptree.

Warning

The generated Mermaid syntax targets the version bundled with sphinxcontrib-mermaid, currently 11.12.1. See the hard-coded MERMAID_VERSION constant in sphinxcontrib-mermaid’s source. Avoid using Mermaid features introduced after that version.

repomatic.deps_graph.STYLE_PRIMARY_DEPS_SUBGRAPH: str = 'fill:#1565C020,stroke:#42A5F5'

Mermaid style for the primary dependencies subgraph box.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps_graph.STYLE_EXTRA_SUBGRAPH: str = 'fill:#7B1FA220,stroke:#BA68C8'

Mermaid style for extra dependency subgraph boxes.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps_graph.STYLE_GROUP_SUBGRAPH: str = 'fill:#546E7A20,stroke:#90A4AE'

Mermaid style for group dependency subgraph boxes.

Uses semi-transparent fill (8-digit hex) so the tint adapts to both light and dark page backgrounds.

repomatic.deps_graph.STYLE_PRIMARY_NODE: str = 'stroke-width:3px'

Mermaid style for root and primary dependency nodes (thick border).

repomatic.deps_graph.MERMAID_RESERVED_KEYWORDS: frozenset[str] = frozenset({'C4Component', 'C4Container', 'C4Deployment', 'C4Dynamic', '_blank', '_parent', '_self', '_top', 'call', 'class', 'classDef', 'click', 'end', 'flowchart', 'flowchart-v2', 'graph', 'interpolate', 'linkStyle', 'style', 'subgraph'})

Mermaid keywords that cannot be used as node IDs.

repomatic.deps_graph.normalize_package_name(name)[source]

Normalize package name for use as Mermaid node ID.

Converts to lowercase and replaces non-alphanumeric characters with underscores. Appends _0 suffix to avoid conflicts with Mermaid reserved keywords.

Return type:

str

repomatic.deps_graph.parse_bom_ref(bom_ref)[source]

Parse a CycloneDX bom-ref into package name and version.

The format is typically name-index@version (e.g., click-extra-11@7.4.0).

Parameters:

bom_ref (str) – The bom-ref string from CycloneDX.

Return type:

tuple[str, str]

Returns:

Tuple of (package_name, version).

repomatic.deps_graph.get_available_groups(pyproject_path=None)[source]

Discover available dependency groups from pyproject.toml.

Parameters:

pyproject_path (Path | None) – Path to pyproject.toml. If None, looks in current directory.

Return type:

tuple[str, ...]

Returns:

Tuple of group names.

repomatic.deps_graph.get_available_extras(pyproject_path=None)[source]

Discover available optional extras from pyproject.toml.

Parameters:

pyproject_path (Path | None) – Path to pyproject.toml. If None, looks in current directory.

Return type:

tuple[str, ...]

Returns:

Tuple of extra names.

repomatic.deps_graph.get_cyclonedx_sbom(package=None, groups=None, extras=None, frozen=True)[source]

Run uv export and return the CycloneDX SBOM as a dictionary.

Results are cached to avoid redundant subprocess calls within the same process.

Parameters:
  • package (str | None) – Optional package name to focus the export on.

  • groups (tuple[str, ...] | None) – Optional dependency groups to include (e.g., “test”, “typing”).

  • extras (tuple[str, ...] | None) – Optional extras to include (e.g., “xml”, “json5”).

  • frozen (bool) – If True, use –frozen to skip lock file updates.

Return type:

dict[str, Any]

Returns:

Parsed CycloneDX SBOM dictionary.

Raises:
repomatic.deps_graph.get_package_names_from_sbom(sbom)[source]

Extract all package names from a CycloneDX SBOM.

Parameters:

sbom (dict[str, Any]) – Parsed CycloneDX SBOM dictionary.

Return type:

set[str]

Returns:

Set of package names.

repomatic.deps_graph.build_dependency_graph(sbom, root_package=None)[source]

Build a dependency graph from CycloneDX SBOM data.

Parameters:
  • sbom (dict[str, Any]) – Parsed CycloneDX SBOM dictionary.

  • root_package (str | None) – Optional package name to use as root. If None, uses the metadata component from the SBOM.

Return type:

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

Returns:

Tuple of (root_name, nodes_dict, edges_list) where: - root_name is the root package name - nodes_dict maps bom-ref to (name, version) tuples - edges_list is a list of (from_name, to_name) tuples

repomatic.deps_graph.filter_graph_to_package(root_name, nodes, edges, package)[source]

Filter the graph to only include dependencies of a specific package.

Parameters:
  • root_name (str) – The root package name.

  • nodes (dict[str, tuple[str, str]]) – Dictionary mapping bom-ref to (name, version) tuples.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • package (str) – Package name to filter to.

Return type:

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

Returns:

Filtered (nodes, edges) tuple.

repomatic.deps_graph.trim_graph_to_depth(root_name, nodes, edges, depth)[source]

Trim the graph to only include nodes within a given depth from the root.

Performs a breadth-first traversal from the root, keeping only nodes reachable within depth hops and edges between those nodes.

Parameters:
  • root_name (str) – The root package name.

  • nodes (dict[str, tuple[str, str]]) – Dictionary mapping bom-ref to (name, version) tuples.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • depth (int) – Maximum depth from root. 0 = root only, 1 = root + primary deps, etc.

Return type:

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

Returns:

Filtered (nodes, edges) tuple.

repomatic.deps_graph.render_mermaid(root_name, nodes, edges, group_packages=None, extra_packages=None, lock_specs=None)[source]

Render the dependency graph as a Mermaid flowchart.

Warning

Output must stay compatible with the Mermaid version bundled in sphinxcontrib-mermaid. See module docstring for details.

Parameters:
  • root_name (str) – The root package name (used to highlight it).

  • nodes (dict[str, tuple[str, str]]) – Dictionary mapping bom-ref to (name, version) tuples.

  • edges (list[tuple[str, str]]) – List of (from_name, to_name) edge tuples.

  • group_packages (dict[str, set[str]] | None) – Optional dict mapping group names to sets of package names that are unique to that group. These will be rendered in subgraphs with --group prefix.

  • extra_packages (dict[str, set[str]] | None) – Optional dict mapping extra names to sets of package names that are unique to that extra. These will be rendered in subgraphs with --extra prefix.

  • lock_specs (LockSpecifiers | None) – Optional specifiers extracted from uv.lock. Provides edge labels (by_package) and subgraph node labels (by_subgraph).

Return type:

str

Returns:

Mermaid flowchart string.

repomatic.deps_graph.generate_dependency_graph(package=None, groups=None, extras=None, frozen=True, depth=None, exclude_base=False)[source]

Generate a Mermaid dependency graph.

Parameters:
  • package (str | None) – Optional package name to focus on. If None, shows the entire project dependency tree.

  • groups (tuple[str, ...] | None) – Optional dependency groups to include (e.g., “test”, “typing”).

  • extras (tuple[str, ...] | None) – Optional extras to include (e.g., “xml”, “json5”).

  • frozen (bool) – If True, use –frozen to skip lock file updates.

  • depth (int | None) – Optional maximum depth from root. If None, shows the full tree.

  • exclude_base (bool) – If True, exclude main (base) dependencies from the graph, showing only packages unique to the requested groups/extras. Used by --only-group and --only-extra.

Return type:

str

Returns:

The graph in Mermaid format.

repomatic.git_ops module

Git operations for GitHub Actions workflows.

This module provides utilities for common Git operations in CI/CD contexts, with idempotent behavior to allow safe re-runs of failed workflows.

All operations follow a “belt-and-suspenders” approach: combine workflow timing guarantees (e.g. workflow_run ensures tags exist) with idempotent guards (e.g. skip_existing on tag creation). This ensures correctness in the face of race conditions, API eventual consistency, and partial failures that are common in GitHub Actions.

Warning

Tag push requires REPOMATIC_PAT

Tags pushed with the default GITHUB_TOKEN do not trigger downstream on.push.tags workflows. The custom PAT is required so that tagging a release commit actually fires the publish and release creation jobs.

repomatic.git_ops.SHORT_SHA_LENGTH = 7

Default SHA length hard-coded to 7.

Caution

The default is subject to change and depends on the size of the repository.

repomatic.git_ops.GITHUB_REMOTE_PATTERN = re.compile('github\\.com[:/](?P<slug>[^/]+/[^/]+?)(?:\\.git)?$')

Extracts an owner/repo slug from a GitHub remote URL.

Handles both HTTPS (https://github.com/owner/repo.git) and SSH (git@github.com:owner/repo.git) formats.

repomatic.git_ops.RELEASE_COMMIT_PATTERN = re.compile('^\\[changelog\\] Release v(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)$')

Pre-compiled regex for release commit messages.

Matches the full message and captures the version number. Use fullmatch to validate a commit is a release commit, or match/search with .group("version") to extract the version string.

A rebase merge preserves the original commit messages, so release commits match this pattern. A squash merge replaces them with the PR title (e.g. Release `v1.2.3` (#42)), which does not match. This mismatch is the mechanism by which squash merges are safely skipped: the create-tag job only processes commits matching this pattern, so no tag, PyPI publish, or GitHub release is created from a squash merge. The detect-squash-merge job in release.yaml detects this and opens an issue to notify the maintainer.

repomatic.git_ops.get_repo_slug_from_remote(remote='origin')[source]

Extract the owner/repo slug from a git remote URL.

Parses both HTTPS and SSH GitHub remote formats. Returns None if the remote is not set, not a GitHub URL, or git is unavailable.

Return type:

str | None

repomatic.git_ops.get_latest_tag_version()[source]

Returns the latest release version from Git tags.

Looks for tags matching the pattern vX.Y.Z and returns the highest version. Returns None if no matching tags are found.

Return type:

Version | None

repomatic.git_ops.get_release_version_from_commits(max_count=10)[source]

Extract release version from recent commit messages.

Searches recent commits for messages matching the pattern [changelog] Release vX.Y.Z and returns the version from the most recent match.

This provides a fallback when tags haven’t been pushed yet due to race conditions between workflows. The release commit message contains the version information before the tag is created.

Parameters:

max_count (int) – Maximum number of commits to search.

Return type:

Version | None

Returns:

The version from the most recent release commit, or None if not found.

repomatic.git_ops.get_tag_date(tag)[source]

Get the date of a Git tag in YYYY-MM-DD format.

Uses creatordate which resolves to the tagger date for annotated tags and the commit date for lightweight tags.

Parameters:

tag (str) – The tag name to look up.

Return type:

str | None

Returns:

Date string in YYYY-MM-DD format, or None if the tag does not exist.

repomatic.git_ops.get_all_version_tags()[source]

Get all version tags and their dates.

Runs a single git tag command to list all tags matching the vX.Y.Z pattern and extracts their dates.

Return type:

dict[str, str]

Returns:

Dict mapping version strings (without v prefix) to dates in YYYY-MM-DD format.

repomatic.git_ops.tag_exists(tag)[source]

Check if a Git tag already exists locally.

Parameters:

tag (str) – The tag name to check.

Return type:

bool

Returns:

True if the tag exists, False otherwise.

repomatic.git_ops.create_tag(tag, commit=None)[source]

Create a local Git tag.

Parameters:
  • tag (str) – The tag name to create.

  • commit (str | None) – The commit to tag. Defaults to HEAD.

Raises:

subprocess.CalledProcessError – If tag creation fails.

Return type:

None

repomatic.git_ops.push_tag(tag, remote='origin')[source]

Push a Git tag to a remote repository.

Parameters:
  • tag (str) – The tag name to push.

  • remote (str) – The remote name. Defaults to “origin”.

Raises:

subprocess.CalledProcessError – If push fails.

Return type:

None

repomatic.git_ops.create_and_push_tag(tag, commit=None, push=True, skip_existing=True)[source]

Create and optionally push a Git tag.

This function is idempotent: if the tag already exists and skip_existing is True, it returns False without failing. This allows safe re-runs of workflows that were interrupted after tag creation but before other steps.

Parameters:
  • tag (str) – The tag name to create.

  • commit (str | None) – The commit to tag. Defaults to HEAD.

  • push (bool) – Whether to push the tag to the remote. Defaults to True.

  • skip_existing (bool) – If True, skip silently when tag exists. If False, raise an error. Defaults to True.

Return type:

bool

Returns:

True if the tag was created, False if it already existed.

Raises:

repomatic.images module

Image optimization using external CLI tools.

Replaces the Docker-based calibreapp/image-actions GitHub Action with direct invocations of lightweight CLI tools, removing the Docker dependency and enabling ubuntu-slim runners.

Tools used per format:

  • PNG: oxipng (lossless, multithreaded Rust optimizer).

  • JPEG/JPG: jpegoptim (lossless Huffman optimization + metadata stripping).

Note

Both tools are strictly lossless: oxipng finds optimal PNG encoding parameters without altering pixel data, and jpegoptim (without -m) rewrites Huffman tables only. This means optimization is idempotent — a second run produces no further changes, so the workflow never creates noisy PRs for negligible savings.

Warning

WebP and AVIF are intentionally not optimized. The only available tools (cwebp, avifenc) work by lossy re-encoding: decode → re-compress at a target quality. This is not idempotent — each pass re-compresses the previous output, producing progressively smaller (and worse) files. The earlier calibreapp/image-actions suffered from this: it required multiple workflow runs to stabilize below the savings threshold, generating repeated PRs with diminishing returns and cumulative quality loss. Lossless WebP/AVIF modes exist but typically increase file size when applied to already lossy-encoded images, making them counterproductive. Since WebP and AVIF are modern formats chosen specifically for their compression efficiency, files in these formats are almost always already well-optimized at creation time.

class repomatic.images.OptimizationResult(path, before_bytes, after_bytes)[source]

Bases: object

Result of optimizing a single image file.

path: Path
before_bytes: int
after_bytes: int
property saved_bytes: int

Bytes saved by optimization.

property saved_pct: float

Percentage saved, as a float 0–100.

repomatic.images.format_file_size(size_bytes)[source]

Format a byte count as a human-readable string.

Uses KB/MB/GB with one decimal place, matching the format produced by calibreapp/image-actions.

Return type:

str

repomatic.images.optimize_image(path, min_savings_pct, min_savings_bytes=1024)[source]

Optimize a single image file in-place.

Parameters:
  • path (Path) – Path to the image file.

  • min_savings_pct (float) – Minimum percentage savings to keep the result. If savings are below this threshold, the original file is restored.

  • min_savings_bytes (int) – Minimum absolute byte savings to keep the result. Prevents noisy diffs for tiny files where even a high percentage represents negligible absolute savings.

Return type:

OptimizationResult | None

Returns:

An OptimizationResult if the file was optimized, or None if the format is unsupported, the required tool is missing, or savings were below the threshold.

repomatic.images.optimize_images(image_files, min_savings_pct=5, min_savings_bytes=1024)[source]

Optimize a list of image files.

Parameters:
  • image_files (Sequence[Path]) – Paths to image files.

  • min_savings_pct (float) – Minimum percentage savings to keep an optimization.

  • min_savings_bytes (int) – Minimum absolute byte savings to keep an optimization.

Return type:

list[OptimizationResult]

Returns:

List of results for files that were successfully optimized.

repomatic.images.generate_markdown_summary(results)[source]

Generate a markdown summary table of optimization results.

Produces a table similar to calibreapp/image-actions output, showing before/after sizes and percentage improvement for each optimized file.

Return type:

str

repomatic.init_project module

Bundled data files, configuration templates, and repository initialization.

Provides a unified interface for accessing bundled data files from repomatic/data/ and orchestrates repository bootstrapping via repomatic init.

Available components (repomatic init <component>):

  • workflows - Thin-caller workflow files

  • labels - Label definitions (labels.toml + labeller rules)

  • renovate - Renovate dependency update configuration (renovate.json5)

  • changelog - Minimal changelog.md

  • ruff - Merges [tool.ruff] into pyproject.toml

  • pytest - Merges [tool.pytest] into pyproject.toml

  • mypy - Merges [tool.mypy] into pyproject.toml

  • bumpversion - Merges [tool.bumpversion] into pyproject.toml

  • skills - Claude Code skill definitions (.claude/skills/)

  • awesome-template - Boilerplate for awesome-* repositories

Selectors use the same component[/file] syntax as the exclude config option in [tool.repomatic]. Qualified entries like skills/repomatic-topics select a single file within a component.

repomatic.init_project.EXPORTABLE_FILES: dict[str, str | None] = {'autofix.yaml': '.github/workflows/autofix.yaml', 'autolock.yaml': '.github/workflows/autolock.yaml', 'bumpversion.toml': None, 'cancel-runs.yaml': '.github/workflows/cancel-runs.yaml', 'changelog.yaml': '.github/workflows/changelog.yaml', 'codecov.yaml': '.github/codecov.yaml', 'debug.yaml': '.github/workflows/debug.yaml', 'docs.yaml': '.github/workflows/docs.yaml', 'labeller-content-based.yaml': '.github/labeller-content-based.yaml', 'labeller-file-based.yaml': '.github/labeller-file-based.yaml', 'labels.toml': 'labels.toml', 'labels.yaml': '.github/workflows/labels.yaml', 'lint.yaml': '.github/workflows/lint.yaml', 'lychee.toml': None, 'mdformat.toml': None, 'mypy.toml': None, 'pytest.toml': None, 'release.yaml': '.github/workflows/release.yaml', 'renovate.json5': 'renovate.json5', 'renovate.yaml': '.github/workflows/renovate.yaml', 'ruff.toml': None, 'skill-av-false-positive.md': '.claude/skills/av-false-positive/SKILL.md', 'skill-awesome-triage.md': '.claude/skills/awesome-triage/SKILL.md', 'skill-babysit-ci.md': '.claude/skills/babysit-ci/SKILL.md', 'skill-benchmark-update.md': '.claude/skills/benchmark-update/SKILL.md', 'skill-brand-assets.md': '.claude/skills/brand-assets/SKILL.md', 'skill-file-bug-report.md': '.claude/skills/file-bug-report/SKILL.md', 'skill-repomatic-audit.md': '.claude/skills/repomatic-audit/SKILL.md', 'skill-repomatic-changelog.md': '.claude/skills/repomatic-changelog/SKILL.md', 'skill-repomatic-deps.md': '.claude/skills/repomatic-deps/SKILL.md', 'skill-repomatic-init.md': '.claude/skills/repomatic-init/SKILL.md', 'skill-repomatic-lint.md': '.claude/skills/repomatic-lint/SKILL.md', 'skill-repomatic-release.md': '.claude/skills/repomatic-release/SKILL.md', 'skill-repomatic-sync.md': '.claude/skills/repomatic-sync/SKILL.md', 'skill-repomatic-test.md': '.claude/skills/repomatic-test/SKILL.md', 'skill-repomatic-topics.md': '.claude/skills/repomatic-topics/SKILL.md', 'skill-sphinx-docs-sync.md': '.claude/skills/sphinx-docs-sync/SKILL.md', 'skill-translation-sync.md': '.claude/skills/translation-sync/SKILL.md', 'skill-upstream-audit.md': '.claude/skills/upstream-audit/SKILL.md', 'tests.yaml': '.github/workflows/tests.yaml', 'typos.toml': None, 'unsubscribe.yaml': '.github/workflows/unsubscribe.yaml', 'yamllint.yaml': None, 'zizmor.yaml': None}

Registry of all exportable files: maps filename to default output path.

None means stdout (for pyproject.toml templates that need merging).

repomatic.init_project.get_data_content(filename)[source]

Get the content of a bundled data file.

This is the low-level function for reading any file from repomatic/data/.

Parameters:

filename (str) – Name of the file to retrieve (e.g., “labels.toml”).

Return type:

str

Returns:

Content of the file as a string.

Raises:

FileNotFoundError – If the file doesn’t exist.

repomatic.init_project.export_content(filename)[source]

Get the content of any exportable bundled file.

Parameters:

filename (str) – The filename (e.g., “ruff.toml”, “labels.toml”, “release.yaml”).

Return type:

str

Returns:

Content of the file as a string.

Raises:
repomatic.init_project.init_config(config_type, pyproject_path=None)[source]

Initialize a configuration by merging it into pyproject.toml.

Reads the pyproject.toml file, checks if the tool section already exists, and if not, inserts the bundled template at the appropriate location.

The template is stored in native format (without [tool.X] prefix) and is parsed by tomlkit and added under the [tool] table.

Parameters:
  • config_type (str) – The configuration type (e.g., "ruff", "bumpversion").

  • pyproject_path (Path | None) – Path to pyproject.toml. Defaults to ./pyproject.toml.

Return type:

str | None

Returns:

The modified pyproject.toml content, or None if no changes needed.

Raises:

ValueError – If the config type is not supported.

repomatic.init_project.default_version_pin()[source]

Derive the default version pin from __version__.

Strips any .dev0 suffix and prefixes with v. For example, "5.10.0.dev0" becomes "v5.10.0".

Return type:

str

class repomatic.init_project.InitResult(created=<factory>, updated=<factory>, skipped=<factory>, excluded=<factory>, excluded_existing=<factory>, unmodified_configs=<factory>, warnings=<factory>)[source]

Bases: object

Result of a repository initialization run.

created: list[str]

Relative paths of newly created files.

updated: list[str]

Relative paths of existing files overwritten with new content.

skipped: list[str]

Relative paths of skipped (already existing) files.

excluded: list[str]

Exclude entries that were applied.

excluded_existing: list[str]

Relative paths of excluded files that still exist on disk.

unmodified_configs: list[str]

Relative paths of config files identical to bundled defaults.

warnings: list[str]

Warning messages emitted during initialization.

repomatic.init_project.run_init(output_dir, components=(), version=None, repo='kdeldycke/repomatic', repo_slug=None, config=None)[source]

Bootstrap a repository for use with kdeldycke/repomatic.

Creates thin-caller workflow files, exports configuration files, and generates a minimal changelog.md if missing. Managed files (workflows, configs, skills) are always overwritten. User-owned files (changelog.md, zizmor.yaml) are created once and never overwritten.

For awesome-* repositories, the awesome-template component is auto-included when no explicit component selection is made.

Note

Scope exclusions (RepoScope.NON_AWESOME, AWESOME_ONLY) and user-config exclusions ([tool.repomatic] exclude) only apply during bare repomatic init. When components are explicitly named on the CLI, scope is bypassed: the caller knows what they asked for. This allows workflows to materialize out-of-scope configs at runtime (e.g., repomatic init renovate in an awesome repo).

Parameters:
  • output_dir (Path) – Root directory of the target repository.

  • components (Sequence[str]) – Components to initialize. Empty means all defaults. When non-empty, scope and user-config exclusions are bypassed.

  • version (str | None) – Version pin for upstream workflows (e.g., v5.10.0).

  • repo (str) – Upstream repository containing reusable workflows.

  • repo_slug (str | None) – Repository owner/name slug for awesome-template URL rewriting. Auto-detected via Metadata if not provided.

Return type:

InitResult

Returns:

Summary of created, updated, skipped, and warned items.

repomatic.init_project.AWESOME_TEMPLATE_SLUG = 'kdeldycke/awesome-template'

Source slug embedded in bundled awesome-template files, rewritten at sync time.

repomatic.init_project.init_awesome_template(output_dir, repo_slug, result)[source]

Copy bundled awesome-template files and rewrite URLs.

Copies all files from the repomatic/data/awesome_template/ bundle into output_dir and rewrites kdeldycke/awesome-template URLs in .github/ markdown and YAML files to match repo_slug.

Parameters:
  • output_dir (Path) – Root directory of the target repository.

  • repo_slug (str) – Target owner/name slug for URL rewriting.

  • result (InitResult) – InitResult accumulator for created/updated files.

Return type:

None

repomatic.init_project.find_unmodified_init_files()[source]

Find init-managed config files identical to their bundled defaults.

Checks bundled components without keep_unmodified for files on disk whose content matches the bundled template (via export_content()) after trailing-whitespace normalization (.rstrip() + "\n").

Mirrors the API of tool_runner.find_unmodified_configs(), returning (component_name, relative_path) tuples.

Return type:

list[tuple[str, str]]

Returns:

List of (component_name, relative_path) tuples for each unmodified file found.

repomatic.init_project.find_all_unmodified_configs()[source]

Find all config files identical to their bundled defaults.

Combines tool configs (yamllint, zizmor, etc.) from tool_runner.find_unmodified_configs() and init-managed configs (labels, renovate) from find_unmodified_init_files().

Return type:

list[tuple[str, str]]

Returns:

List of (label, relative_path) tuples for each unmodified file found.

repomatic.lint_repo module

Repository linting for GitHub Actions workflows.

This module provides consistency checks for repository metadata, including package names, website fields, descriptions, and funding configuration.

repomatic.lint_repo.get_repo_metadata(repo)[source]

Fetch repository metadata from GitHub API.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

dict[str, str | None]

Returns:

Dictionary with ‘homepageUrl’ and ‘description’ keys.

repomatic.lint_repo.check_package_name_vs_repo(package_name, repo_name)[source]

Check if package name matches repository name.

Parameters:
  • package_name (str | None) – The Python package name.

  • repo_name (str) – The repository name.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_website_for_sphinx(repo, is_sphinx, homepage_url=None)[source]

Check that Sphinx projects have a website set.

Parameters:
  • repo (str) – Repository in ‘owner/repo’ format.

  • is_sphinx (bool) – Whether the project uses Sphinx documentation.

  • homepage_url (str | None) – The homepage URL from API (to avoid duplicate calls).

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_description_matches(repo, project_description, repo_description=None)[source]

Check that repository description matches project description.

Parameters:
  • repo (str) – Repository in ‘owner/repo’ format.

  • project_description (str | None) – Description from pyproject.toml.

  • repo_description (str | None) – Description from API (to avoid duplicate calls).

Return type:

tuple[str | None, str]

Returns:

Tuple of (error_message or None, info_message).

repomatic.lint_repo.check_funding_file(repo)[source]

Check that repos with GitHub Sponsors have a FUNDING.yml.

Skips forks (they inherit the parent’s sponsor button) and owners without a Sponsors listing. Uses the GraphQL API because the REST API does not expose hasSponsorsListing.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_stale_draft_releases(repo)[source]

Check for draft releases that are not dev pre-releases.

Draft releases whose tag does not end with .dev0 are likely leftovers from abandoned or failed release attempts. The only expected drafts are the rolling dev pre-releases managed by sync-dev-release.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_topics_subset_of_keywords(repo, keywords=None)[source]

Check that GitHub repo topics are a subset of pyproject.toml keywords.

Parameters:
  • repo (str) – Repository in ‘owner/repo’ format.

  • keywords (list[str] | None) – Keywords from pyproject.toml. If None, check is skipped.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_pat_repository_scope(repo)[source]

Check that the PAT is scoped to only the current repository.

Fine-grained PATs should use Only select repositories to follow the principle of least privilege. This check detects tokens configured with All repositories access.

Two strategies are tried in order:

  1. GET /installation/repositories — returns the repos the token can access, including a repository_selection field.

  2. Cross-repo probe — check permissions.push on another repo owned by the same user. If the token can push to a repo it should not have access to, it is over-scoped.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_fork_pr_approval_policy(repo)[source]

Check that fork PR workflows require approval for first-time contributors.

GitHub Actions has a per-repository policy that controls when workflows from fork pull requests must be approved by a maintainer before they run. The three values, from weakest to strongest, are first_time_contributors_new_to_github, first_time_contributors, and all_external_contributors.

The default (first_time_contributors_new_to_github) only catches brand-new GitHub accounts, which is trivial to bypass with a slightly aged account. The minimum acceptable setting is first_time_contributors, which requires approval for any first-time contributor to this repository. This is one of the mitigations recommended in Astral’s open-source security post: see https://astral.sh/blog/open-source-security-at-astral.

Queries GET /repos/{repo}/actions/permissions/fork-pr-contributor-approval and returns False when the policy is weaker than first_time_contributors.

Note

This endpoint requires the Actions: read permission. When the REPOMATIC_PAT lacks it (or the API call fails for any other reason), the check returns None to signal that the result is indeterminate rather than negative.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool | None, str]

Returns:

Tuple of (passed_or_None, message). None means the check could not run (API inaccessible, unparsable, or unknown policy).

repomatic.lint_repo.check_tag_protection_rules(repo)[source]

Check that no tag rulesets could block the create-tag workflow job.

Tag rulesets that restrict creation or require status checks can prevent REPOMATIC_PAT (or GITHUB_TOKEN) from pushing release tags. This check queries the repository rulesets API and warns when any ruleset targets tags.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[str | None, str]

Returns:

Tuple of (warning_message or None, info_message).

repomatic.lint_repo.check_branch_ruleset_on_default(repo)[source]

Check that at least one active branch ruleset exists.

Queries the same GET /repos/{repo}/rulesets endpoint as check_tag_protection_rules() and looks for active rulesets with target == "branch". The presence of any such ruleset is taken as evidence that the default branch is protected (restrict deletions and block force pushes).

Note

This is a heuristic: it does not verify the ruleset targets the default branch specifically, nor that it enables the exact rules recommended by the setup guide. A deeper check would require fetching each ruleset’s conditions via GET /repos/{repo}/rulesets/{id}, adding N+1 API calls.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool, str]

Returns:

Tuple of (passed, message).

repomatic.lint_repo.check_immutable_releases(repo)[source]

Check that immutable releases are enabled for the repository.

Queries GET /repos/{repo}/immutable-releases and inspects the enabled field in the response.

Note

This endpoint requires the “Administration: Read-only” permission on fine-grained PATs. The REPOMATIC_PAT does not include this scope (too broad), so the check returns None when the API call fails, signaling that the result is indeterminate rather than negative.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool | None, str]

Returns:

Tuple of (passed_or_None, message). None means the check could not run (API inaccessible or unparsable).

repomatic.lint_repo.check_pages_deployment_source(repo)[source]

Check that GitHub Pages is deployed via GitHub Actions, not a branch.

The docs.yaml workflow uses actions/upload-pages-artifact and actions/deploy-pages, which require the Pages source to be set to GitHub Actions in the repository settings. Branch-based deployment (legacy) is incompatible.

Queries GET /repos/{repo}/pages and inspects the build_type field in the response.

Note

A 404 means Pages is not configured at all. This is treated as indeterminate (None) rather than a failure, because the repo may not have deployed docs yet.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool | None, str]

Returns:

Tuple of (passed_or_None, message). None means the check could not run (Pages not configured, or API inaccessible).

repomatic.lint_repo.check_stale_gh_pages_branch(repo)[source]

Check for a leftover gh-pages branch after switching to GitHub Actions.

When Pages is deployed via GitHub Actions, the gh-pages branch is no longer needed and should be deleted to avoid confusion.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool | None, str]

Returns:

Tuple of (passed_or_None, message).

repomatic.lint_repo.check_workflow_permissions()[source]

Check that workflows with custom jobs declare permissions: {}.

Thin-caller workflows (all jobs use uses: to call a reusable workflow) inherit permissions from the called workflow and do not need a top-level permissions key. Workflows that define their own steps: should declare permissions: {} to follow the principle of least privilege.

Return type:

list[tuple[str | None, str]]

Returns:

List of (warning_message or None, info_message) tuples.

repomatic.lint_repo.run_repo_lint(package_name=None, repo_name=None, is_sphinx=False, project_description=None, keywords=None, repo=None, has_pat=False, has_virustotal_key=False, nuitka_active=False, sha=None)[source]

Run all repository lint checks.

Emits GitHub Actions annotations for each check result.

Parameters:
  • package_name (str | None) – The Python package name.

  • repo_name (str | None) – The repository name.

  • is_sphinx (bool) – Whether the project uses Sphinx documentation.

  • project_description (str | None) – Description from pyproject.toml.

  • keywords (list[str] | None) – Keywords list from pyproject.toml.

  • repo (str | None) – Repository in ‘owner/repo’ format.

  • has_pat (bool) – Whether GH_TOKEN contains REPOMATIC_PAT.

  • has_virustotal_key (bool) – Whether VIRUSTOTAL_API_KEY is configured.

  • sha (str | None) – Commit SHA for permission checks.

Return type:

int

Returns:

Exit code (0 for success, 1 for errors).

repomatic.mailmap module

repomatic.mailmap.MAILMAP_PATH = PosixPath('.mailmap')

Canonical path to the .mailmap file in the repository root.

class repomatic.mailmap.Record(canonical='', aliases=<factory>, pre_comment='')[source]

Bases: object

A mailmap identity mapping entry.

canonical: str = ''
aliases: set[str]
pre_comment: str = ''
class repomatic.mailmap.Mailmap[source]

Bases: object

Helpers to manipulate .mailmap files.

.mailmap file format is documented on Git website.

Initialize the mailmap with an empty list of records.

records: list[Record]
static split_identities(mapping)[source]

Split a mapping of identities and normalize them.

Return type:

tuple[str, set[str]]

parse(content)[source]

Parse mailmap content and add it to the current list of records.

Each non-empty, non-comment line is considered a mapping entry.

The preceding lines of a mapping entry are kept attached to it as pre-comments, so the layout will be preserved on rendering, during which records are sorted.

Return type:

None

find(identity)[source]

Returns True if the provided identity matched any record.

Return type:

bool

property git_contributors: set[str][source]

Returns the set of all contributors found in the Git commit history.

No normalization happens: all variations of authors and committers strings attached to all commits are considered.

For format output syntax, see: https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emaNem

update_from_git()[source]

Add to internal records all missing contributors found in commit history.

This method will refrain from adding contributors already registered as aliases.

Return type:

None

render()[source]

Render internal records in Mailmap format.

Return type:

str

repomatic.metadata module

Extract metadata from repository and Python projects to be used by GitHub workflows.

This module solves a fundamental limitation of GitHub Actions: a workflow run is triggered by a singular event, which might encapsulate multiple commits. GitHub only exposes github.event.head_commit (the most recent commit), but workflows often need to process all commits in the push event.

This is critical for releases, where two commits are pushed together:

  1. [changelog] Release vX.Y.Z — the release commit to be tagged and published

  2. [changelog] Post-release bump vX.Y.Z vX.Y.Z — bumps version for the next dev cycle

Since github.event.head_commit only sees the post-release bump, this module extracts the full commit range from the push event and identifies release commits that need special handling (tagging, PyPI publishing, GitHub release creation).

The following variables are printed to the environment file:

```text is_bot=false new_commits=346ce664f055fbd042a25ee0b7e96702e95 6f27db47612aaee06fdf08744b09a9f5f6c2 release_commits=6f27db47612aaee06fdf08744b09a9f5f6c2 mailmap_exists=true gitignore_exists=true python_files=”.github/update_mailmap.py” “.github/metadata.py” “setup.py” json_files= yaml_files=”config.yaml” “.github/workflows/lint.yaml” “.github/workflows/test.yaml” workflow_files=”.github/workflows/lint.yaml” “.github/workflows/test.yaml” doc_files=”changelog.md” “readme.md” “docs/license.md” markdown_files=”changelog.md” “readme.md” “docs/license.md” image_files= zsh_files= is_python_project=true package_name=click-extra project_description=📦 Extra colorful clickable helpers for the CLI. mypy_params=–python-version 3.7 current_version=2.0.1 released_version=2.0.0 is_sphinx=true active_autodoc=true release_notes=[🐍 Available on PyPI](https://pypi.org/project/click-extra/2.21.3). new_commits_matrix={

“commit”: [

“346ce664f055fbd042a25ee0b7e96702e95”, “6f27db47612aaee06fdf08744b09a9f5f6c2”

], “include”: [

{

“commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “short_sha”: “346ce66”, “current_version”: “2.0.1”

}, {

“commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “short_sha”: “6f27db4”, “current_version”: “2.0.0”

}

]

} release_commits_matrix={

“commit”: [“6f27db47612aaee06fdf08744b09a9f5f6c2”], “include”: [

{

“commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “short_sha”: “6f27db4”, “current_version”: “2.0.0”

}

]

} build_targets=[

{

“target”: “linux-arm64”, “os”: “ubuntu-24.04-arm”, “platform_id”: “linux”, “arch”: “arm64”, “extension”: “bin”

}, {

“target”: “linux-x64”, “os”: “ubuntu-24.04”, “platform_id”: “linux”, “arch”: “x64”, “extension”: “bin”

}, {

“target”: “macos-arm64”, “os”: “macos-26”, “platform_id”: “macos”, “arch”: “arm64”, “extension”: “bin”

}, {

“target”: “macos-x64”, “os”: “macos-26-intel”, “platform_id”: “macos”, “arch”: “x64”, “extension”: “bin”

}, {

“target”: “windows-arm64”, “os”: “windows-11-arm”, “platform_id”: “windows”, “arch”: “arm64”, “extension”: “exe”

}, {

“target”: “windows-x64”, “os”: “windows-2025”, “platform_id”: “windows”, “arch”: “x64”, “extension”: “exe”

}

] nuitka_matrix={

“os”: [

“ubuntu-24.04-arm”, “ubuntu-24.04”, “macos-26”, “macos-26-intel”, “windows-11-arm”, “windows-2025”

], “entry_point”: [“mpm”], “commit”: [

“346ce664f055fbd042a25ee0b7e96702e95”, “6f27db47612aaee06fdf08744b09a9f5f6c2”

], “include”: [

{

“target”: “linux-arm64”, “os”: “ubuntu-24.04-arm”, “platform_id”: “linux”, “arch”: “arm64”, “extension”: “bin”

}, {

“target”: “linux-x64”, “os”: “ubuntu-24.04”, “platform_id”: “linux”, “arch”: “x64”, “extension”: “bin”

}, {

“target”: “macos-arm64”, “os”: “macos-26”, “platform_id”: “macos”, “arch”: “arm64”, “extension”: “bin”

}, {

“target”: “macos-x64”, “os”: “macos-26-intel”, “platform_id”: “macos”, “arch”: “x64”, “extension”: “bin”

}, {

“target”: “windows-arm64”, “os”: “windows-11-arm”, “platform_id”: “windows”, “arch”: “arm64”, “extension”: “exe”

}, {

“target”: “windows-x64”, “os”: “windows-2025”, “platform_id”: “windows”, “arch”: “x64”, “extension”: “exe”

}, {

“entry_point”: “mpm”, “cli_id”: “mpm”, “module_id”: “meta_package_manager.__main__”, “callable_id”: “main”, “module_path”: “meta_package_manager”

}, {

“commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “short_sha”: “346ce66”, “current_version”: “2.0.0”

}, {

“commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “short_sha”: “6f27db4”, “current_version”: “1.9.1”

}, {

“os”: “ubuntu-24.04-arm”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-linux-arm64.bin”

}, {

“os”: “ubuntu-24.04-arm”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-linux-arm64.bin”

}, {

“os”: “ubuntu-24.04”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-linux-x64.bin”

}, {

“os”: “ubuntu-24.04”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-linux-x64.bin”

}, {

“os”: “macos-26”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-macos-arm64.bin”

}, {

“os”: “macos-26”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-macos-arm64.bin”

}, {

“os”: “macos-26-intel”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-macos-x64.bin”

}, {

“os”: “macos-26-intel”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-macos-x64.bin”

}, {

“os”: “windows-11-arm”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-windows-arm64.bin”

}, {

“os”: “windows-11-arm”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-windows-arm64.bin”

}, {

“os”: “windows-2025”, “entry_point”: “mpm”, “commit”: “346ce664f055fbd042a25ee0b7e96702e95”, “bin_name”: “mpm-windows-x64.exe”

}, {

“os”: “windows-2025”, “entry_point”: “mpm”, “commit”: “6f27db47612aaee06fdf08744b09a9f5f6c2”, “bin_name”: “mpm-windows-x64.exe”

}, {“state”: “stable”}

]

}

Warning

Fields with serialized lists and dictionaries, like new_commits_matrix, build_targets or nuitka_matrix, are pretty-printed in the example above for readability. They are inlined in the actual output and not formatted this way.

class repomatic.metadata.Dialect(*values)[source]

Bases: StrEnum

Output dialect for metadata serialization.

github = 'github'
github_json = 'github-json'
json = 'json'
repomatic.metadata.METADATA_KEYS_HEADERS = ('Key', 'Description')

Column headers for the metadata keys reference table.

repomatic.metadata.metadata_keys_reference()[source]

Build the metadata keys reference as table rows.

Returns a list of (key, description) tuples for all keys produced by Metadata.dump(), including [tool.repomatic] config fields that are exposed as metadata outputs.

Return type:

list[tuple[str, str]]

repomatic.metadata.all_metadata_keys()[source]

Returns the set of all valid metadata key names.

Return type:

frozenset[str]

repomatic.metadata.HEREDOC_FIELDS: Final[frozenset[str]] = frozenset({'release_notes', 'release_notes_with_admonition'})

Metadata fields that should always use heredoc format in GitHub Actions output.

Some fields may contain special characters (brackets, parentheses, emojis, or potential newlines) that can break GitHub Actions parsing when using simple key=value format. These fields will use the heredoc delimiter format regardless of whether they currently contain multiple lines.

repomatic.metadata.is_version_bump_allowed(part)[source]

Check if a version bump of the specified part is allowed.

This prevents double version increments within a development cycle. A bump is blocked if the version has already been bumped (but not released) since the last tagged release.

For example: - Last release: v5.0.1, current: 5.0.2 → minor bump allowed - Last release: v5.0.1, current: 5.1.0 → minor bump NOT allowed (bumped) - Last release: v5.0.1, current: 6.0.0 → major bump NOT allowed (bumped)

Note

When tags are not available (e.g., due to race conditions between workflows), this function falls back to parsing version from recent commit messages.

Parameters:

part (Literal['minor', 'major']) – The version part to check (minor or major).

Return type:

bool

Returns:

True if the bump should proceed, False if it should be skipped.

class repomatic.metadata.JSONMetadata(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)[source]

Bases: JSONEncoder

Custom JSON encoder for metadata serialization.

Constructor for JSONEncoder, with sensible defaults.

If skipkeys is false, then it is a TypeError to attempt encoding of keys that are not str, int, float, bool or None. If skipkeys is True, such items are simply skipped.

If ensure_ascii is true, the output is guaranteed to be str objects with all incoming non-ASCII and non-printable characters escaped. If ensure_ascii is false, the output can contain non-ASCII and non-printable characters.

If check_circular is true, then lists, dicts, and custom encoded objects will be checked for circular references during encoding to prevent an infinite recursion (which would cause an RecursionError). Otherwise, no such check takes place.

If allow_nan is true, then NaN, Infinity, and -Infinity will be encoded as such. This behavior is not JSON specification compliant, but is consistent with most JavaScript based encoders and decoders. Otherwise, it will be a ValueError to encode such floats.

If sort_keys is true, then the output of dictionaries will be sorted by key; this is useful for regression tests to ensure that JSON serializations can be compared on a day-to-day basis.

If indent is a non-negative integer, then JSON array elements and object members will be pretty-printed with that indent level. An indent level of 0 will only insert newlines. None is the most compact representation.

If specified, separators should be an (item_separator, key_separator) tuple. The default is (’, ‘, ‘: ‘) if indent is None and (‘,’, ‘: ‘) otherwise. To get the most compact JSON representation, you should specify (‘,’, ‘:’) to eliminate whitespace.

If specified, default is a function that gets called for objects that can’t otherwise be serialized. It should return a JSON encodable version of the object or raise a TypeError.

default(o)[source]

Implement this method in a subclass such that it returns a serializable object for o, or calls the base implementation (to raise a TypeError).

For example, to support arbitrary iterators, you could implement default like this:

def default(self, o):
    try:
        iterable = iter(o)
    except TypeError:
        pass
    else:
        return list(iterable)
    # Let the base class default method raise the TypeError
    return super().default(o)
Return type:

Any

class repomatic.metadata.Metadata[source]

Bases: object

Metadata class.

Implemented as a singleton: every Metadata() call returns the same instance within a process. This is safe because env vars and project files do not change during a single CLI invocation. Use reset() in test teardown to discard the cached instance between tests.

Initialize internal variables.

classmethod reset()[source]

Discard the singleton so the next call creates a fresh instance.

Intended for test teardown only. Production code should never call this.

Return type:

None

pyproject_path = PosixPath('pyproject.toml')
sphinx_conf_path = PosixPath('docs/conf.py')
property github_event: dict[str, Any][source]

Load the GitHub event payload from GITHUB_EVENT_PATH.

GitHub Actions automatically sets GITHUB_EVENT_PATH to a JSON file containing the complete webhook event payload.

property git: Git[source]

Return a PyDriller Git object.

git_stash_count()[source]

Returns the number of stashes.

Return type:

int

git_deepen(commit_hash, max_attempts=10, deepen_increment=50)[source]

Deepen a shallow clone until the provided commit_hash is found.

Progressively fetches more commits from the current repository until the specified commit is found or max attempts is reached.

Returns True if the commit was found, False otherwise.

Return type:

bool

commit_matrix(commits)[source]

Pre-compute a matrix of commits.

Danger

This method temporarily modify the state of the repository to compute version metadata from the past.

To prevent any loss of uncommitted data, it stashes and unstash the local changes between checkouts.

The list of commits is augmented with long and short SHA values, as well as current version. Most recent commit is first, oldest is last.

Returns a ready-to-use matrix structure:

Return type:

Matrix | None

property event_type: WorkflowEvent | None[source]

Returns the type of event that triggered the workflow run.

Caution

This property is based on a crude heuristics as it only looks at the value of the GITHUB_BASE_REF environment variable. Which is only set when the event that triggers a workflow run is either pull_request or pull_request_target.

Todo

Add detection of all workflow trigger events.

property event_actor: str | None[source]

Returns the GitHub login of the user that triggered the workflow run.

property event_sender_type: str | None[source]

Returns the type of the user that triggered the workflow run.

property is_bot: bool[source]

Returns True if the workflow was triggered by a bot or automated process.

This is useful to only run some jobs on human-triggered events. Or skip jobs triggered by bots to avoid infinite loops.

Also detects Renovate PRs by branch name pattern (renovate/*), which handles cases where Renovate runs as a user account rather than the renovate[bot] app.

property head_branch: str | None[source]

Returns the head branch name for pull request events.

For pull request events, this is the source branch name (e.g., update-mailmap). For push events, returns None since there’s no head branch concept.

The branch name is extracted from the GITHUB_HEAD_REF environment variable, which is only set for pull request events.

property event_name: str | None[source]

Returns the name of the event that triggered the workflow.

Reads GITHUB_EVENT_NAME. This is the raw event name (e.g., "push", "pull_request", "workflow_run"), as opposed to event_type which returns a WorkflowEvent enum based on heuristics.

property job_name: str | None[source]

Returns the ID of the current job in the workflow.

Reads GITHUB_JOB.

property ref_name: str | None[source]

Returns the short ref name of the branch or tag.

Reads GITHUB_REF_NAME.

property repo_name: str | None[source]

Returns the repository name without owner prefix.

Derived from repo_slug by splitting on /.

property is_awesome: bool[source]

Whether this is an awesome-list repository.

Detected by the awesome- prefix on the repository name.

property repo_owner: str | None[source]

Returns the repository owner.

Reads GITHUB_REPOSITORY_OWNER, falling back to the owner component of repo_slug.

property repo_slug: str | None[source]

Returns the owner/name slug for the current repository.

Resolution order: GITHUB_REPOSITORY env var (CI), gh repo view (authenticated local), git remote URL parsing (offline fallback).

property repo_url: str | None[source]

Returns the full URL to the repository.

Derived from server_url and repo_slug.

property run_attempt: str | None[source]

Returns the run attempt number.

Reads GITHUB_RUN_ATTEMPT.

property run_id: str | None[source]

Returns the unique ID of the current workflow run.

Reads GITHUB_RUN_ID.

property run_number: str | None[source]

Returns the run number for the current workflow.

Reads GITHUB_RUN_NUMBER.

property server_url: str[source]

Returns the GitHub server URL.

Reads GITHUB_SERVER_URL, defaulting to https://github.com.

property sha: str | None[source]

Returns the commit SHA that triggered the workflow.

Reads GITHUB_SHA.

property triggering_actor: str | None[source]

Returns the login of the user that initiated the workflow run.

Reads GITHUB_TRIGGERING_ACTOR. This differs from event_actor (GITHUB_ACTOR) when a workflow is re-run by a different user.

property workflow_ref: str | None[source]

Returns the full workflow reference.

Reads GITHUB_WORKFLOW_REF. The format is owner/repo/.github/workflows/name.yaml@refs/heads/branch.

property changed_files: tuple[str, ...] | None[source]

Returns the list of files changed in the current event’s commit range.

Uses git diff --name-only between the start and end of the commit range. Returns None if no commit range is available (e.g., outside CI).

property binary_affecting_paths: tuple[str, ...][source]

Path prefixes that affect compiled binaries for this project.

Combines the static BINARY_AFFECTING_PATHS (common files like pyproject.toml, uv.lock, tests/) with project-specific source directories derived from [project.scripts] in pyproject.toml.

For example, a project with mpm = "meta_package_manager.__main__:main" adds meta_package_manager/ as an affecting path. This makes the check reusable across downstream repositories without hardcoding source directories.

property skip_binary_build: bool[source]

Returns True if binary builds should be skipped for this event.

Binary builds are expensive and time-consuming. This property identifies contexts where the changes cannot possibly affect compiled binaries, allowing workflows to skip Nuitka compilation jobs.

Two mechanisms are checked:

  1. Branch name — PRs from known non-code branches (documentation, .mailmap, .gitignore, etc.) are skipped.

  2. Changed files — Push events where all changed files fall outside binary_affecting_paths are skipped. This avoids ~2h of Nuitka builds for documentation-only commits to main.

property commit_range: tuple[str | None, str] | None[source]

Range of commits bundled within the triggering event.

A workflow run is triggered by a singular event, which might encapsulate one or more commits. This means the workflow will only run once on the last commit, even if multiple new commits were pushed.

This is critical for releases where two commits are pushed together:

  1. [changelog] Release vX.Y.Z — the release commit

  2. [changelog] Post-release bump vX.Y.Z vX.Y.Z — the post-release bump

Without extracting the full commit range, the release commit would be missed since github.event.head_commit only exposes the post-release bump.

This property also enables processing each commit individually when we want to keep a carefully constructed commit history. The typical example is a pull request that is merged upstream but we’d like to produce artifacts (builds, packages, etc.) for each individual commit.

The default GITHUB_SHA environment variable is not enough as it only points to the last commit. We need to inspect the commit history to find all new ones. New commits need to be fetched differently in push and pull_request events.

See also

Pull request events on GitHub are a bit complex, see: The Many SHAs of a GitHub Pull Request.

property current_commit: Commit | None[source]

Returns the current Commit object.

property current_commit_matrix: Matrix | None[source]

Pre-computed matrix with long and short SHA values of the current commit.

property new_commits: tuple[Commit, ...] | None[source]

Returns list of all Commit objects bundled within the triggering event.

This extracts all commits from the push event, not just head_commit. For releases, this typically includes both the release commit and the post-release bump commit, allowing downstream jobs to process each one.

Commits are returned in chronological order (oldest first, most recent last).

property new_commits_matrix: Matrix | None[source]

Pre-computed matrix with long and short SHA values of new commits.

property new_commits_hash: tuple[str, ...] | None[source]

List all hashes of new commits.

property release_commits: tuple[Commit, ...] | None[source]

Returns list of Commit objects to be tagged within the triggering event.

This filters new_commits to find release commits that need special handling: tagging, PyPI publishing, and GitHub release creation.

This is essential because when a release is pushed, github.event.head_commit only exposes the post-release bump commit, not the release commit. By extracting all commits from the event (via new_commits) and filtering for release commits here, we ensure the release workflow can properly identify and process the [changelog] Release vX.Y.Z commit.

We cannot identify a release commit based on the presence of a vX.Y.Z tag alone. That’s because the tag is not present in the prepare-release pull request produced by the changelog.yaml workflow. The tag is created later by the release.yaml workflow, when the pull request is merged to main.

Our best option is to identify a release based on the full commit message, using the template from the changelog.yaml workflow.

property release_commits_matrix: Matrix | None[source]

Pre-computed matrix with long and short SHA values of release commits.

property release_commits_hash: tuple[str, ...] | None[source]

List all hashes of release commits.

property mailmap_exists: bool[source]
property gitignore_exists: bool[source]
property renovate_config_exists: bool[source]
property gitignore_parser: Parser | None[source]

Returns a parser for the .gitignore file, if it exists.

gitignore_match(file_path)[source]
Return type:

bool

glob_files(*patterns)[source]

Return all file path matching the patterns.

Patterns are glob patterns supporting ** for recursive search, and ! for negation.

All directories are traversed, whether they are hidden (i.e. starting with a dot .) or not, including symlinks.

Skips:

  • files which does not exists

  • directories

  • broken symlinks

  • files matching patterns specified by .gitignore file

Returns both hidden and non-hidden files.

All files are normalized to their absolute path, so that duplicates produced by symlinks are ignored.

File path are returned as relative to the current working directory if possible, or as absolute path otherwise.

The resulting list of file paths is sorted.

Return type:

list[Path]

property python_files: list[Path][source]

Returns a list of python files.

property json_files: list[Path][source]

Returns a list of JSON files.

Note

JSON5 files are excluded because Biome doesn’t support them.

property yaml_files: list[Path][source]

Returns a list of YAML files.

property toml_files: list[Path][source]

Returns a list of TOML files.

property pyproject_files: list[Path][source]

Returns a list of pyproject.toml files.

property workflow_files: list[Path][source]

Returns a list of GitHub workflow files.

property doc_files: list[Path][source]

Returns a list of doc files.

property markdown_files: list[Path][source]

Returns a list of Markdown files.

property image_files: list[Path][source]

Returns a list of image files.

Covers the formats handled by repomatic format-images: JPEG, PNG, WebP, and AVIF. See repomatic.images for the optimization tools.

property shfmt_files: list[Path][source]

Returns a list of shell files that shfmt can reliably format.

shfmt supports the following dialects (-ln flag):

  • bash: GNU Bourne Again Shell.

  • posix: POSIX Shell (/bin/sh).

  • mksh: MirBSD Korn Shell.

  • bats: Bash Automated Testing System.

Zsh is excluded. shfmt added experimental Zsh support in v3.13.0 but it fails on common constructs: for var (list) short-form loops and for ... { } brace-delimited loops. See mvdan/sh#1203 for upstream tracking.

Files are excluded by extension (.zsh, .zshrc, etc.) and by shebang (any .sh file whose first line references zsh).

property zsh_files: list[Path][source]

Returns a list of Zsh files.

property is_python_project[source]

Returns True if repository is a Python project.

Presence of a pyproject.toml file that respects the standards is enough to consider the project as a Python one.

property pyproject_toml: dict[str, Any][source]

Returns the raw parsed content of pyproject.toml.

Returns an empty dict if the file does not exist.

property pyproject: StandardMetadata | None[source]

Returns metadata stored in the pyproject.toml file.

Returns None if the pyproject.toml does not exists or does not respects the PEP standards.

Warning

Some third-party apps have their configuration saved into pyproject.toml file, but that does not means the project is a Python one. For that, the pyproject.toml needs to respect the PEPs.

property config: Config[source]

Returns the [tool.repomatic] section from pyproject.toml.

Merges user configuration with defaults from Config.

property nuitka_entry_points: list[str][source]

Entry points selected for Nuitka binary compilation.

Reads [tool.repomatic].nuitka.entry-points from pyproject.toml. When empty (the default), deduplicates by callable target: keeps the first entry point for each unique module:callable pair, so alias entry points (like both mpm and meta-package-manager pointing to the same function) don’t produce duplicate binaries. Unrecognized CLI IDs are logged as warnings and discarded.

property unstable_targets: set[str][source]

Nuitka build targets allowed to fail without blocking the release.

Reads [tool.repomatic].nuitka.unstable-targets from pyproject.toml. Defaults to an empty set.

Unrecognized target names are logged as warnings and discarded.

property package_name: str | None[source]

Returns package name as published on PyPI.

property project_description: str | None[source]

Returns project description from pyproject.toml.

property script_entries: list[tuple[str, str, str]][source]

Returns a list of tuples containing the script name, its module and callable.

Results are derived from the script entries of pyproject.toml. So that:

Will yields the following list:

property mypy_params: list[str] | None[source]

Generates mypy parameters.

Mypy needs to be fed with this parameter: --python-version 3.x.

Extracts the minimum Python version from the project’s requires-python specifier. Only takes major.minor into account.

static get_current_version()[source]

Returns the current version as managed by bump-my-version.

Same as calling the CLI:

Return type:

str | None

property current_version: str | None[source]

Returns the current version.

Current version is fetched from the bump-my-version configuration file.

During a release, two commits are bundled into a single push event:

  1. [changelog] Release vX.Y.Z — freezes the version to the release number

  2. [changelog] Post-release bump vX.Y.Z vX.Y.Z — bumps to the next dev version

In this situation, the current version returned is the one from the most recent commit (the post-release bump), which represents the next development version. Use released_version to get the version from the release commit.

property released_version: str | None[source]

Returns the version of the release commit.

During a release push event, this extracts the version from the [changelog] Release vX.Y.Z commit, which is distinct from current_version (the post-release bump version). This is used for tagging, PyPI publishing, and GitHub release creation.

Returns None if no release commit is found in the current event.

property is_sphinx: bool[source]

Returns True if the Sphinx config file is present.

property minor_bump_allowed: bool[source]

Check if a minor version bump is allowed.

This prevents double version increments within a development cycle.

property major_bump_allowed: bool[source]

Check if a major version bump is allowed.

This prevents double version increments within a development cycle.

property active_autodoc: bool[source]

Returns True if Sphinx autodoc is active.

property uses_myst: bool[source]

Returns True if MyST-Parser is active in Sphinx.

property nuitka_matrix: Matrix | None[source]

Pre-compute a matrix for Nuitka compilation workflows.

Combine the variations of: - release commits only (during releases) or all new commits (otherwise) - all entry points - for the 3 main OSes - for a set of architectures

Returns a ready-to-use matrix structure, where each variation is augmented with specific extra parameters by the way of matching parameters in the include directive.

property test_matrix: Matrix[source]

Full test matrix for non-PR events.

Combines all runner OS images and Python versions, excluding known incompatible combinations. Marks development Python versions as unstable so CI can use continue-on-error. Per-project config from [tool.repomatic.test-matrix] is applied last.

property test_matrix_pr: Matrix[source]

Reduced test matrix for pull requests.

Skips experimental Python versions and redundant architecture variants to reduce CI load on PRs. Per-project config excludes and includes from [tool.repomatic.test-matrix] are applied, but variations are not (to keep the PR matrix small).

property release_notes: str | None[source]

Generate notes to be attached to the GitHub release.

Renders the github-releases template with changelog content for the version. The template is the single place that defines the release body layout.

property release_notes_with_admonition: str | None[source]

Generate release notes with a pre-computed availability admonition.

Builds the same body as release_notes, but injects a > [!NOTE] admonition linking to PyPI and GitHub even before fix-changelog has a chance to update changelog.md. This lets the create-release workflow step include the admonition at creation time when publish-pypi succeeds.

Returns None when the project is not on PyPI, has no changelog, or has no version to release.

static format_github_value(value)[source]

Transform Python value to GitHub-friendly, JSON-like, console string.

Renders: - str as-is - None into empty string - bool into lower-cased string - Matrix into JSON string - Iterable of mixed strings and Path into a serialized space-separated

string, where Path items are double-quoted

  • other Iterable into a JSON string

Return type:

str

dump(dialect=Dialect.github, keys=())[source]

Returns metadata in the specified format.

Defaults to GitHub dialect. When keys is non-empty, only the requested keys are included in the output.

Return type:

str

repomatic.pypi module

PyPI API client for package metadata lookups.

Provides a shared HTTP client and domain-specific query functions used by repomatic.changelog (release dates, yanked status) and repomatic.renovate (source repository discovery).

repomatic.pypi.PYPI_API_URL = 'https://pypi.org/pypi/{package}/json'

PyPI JSON API URL for fetching all release metadata for a package.

repomatic.pypi.PYPI_PROJECT_URL = 'https://pypi.org/project/{package}/{version}/'

PyPI project page URL for a specific version.

repomatic.pypi.PYPI_LABEL = '🐍 PyPI'

Display label for PyPI releases in admonitions.

class repomatic.pypi.PyPIRelease(date: str, yanked: bool, package: str)[source]

Bases: NamedTuple

Release metadata for a single version from PyPI.

Create new instance of PyPIRelease(date, yanked, package)

date: str

Earliest upload date across all files in YYYY-MM-DD format.

yanked: bool

Whether all files for this version are yanked.

package: str

PyPI package name this release was fetched from.

Needed for projects that were renamed: older versions live under a former package name and their PyPI URLs must point to that name, not the current one.

repomatic.pypi.get_release_dates(package)[source]

Get upload dates and yanked status for all versions from PyPI.

Fetches the package metadata in a single API call. For each version, selects the earliest upload time across all distribution files as the canonical release date. A version is considered yanked only if all of its files are yanked.

Parameters:

package (str) – The PyPI package name.

Return type:

dict[str, PyPIRelease]

Returns:

Dict mapping version strings to PyPIRelease tuples. Empty dict if the package is not found or the request fails.

repomatic.pypi.get_source_url(package)[source]

Discover the GitHub repository URL for a PyPI package.

Queries the PyPI JSON API and scans project_urls for keys that typically point to a source repository on GitHub.

Parameters:

package (str) – The PyPI package name.

Return type:

str | None

Returns:

The GitHub repository URL, or None if not found.

repomatic.pypi.get_changelog_url(package)[source]

Discover the changelog URL for a PyPI package.

Queries the PyPI JSON API and scans project_urls for keys that typically point to a changelog or release notes page.

Parameters:

package (str) – The PyPI package name.

Return type:

str | None

Returns:

The changelog URL, or None if not found.

repomatic.pyproject module

Utilities for reading and interpreting pyproject.toml metadata.

Provides standalone functions for extracting project name and source paths from pyproject.toml. These functions have no dependency on the Metadata singleton and can be used independently.

repomatic.pyproject.derive_source_paths(pyproject_data=None)[source]

Derive source code directory name from [project.name].

Converts the project name to its importable form by replacing hyphens with underscores — the universal Python convention that all build backends (setuptools, hatchling, flit, uv) follow by default. For example, name = "extra-platforms" yields ["extra_platforms"].

Parameters:

pyproject_data (dict[str, Any] | None) – Pre-parsed pyproject.toml dict. If None, reads from the current working directory.

Return type:

list[str]

Returns:

Single-element list with the source directory name, or an empty list if no project name is defined.

repomatic.pyproject.resolve_source_paths(config, pyproject_data=None)[source]

Resolve workflow source paths from config or auto-derivation.

Parameters:
  • config (Config) – Loaded Config instance from [tool.repomatic].

  • pyproject_data (dict[str, Any] | None) – Pre-parsed pyproject.toml dict for derivation.

Return type:

list[str] | None

Returns:

List of source directory names, or None when no source paths can be determined (paths should be stripped entirely).

repomatic.pyproject.get_project_name(pyproject_data=None)[source]

Read the project name from pyproject.toml.

Parameters:

pyproject_data (dict[str, Any] | None) – Pre-parsed dict. If None, reads from CWD.

Return type:

str | None

repomatic.registry module

Declarative registry of all components managed by the init subcommand.

Every resource the init subcommand can create, sync, or merge is declared here as a Component subclass instance in the COMPONENTS tuple. Each component carries all its metadata: what kind it is, whether it is selected by default, which files it manages, and any per-file properties like repo-scope gating or config keys.

All derived constants (ALL_COMPONENTS, COMPONENT_FILES, REUSABLE_WORKFLOWS, SKILL_PHASES, etc.) are computed from this single registry in repomatic.init_project.

class repomatic.registry.InitDefault(*values)[source]

Bases: Enum

How init treats the component when no explicit CLI args are given.

INCLUDE = 1

Included by default (e.g., changelog, renovate, workflows).

EXCLUDE = 2

In default set but excluded unless explicitly included (e.g., labels, skills).

AUTO = 3

Auto-included only for matching repos (e.g., awesome-template).

EXPLICIT = 4

Only included when explicitly requested (e.g., tool configs).

class repomatic.registry.SyncMode(*values)[source]

Bases: Enum

How a ToolConfigComponent behaves when the section already exists.

BOOTSTRAP = 1

Insert once, skip if section already exists (e.g., ruff, pytest).

ONGOING = 2

Replace template content on every sync, preserving local additions (e.g., bumpversion).

class repomatic.registry.RepoScope(*values)[source]

Bases: Enum

Which repository types a file entry applies to.

Scope restrictions are defaults: they apply during bare repomatic init but are bypassed when components are explicitly named on the CLI or covered by [tool.repomatic] include.

ALL = 1

Included in all repository types.

AWESOME_ONLY = 2

Only for awesome-* repositories.

NON_AWESOME = 3

Only for non-awesome repositories.

matches(is_awesome)[source]

Whether this scope applies to the given repository type.

Parameters:

is_awesome (bool) – True for awesome-* repositories.

Return type:

bool

class repomatic.registry.FileEntry(source, target='', file_id='', scope=RepoScope.ALL, config_key='', config_default=False, reusable=True, phase='')[source]

Bases: object

A single file managed within a component.

source: str

Filename in repomatic/data/.

target: str = ''

Relative output path in the target repository. Defaults to source (root-level file).

file_id: str = ''

Identifier for file-level --include/--exclude. Defaults to the filename portion of target.

scope: RepoScope = 1

Which repository types get this file.

config_key: str = ''

[tool.repomatic] key that gates this entry.

config_default: bool = False

Value assumed when config_key is absent from config. False means opt-in (excluded unless enabled), True means opt-out (included unless disabled).

reusable: bool = True

Workflow-specific: supports workflow_call trigger.

phase: str = ''

Skill-specific: lifecycle phase for list-skills display.

is_enabled(config)[source]

Whether this entry is enabled by the given Config object.

Returns True when no config_key is set (unconditionally enabled) or when the corresponding config field is truthy.

Parameters:

config (object) – A Config instance.

Return type:

bool

class repomatic.registry.Component(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False)[source]

Bases: object

Base class for all init components.

name: str

Component name used on the CLI (e.g., "skills").

description: str

Human-readable description for help text.

init_default: InitDefault = 1

How init treats this component when no explicit CLI selection is made.

scope: RepoScope = 1

Which repository types get this component. Checked at the component level during auto-exclusion, complementing the file-level FileEntry.scope.

files: tuple[FileEntry, ...] = ()

File entries this component manages.

config_key: str = ''

[tool.repomatic] key that gates this component.

config_default: bool = True

Value assumed when config_key is absent from config. True means opt-out (included unless disabled).

keep_unmodified: bool = False

Preserve files on disk even when identical to the bundled default. When False, unmodified copies are flagged for cleanup by --delete-unmodified.

is_enabled(config)[source]

Whether this component is enabled by the given Config object.

Returns True when no config_key is set (unconditionally enabled) or when the corresponding config field is truthy.

Parameters:

config (object) – A Config instance.

Return type:

bool

class repomatic.registry.BundledComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False)[source]

Bases: Component

Files copied from repomatic/data/ to a target path.

class repomatic.registry.WorkflowComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False)[source]

Bases: Component

Thin-caller generation and header sync.

class repomatic.registry.ToolConfigComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='', tool_section='', insert_after=(), insert_before=(), sync_mode=SyncMode.BOOTSTRAP, preserved_keys=())[source]

Bases: Component

Merged into pyproject.toml.

source_file: str = ''

Filename in repomatic/data/.

tool_section: str = ''

The [tool.X] section name to check for existence.

insert_after: tuple[str, ...] = ()

Sections to insert after in pyproject.toml (in priority order).

insert_before: tuple[str, ...] = ()

Sections to insert before in pyproject.toml (if insert_after not found).

sync_mode: SyncMode = 1

How this config behaves when the section already exists.

BOOTSTRAP: insert once, skip if the section is present. ONGOING: replace template content on every sync while preserving local additions (extra array-of-tables entries, etc.).

preserved_keys: tuple[str, ...] = ()

Top-level keys whose existing values survive an ongoing sync.

Only meaningful when sync_mode is ONGOING. During replacement, these keys keep their value from the existing config rather than being overwritten by the template placeholder.

class repomatic.registry.TemplateComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False)[source]

Bases: Component

Directory tree (awesome-template).

class repomatic.registry.GeneratedComponent(name, description, init_default=InitDefault.INCLUDE, scope=RepoScope.ALL, files=(), config_key='', config_default=True, keep_unmodified=False, target='')[source]

Bases: Component

Produced from code (changelog).

Unlike bundled components, generated components have no files tuple. The target field records the output path so the auto-exclusion logic can detect stale copies on disk.

target: str = ''

Relative output path in the target repository.

repomatic.registry.COMPONENTS: tuple[Component, ...] = (BundledComponent(name='labels', description='Label config files (labels.toml + labeller rules)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='labeller-content-based.yaml', target='.github/labeller-content-based.yaml', file_id='labeller-content-based.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='labeller-file-based.yaml', target='.github/labeller-file-based.yaml', file_id='labeller-file-based.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='labels.toml', target='labels.toml', file_id='labels.toml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='')), config_key='', config_default=True, keep_unmodified=False), BundledComponent(name='codecov', description='Codecov PR comment config (.github/codecov.yaml)', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.NON_AWESOME: 3>, files=(FileEntry(source='codecov.yaml', target='.github/codecov.yaml', file_id='codecov.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''),), config_key='', config_default=True, keep_unmodified=True), BundledComponent(name='renovate', description='Renovate config (renovate.json5)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.NON_AWESOME: 3>, files=(FileEntry(source='renovate.json5', target='renovate.json5', file_id='renovate.json5', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''),), config_key='', config_default=True, keep_unmodified=False), BundledComponent(name='skills', description='Claude Code skill definitions (.claude/skills/)', init_default=<InitDefault.EXCLUDE: 2>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='skill-av-false-positive.md', target='.claude/skills/av-false-positive/SKILL.md', file_id='av-false-positive', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Release'), FileEntry(source='skill-awesome-triage.md', target='.claude/skills/awesome-triage/SKILL.md', file_id='awesome-triage', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', config_default=False, reusable=True, phase='Maintenance'), FileEntry(source='skill-babysit-ci.md', target='.claude/skills/babysit-ci/SKILL.md', file_id='babysit-ci', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Quality'), FileEntry(source='skill-benchmark-update.md', target='.claude/skills/benchmark-update/SKILL.md', file_id='benchmark-update', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Development'), FileEntry(source='skill-brand-assets.md', target='.claude/skills/brand-assets/SKILL.md', file_id='brand-assets', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Development'), FileEntry(source='skill-file-bug-report.md', target='.claude/skills/file-bug-report/SKILL.md', file_id='file-bug-report', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Maintenance'), FileEntry(source='skill-repomatic-audit.md', target='.claude/skills/repomatic-audit/SKILL.md', file_id='repomatic-audit', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Maintenance'), FileEntry(source='skill-repomatic-changelog.md', target='.claude/skills/repomatic-changelog/SKILL.md', file_id='repomatic-changelog', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Release'), FileEntry(source='skill-repomatic-deps.md', target='.claude/skills/repomatic-deps/SKILL.md', file_id='repomatic-deps', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Development'), FileEntry(source='skill-repomatic-init.md', target='.claude/skills/repomatic-init/SKILL.md', file_id='repomatic-init', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Setup'), FileEntry(source='skill-repomatic-lint.md', target='.claude/skills/repomatic-lint/SKILL.md', file_id='repomatic-lint', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Quality'), FileEntry(source='skill-repomatic-release.md', target='.claude/skills/repomatic-release/SKILL.md', file_id='repomatic-release', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Release'), FileEntry(source='skill-repomatic-sync.md', target='.claude/skills/repomatic-sync/SKILL.md', file_id='repomatic-sync', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Setup'), FileEntry(source='skill-repomatic-test.md', target='.claude/skills/repomatic-test/SKILL.md', file_id='repomatic-test', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Quality'), FileEntry(source='skill-repomatic-topics.md', target='.claude/skills/repomatic-topics/SKILL.md', file_id='repomatic-topics', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Development'), FileEntry(source='skill-sphinx-docs-sync.md', target='.claude/skills/sphinx-docs-sync/SKILL.md', file_id='sphinx-docs-sync', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Maintenance'), FileEntry(source='skill-translation-sync.md', target='.claude/skills/translation-sync/SKILL.md', file_id='translation-sync', scope=<RepoScope.AWESOME_ONLY: 2>, config_key='', config_default=False, reusable=True, phase='Maintenance'), FileEntry(source='skill-upstream-audit.md', target='.claude/skills/upstream-audit/SKILL.md', file_id='upstream-audit', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase='Maintenance')), config_key='', config_default=True, keep_unmodified=True), WorkflowComponent(name='workflows', description='Thin-caller workflow files', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.ALL: 1>, files=(FileEntry(source='autofix.yaml', target='.github/workflows/autofix.yaml', file_id='autofix.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='autolock.yaml', target='.github/workflows/autolock.yaml', file_id='autolock.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='cancel-runs.yaml', target='.github/workflows/cancel-runs.yaml', file_id='cancel-runs.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='changelog.yaml', target='.github/workflows/changelog.yaml', file_id='changelog.yaml', scope=<RepoScope.NON_AWESOME: 3>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='debug.yaml', target='.github/workflows/debug.yaml', file_id='debug.yaml', scope=<RepoScope.NON_AWESOME: 3>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='docs.yaml', target='.github/workflows/docs.yaml', file_id='docs.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='labels.yaml', target='.github/workflows/labels.yaml', file_id='labels.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='lint.yaml', target='.github/workflows/lint.yaml', file_id='lint.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='release.yaml', target='.github/workflows/release.yaml', file_id='release.yaml', scope=<RepoScope.NON_AWESOME: 3>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='renovate.yaml', target='.github/workflows/renovate.yaml', file_id='renovate.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=True, phase=''), FileEntry(source='tests.yaml', target='.github/workflows/tests.yaml', file_id='tests.yaml', scope=<RepoScope.ALL: 1>, config_key='', config_default=False, reusable=False, phase=''), FileEntry(source='unsubscribe.yaml', target='.github/workflows/unsubscribe.yaml', file_id='unsubscribe.yaml', scope=<RepoScope.ALL: 1>, config_key='notification.unsubscribe', config_default=False, reusable=True, phase='')), config_key='', config_default=True, keep_unmodified=False), TemplateComponent(name='awesome-template', description='Boilerplate for awesome-* repositories', init_default=<InitDefault.AUTO: 3>, scope=<RepoScope.ALL: 1>, files=(), config_key='awesome-template.sync', config_default=True, keep_unmodified=False), GeneratedComponent(name='changelog', description='Minimal changelog.md', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.NON_AWESOME: 3>, files=(), config_key='', config_default=True, keep_unmodified=False, target='changelog.md'), ToolConfigComponent(name='lychee', description='Lychee link checker configuration', init_default=<InitDefault.INCLUDE: 1>, scope=<RepoScope.AWESOME_ONLY: 2>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='lychee.toml', tool_section='tool.lychee', insert_after=(), insert_before=(), sync_mode=<SyncMode.ONGOING: 2>, preserved_keys=()), ToolConfigComponent(name='ruff', description='Ruff linter/formatter configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='ruff.toml', tool_section='tool.ruff', insert_after=('tool.uv', 'tool.uv.build-backend'), insert_before=('tool.pytest',), sync_mode=<SyncMode.BOOTSTRAP: 1>, preserved_keys=()), ToolConfigComponent(name='pytest', description='Pytest test configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='pytest.toml', tool_section='tool.pytest', insert_after=('tool.ruff', 'tool.ruff.format'), insert_before=('tool.mypy',), sync_mode=<SyncMode.BOOTSTRAP: 1>, preserved_keys=()), ToolConfigComponent(name='mypy', description='Mypy type checking configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='mypy.toml', tool_section='tool.mypy', insert_after=('tool.pytest',), insert_before=('tool.nuitka', 'tool.bumpversion'), sync_mode=<SyncMode.BOOTSTRAP: 1>, preserved_keys=()), ToolConfigComponent(name='mdformat', description='mdformat Markdown formatter configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='mdformat.toml', tool_section='tool.mdformat', insert_after=('tool.coverage',), insert_before=('tool.bumpversion',), sync_mode=<SyncMode.BOOTSTRAP: 1>, preserved_keys=()), ToolConfigComponent(name='bumpversion', description='bump-my-version configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='bumpversion.toml', tool_section='tool.bumpversion', insert_after=('tool.mdformat', 'tool.nuitka', 'tool.mypy'), insert_before=('tool.typos',), sync_mode=<SyncMode.ONGOING: 2>, preserved_keys=('current_version',)), ToolConfigComponent(name='typos', description='Typos spell checker configuration', init_default=<InitDefault.EXPLICIT: 4>, scope=<RepoScope.ALL: 1>, files=(), config_key='', config_default=True, keep_unmodified=False, source_file='typos.toml', tool_section='tool.typos', insert_after=('tool.bumpversion',), insert_before=('tool.pytest',), sync_mode=<SyncMode.BOOTSTRAP: 1>, preserved_keys=()))

The component registry.

Single source of truth for all resources managed by the init subcommand. Every component declares its kind, selection default, file entries, and behavioral flags. All derived constants are computed from this tuple.

repomatic.registry.DEFAULT_REPO: str = 'kdeldycke/repomatic'

Default upstream repository for reusable workflows.

repomatic.registry.UPSTREAM_SOURCE_GLOB: str = 'repomatic/**'

Path glob for the upstream source directory in canonical workflows.

Canonical workflow paths: filters use this glob to match source code changes. In downstream repos, this is replaced with the project’s own source directory.

repomatic.registry.UPSTREAM_SOURCE_PREFIX: str = 'repomatic/'

Path prefix for upstream-specific files in canonical workflows.

Paths starting with this prefix (but not matching UPSTREAM_SOURCE_GLOB) are dropped in downstream thin callers because they reference files that only exist in the upstream repository (e.g., repomatic/data/renovate.json5).

repomatic.registry.SKILL_PHASE_ORDER: tuple[str, ...] = ('Setup', 'Development', 'Quality', 'Maintenance', 'Release')

Canonical display order for lifecycle phases in list-skills output.

repomatic.registry.ALL_COMPONENTS: dict[str, str] = {'awesome-template': 'Boilerplate for awesome-* repositories', 'bumpversion': 'bump-my-version configuration', 'changelog': 'Minimal changelog.md', 'codecov': 'Codecov PR comment config (.github/codecov.yaml)', 'labels': 'Label config files (labels.toml + labeller rules)', 'lychee': 'Lychee link checker configuration', 'mdformat': 'mdformat Markdown formatter configuration', 'mypy': 'Mypy type checking configuration', 'pytest': 'Pytest test configuration', 'renovate': 'Renovate config (renovate.json5)', 'ruff': 'Ruff linter/formatter configuration', 'skills': 'Claude Code skill definitions (.claude/skills/)', 'typos': 'Typos spell checker configuration', 'workflows': 'Thin-caller workflow files'}

All available init components.

repomatic.registry.REUSABLE_WORKFLOWS: tuple[str, ...] = ('autofix.yaml', 'autolock.yaml', 'cancel-runs.yaml', 'changelog.yaml', 'debug.yaml', 'docs.yaml', 'labels.yaml', 'lint.yaml', 'release.yaml', 'renovate.yaml', 'unsubscribe.yaml')

Workflow filenames that support workflow_call triggers.

repomatic.registry.NON_REUSABLE_WORKFLOWS: frozenset[str] = frozenset({'tests.yaml'})

Workflows without workflow_call that cannot be used as thin callers.

repomatic.registry.ALL_WORKFLOW_FILES: tuple[str, ...] = ('autofix.yaml', 'autolock.yaml', 'cancel-runs.yaml', 'changelog.yaml', 'debug.yaml', 'docs.yaml', 'labels.yaml', 'lint.yaml', 'release.yaml', 'renovate.yaml', 'tests.yaml', 'unsubscribe.yaml')

All workflow filenames (reusable and non-reusable).

repomatic.registry.SKILL_PHASES: dict[str, str] = {'av-false-positive': 'Release', 'awesome-triage': 'Maintenance', 'babysit-ci': 'Quality', 'benchmark-update': 'Development', 'brand-assets': 'Development', 'file-bug-report': 'Maintenance', 'repomatic-audit': 'Maintenance', 'repomatic-changelog': 'Release', 'repomatic-deps': 'Development', 'repomatic-init': 'Setup', 'repomatic-lint': 'Quality', 'repomatic-release': 'Release', 'repomatic-sync': 'Setup', 'repomatic-test': 'Quality', 'repomatic-topics': 'Development', 'sphinx-docs-sync': 'Maintenance', 'translation-sync': 'Maintenance', 'upstream-audit': 'Maintenance'}

Maps skill names to lifecycle phases for display grouping.

repomatic.registry.FILE_SELECTOR_COMPONENTS: tuple[str, ...] = ('labels', 'codecov', 'renovate', 'skills', 'workflows')

Components that support file-level component/file selectors.

repomatic.registry.COMPONENT_HELP_TABLE: str = '    labels              Label config files (labels.toml + labeller rules)\n    codecov             Codecov PR comment config (.github/codecov.yaml)\n    renovate            Renovate config (renovate.json5)\n    skills              Claude Code skill definitions (.claude/skills/)\n    workflows           Thin-caller workflow files\n    awesome-template    Boilerplate for awesome-* repositories\n    changelog           Minimal changelog.md\n    lychee              Lychee link checker configuration\n    ruff                Ruff linter/formatter configuration\n    pytest              Pytest test configuration\n    mypy                Mypy type checking configuration\n    mdformat            mdformat Markdown formatter configuration\n    bumpversion         bump-my-version configuration\n    typos               Typos spell checker configuration'

Formatted component table for CLI help text.

repomatic.registry.valid_file_ids(component)[source]

Return valid file identifiers for a component.

Components with file entries report their declared file_id values. Returns an empty set for components without file-level selection (e.g., changelog, tool configs).

Return type:

frozenset[str]

repomatic.registry.excluded_rel_path(component, file_id)[source]

Map a component and file identifier to its relative output path.

Returns None when the identifier cannot be resolved (e.g., for tool config components that have no file-level exclusion support).

Return type:

str | None

repomatic.registry.parse_component_entries(entries, *, context='entry')[source]

Parse component entries into full-component and file-level sets.

Bare names (no /) must be component names from ALL_COMPONENTS. Qualified component/identifier entries target individual files. Raises ValueError on unknown entries.

Used by both the exclude config path and the CLI positional selection, with context controlling error message wording.

Parameters:

context (str) – Label for error messages (e.g., "exclude", "selection").

Return type:

tuple[set[str], dict[str, set[str]]]

Returns:

(full_components, file_selections) where file_selections maps component names to sets of file identifiers.

repomatic.release_prep module

Prepare a release by updating changelog, citation, readme, and workflow files.

A release cycle produces exactly two commits that must be merged via “Rebase and merge” (never squash):

  1. Freeze commit ([changelog] Release vX.Y.Z):

    • Strips the .dev0 suffix from the version.

    • Finalizes the changelog date and comparison URL.

    • Freezes workflow action references: @main@vX.Y.Z.

    • Freezes CLI invocations: --from . repomatic'repomatic==X.Y.Z'.

    • Freezes readme binary download URLs to versioned release paths.

    • Sets the release date in citation.cff.

  2. Unfreeze commit ([changelog] Post-release bump vX.Y.Z vX.Y.(Z+1)):

    • Reverts action references: @vX.Y.Z@main.

    • Reverts CLI invocations back to local source for dogfooding.

    • Bumps the version with a .dev0 suffix.

    • Adds a new unreleased changelog section.

The auto-tagging job in release.yaml depends on these being separate commits — it uses release_commits_matrix to identify and tag only the freeze commit. Squash-merging would collapse both into one, breaking the tagging logic. See the detect-squash-merge job for the safeguard.

Both operations are idempotent: re-running on an already-frozen or already-unfrozen tree is a no-op.

class repomatic.release_prep.ReleasePrep(changelog_path=None, citation_path=None, workflow_dir=None, readme_path=None, default_branch='main')[source]

Bases: object

Prepare files for a release by updating dates, URLs, and removing warnings.

property current_version: str[source]

Extract current version from bump-my-version config in pyproject.toml.

property release_date: str[source]

Return today’s date in UTC as YYYY-MM-DD.

set_citation_release_date()[source]

Update the date-released field in citation.cff.

Return type:

bool

Returns:

True if the file was modified.

freeze_workflow_urls()[source]

Replace workflow URLs from default branch to versioned tag.

This is part of the freeze step: it freezes workflow references to the release tag so released versions reference immutable URLs.

Replaces /repomatic/{default_branch}/ with /repomatic/v{version}/ and /repomatic/.github/actions/...@{default_branch} with /repomatic/.github/actions/...@v{version} in all workflow YAML files.

Return type:

int

Returns:

Number of files modified.

freeze_readme_download_urls(version)[source]

Replace binary download URLs in readme with versioned release paths.

This is part of the freeze step: it freezes readme download links to a specific GitHub release so users get explicit, versioned URLs instead of the /releases/latest/download/ redirect.

Handles two input forms:

  • Initial (never frozen): /releases/latest/download/repomatic-linux-arm64.bin

  • Previously frozen: /releases/download/v6.0.0/repomatic-6.0.0-linux-arm64.bin

Both are transformed to: /releases/download/v{version}/repomatic-{version}-linux-arm64.bin

Note

No unfreeze method is needed. Unlike workflow URLs (which toggle @main@vX.Y.Z), readme download URLs ratchet forward — they always point to a specific release. After unfreeze, the readme still shows the last release’s URLs, which is correct for users wanting stable binaries.

Parameters:

version (str) – The release version to freeze to.

Return type:

bool

Returns:

True if the file was modified.

freeze_cli_version(version)[source]

Replace local source CLI invocations with a frozen PyPI version.

This is part of the freeze step: it freezes repomatic invocations to a specific PyPI version so the released workflow files reference a published package. Downstream repos that check out a tagged release will install from PyPI rather than expecting a local source tree.

Replaces --from . repomatic with 'repomatic=={version}' in all workflow YAML files, and with repomatic=={version} (unquoted) in renovate.json5 files (where the command lives inside bash -c '...' and single quotes would break the outer quoting).

Comment lines in YAML (starting with #) and JSON5 (starting with //) are skipped to avoid corrupting explanatory comments.

Parameters:

version (str) – The PyPI version to freeze to.

Return type:

int

Returns:

Number of files modified.

unfreeze_cli_version()[source]

Replace frozen PyPI CLI invocations with local source.

This is part of the unfreeze step: it reverts repomatic invocations back to local source (--from . repomatic) for the next development cycle on main.

Replaces 'repomatic==X.Y.Z' (quoted, in YAML) and repomatic==X.Y.Z (unquoted, in renovate.json5) with --from . repomatic.

Comment lines are skipped (see freeze_cli_version()).

Return type:

int

Returns:

Number of files modified.

unfreeze_workflow_urls()[source]

Replace workflow URLs from versioned tag back to default branch.

This is part of the unfreeze step: it reverts workflow references back to the default branch for the next development cycle.

Replaces /repomatic/v{version}/ with /repomatic/{default_branch}/ and /repomatic/.github/actions/...@v{version} with /repomatic/.github/actions/...@{default_branch} in all workflow YAML files.

Return type:

int

Returns:

Number of files modified.

prepare_release(update_workflows=False)[source]

Run all freeze steps to prepare the release commit.

Parameters:

update_workflows (bool) – If True, also freeze workflow URLs to versioned tag and freeze CLI invocations to the current version.

Return type:

list[Path]

Returns:

List of modified files.

post_release(update_workflows=False)[source]

Run all unfreeze steps to prepare the post-release commit.

Parameters:

update_workflows (bool) – If True, unfreeze workflow URLs back to default branch and unfreeze CLI invocations back to local source.

Return type:

list[Path]

Returns:

List of modified files.

repomatic.renovate module

Renovate-related utilities for GitHub Actions workflows.

This module provides utilities for managing Renovate prerequisites and migrating from Dependabot to Renovate. uv lock file operations (version parsing, noise detection, vulnerability auditing) live in repomatic.uv.

repomatic.renovate.RENOVATE_CONFIG_PATH = PosixPath('renovate.json5')

Canonical path to the Renovate configuration file.

class repomatic.renovate.CheckFormat(*values)[source]

Bases: StrEnum

Output format for Renovate prerequisite checks.

github = 'github'
json = 'json'
text = 'text'
class repomatic.renovate.RenovateCheckResult(renovate_config_exists, dependabot_config_path, dependabot_security_disabled, commit_statuses_permission, contents_permission=True, issues_permission=True, pull_requests_permission=True, vulnerability_alerts_permission=True, workflows_permission=True, repo='')[source]

Bases: object

Result of all Renovate prerequisite checks.

This dataclass holds the results of each check, allowing workflows to consume the data and build dynamic PR bodies or conditional logic.

renovate_config_exists: bool

Whether renovate.json5 exists in the repository.

dependabot_config_path: str

Path to Dependabot config file, or empty string if not found.

dependabot_security_disabled: bool

Whether Dependabot security updates are disabled.

commit_statuses_permission: bool

Whether the token has commit statuses permission.

contents_permission: bool = True

Whether the token has contents permission.

issues_permission: bool = True

Whether the token has issues permission.

pull_requests_permission: bool = True

Whether the token has pull requests permission.

vulnerability_alerts_permission: bool = True

Whether the token has Dependabot alerts permission.

workflows_permission: bool = True

Whether the token has workflows permission.

repo: str = ''

Repository in ‘owner/repo’ format, used for generating settings links.

to_github_output()[source]

Format results for GitHub Actions output.

Return type:

str

Returns:

Multi-line string in key=value format for $GITHUB_OUTPUT.

to_json()[source]

Format results as JSON.

Return type:

str

Returns:

JSON string representation of the check results.

to_pr_body()[source]

Generate PR body for the migration PR.

Return type:

str

Returns:

Markdown-formatted PR body with changes and prerequisites table.

repomatic.renovate.get_dependabot_config_path()[source]

Get the path to the Dependabot configuration file if it exists.

Return type:

Path | None

Returns:

Path to the Dependabot config file, or None if not found.

repomatic.renovate.check_dependabot_config_absent()[source]

Check that no Dependabot version updates config file exists.

Renovate handles dependency updates, so Dependabot should be disabled.

Return type:

tuple[bool, str]

Returns:

Tuple of (passed, message).

repomatic.renovate.check_dependabot_security_disabled(repo)[source]

Check that Dependabot security updates are disabled.

Renovate creates security PRs instead.

Parameters:

repo (str) – Repository in ‘owner/repo’ format.

Return type:

tuple[bool, str]

Returns:

Tuple of (passed, message).

repomatic.renovate.check_renovate_config_exists()[source]

Check if renovate.json5 configuration file exists.

Return type:

tuple[bool, str]

Returns:

Tuple of (exists, message).

repomatic.renovate.collect_check_results(repo, sha)[source]

Collect all Renovate prerequisite check results.

Runs all checks and returns structured results that can be formatted as JSON or GitHub Actions output.

Parameters:
  • repo (str) – Repository in ‘owner/repo’ format.

  • sha (str) – Commit SHA for permission checks.

Return type:

RenovateCheckResult

Returns:

RenovateCheckResult with all check outcomes.

repomatic.renovate.run_migration_checks(repo, sha)[source]

Run Renovate migration prerequisite checks with console output.

Checks for: - Missing renovate.json5 configuration - Existing Dependabot configuration - Dependabot security updates enabled - PAT permissions: commit statuses, contents, issues, pull requests,

vulnerability alerts, workflows

Parameters:
  • repo (str) – Repository in ‘owner/repo’ format.

  • sha (str) – Commit SHA for permission checks.

Return type:

int

Returns:

Exit code (0 for success, 1 for fatal errors).

repomatic.rst_to_myst module

Convert sphinx-apidoc RST output to MyST markdown.

Note

The converter handles only the narrow RST subset that sphinx-apidoc generates: section headings (title + underline), automodule directives with indented options, and structural headers like Submodules.

Autodoc directives cannot be used as native MyST directives because they perform internal rST nested parsing that requires an rST parser context only {eval-rst} provides. See MyST-Parser #587.

repomatic.rst_to_myst.convert_apidoc_rst_to_myst(content)[source]

Convert sphinx-apidoc RST to MyST markdown with {eval-rst} blocks.

Parameters:

content (str) – RST content produced by sphinx-apidoc.

Return type:

str

Returns:

Equivalent MyST markdown.

repomatic.rst_to_myst.convert_rst_files_in_directory(directory)[source]

Convert sphinx-apidoc RST files to MyST markdown in the given directory.

For each .rst file containing .. automodule:: directives:

  • If a .md file with the same stem exists, delete the .rst (the existing markdown takes precedence).

  • Otherwise, convert the RST content to MyST and write a .md file, then delete the .rst.

Parameters:

directory (Path) – Directory to scan for .rst files.

Return type:

list[Path]

Returns:

List of newly created .md file paths.

repomatic.sponsor module

Check if a GitHub user is a sponsor of another user or organization.

Uses the GitHub GraphQL API via the gh CLI to query sponsorship data. Supports both user and organization owners, with pagination for accounts that have more than 100 sponsors.

When run in GitHub Actions, defaults are read from Metadata for owner and repository, and from GITHUB_EVENT_PATH for the author and issue/PR number.

repomatic.sponsor.get_default_owner()[source]

Get the repository owner from CI context.

Delegates to Metadata.repo_owner.

Return type:

str | None

repomatic.sponsor.get_default_repo()[source]

Get the repository slug from CI context.

Delegates to Metadata.repo_slug.

Return type:

str | None

repomatic.sponsor.get_default_author()[source]

Get the issue/PR author from the GitHub event payload.

Return type:

str | None

repomatic.sponsor.get_default_number()[source]

Get the issue/PR number from the GitHub event payload.

Return type:

int | None

repomatic.sponsor.is_pull_request()[source]

Check if the current event is a pull request.

Return type:

bool

repomatic.sponsor.get_sponsors(owner: str) frozenset[str][source]

Get all sponsors for a user or organization.

Tries the user query first, then falls back to organization query.

Results are cached to avoid redundant API calls within the same process.

Parameters:

owner (str) – The GitHub username or organization name.

Return type:

frozenset[str]

Returns:

Frozenset of sponsor login names.

repomatic.sponsor.is_sponsor(owner, user)[source]

Check if a user is a sponsor of an owner.

Parameters:
  • owner (str) – The GitHub username or organization to check sponsorship for.

  • user (str) – The GitHub username to check if they are a sponsor.

Return type:

bool

Returns:

True if user is a sponsor of owner, False otherwise.

repomatic.sponsor.add_sponsor_label(repo, number, label, is_pr=False)[source]

Add a label to an issue or PR.

Parameters:
  • repo (str) – The repository in “owner/repo” format.

  • number (int) – The issue or PR number.

  • label (str) – The label to add.

  • is_pr (bool) – True if this is a PR, False for an issue.

Return type:

bool

Returns:

True if label was added successfully, False otherwise.

repomatic.test_matrix module

Test matrix constants for CI workflows.

Defines the GitHub-hosted runner images and Python versions used to build test matrices. Separating these from repomatic.metadata makes the CI matrix configuration self-contained and easier to update when runner images or Python releases change.

repomatic.test_matrix.TEST_RUNNERS_FULL = ('ubuntu-24.04-arm', 'ubuntu-slim', 'macos-26', 'macos-26-intel', 'windows-11-arm', 'windows-2025')

GitHub-hosted runners for the full test matrix.

Two variants per platform (one per architecture) to keep the matrix small. See available images.

repomatic.test_matrix.TEST_RUNNERS_PR = ('ubuntu-slim', 'macos-26', 'windows-2025')

Reduced runner set for pull request test matrices.

One runner per platform, skipping redundant architecture variants.

repomatic.test_matrix.TEST_PYTHON_FULL = ('3.10', '3.14', '3.14t', '3.15')

Python versions for the full test matrix.

Intermediate versions (3.11, 3.12, 3.13) are skipped to reduce CI load.

repomatic.test_matrix.TEST_PYTHON_PR = ('3.10', '3.14')

Reduced Python version set for pull request test matrices.

Skips experimental versions (free-threaded, development) to reduce CI load.

repomatic.test_matrix.UNSTABLE_PYTHON_VERSIONS: Final[frozenset[str]] = frozenset({'3.15'})

Python versions still in development.

Jobs using these versions run with continue-on-error in CI.

repomatic.test_matrix.MYPY_VERSION_MIN: Final = (3, 8)

Earliest version supported by Mypy’s --python-version 3.x parameter.

Sourced from Mypy original implementation.

repomatic.test_plan module

exception repomatic.test_plan.SkippedTest[source]

Bases: Exception

Raised when a test case should be skipped.

class repomatic.test_plan.CLITestCase(cli_parameters=<factory>, skip_platforms=<factory>, only_platforms=<factory>, timeout=None, exit_code=None, strip_ansi=False, output_contains=<factory>, stdout_contains=<factory>, stderr_contains=<factory>, output_regex_matches=<factory>, stdout_regex_matches=<factory>, stderr_regex_matches=<factory>, output_regex_fullmatch=None, stdout_regex_fullmatch=None, stderr_regex_fullmatch=None, execution_trace=None)[source]

Bases: object

cli_parameters: tuple[str, ...] | str

Parameters, arguments and options to pass to the CLI.

skip_platforms: Trait | Group | str | None | Iterable[Trait | Group | str | None | Iterable[_TNestedReferences]]
only_platforms: Trait | Group | str | None | Iterable[Trait | Group | str | None | Iterable[_TNestedReferences]]
timeout: float | str | None = None
exit_code: int | str | None = None
strip_ansi: bool = False
output_contains: tuple[str, ...] | str
stdout_contains: tuple[str, ...] | str
stderr_contains: tuple[str, ...] | str
output_regex_matches: tuple[Pattern | str, ...] | str
stdout_regex_matches: tuple[Pattern | str, ...] | str
stderr_regex_matches: tuple[Pattern | str, ...] | str
output_regex_fullmatch: Pattern | str | None = None
stdout_regex_fullmatch: Pattern | str | None = None
stderr_regex_fullmatch: Pattern | str | None = None
execution_trace: str | None = None

User-friendly rendering of the CLI command execution and its output.

run_cli_test(command, additional_skip_platforms, default_timeout)[source]

Run a CLI command and check its output against the test case.

The provided command can be either:

  • a path to a binary or script to execute;

  • a command name to be searched in the PATH,

  • a command line with arguments to be parsed and executed by the shell.

Todo

Add support for environment variables.

Todo

Add support for proper mixed <stdout>/<stderr> stream as a single, intertwined output.

repomatic.test_plan.parse_test_plan(plan_string)[source]
Return type:

Generator[CLITestCase, None, None]

repomatic.tool_runner module

Unified tool runner with managed config resolution.

Provides repomatic run <tool> — a single entry point that installs an external tool at a pinned version, resolves its configuration through a strict 4-level precedence chain, translates [tool.X] sections from pyproject.toml into the tool’s native format, and invokes the tool with the resolved config.

Important

Config resolution precedence (first match wins, no merging):

  1. Native config file — tool’s own config file in the repo.

  2. ``[tool.X]`` in ``pyproject.toml`` — translated to native format.

  3. Bundled default — from repomatic/data/.

  4. Bare invocation — no config at all.

repomatic.tool_runner.GENERATED_HEADER_TEMPLATE = 'Generated by {command} v{version} - https://github.com/kdeldycke/repomatic'

Template for the first line of generated-file headers.

Used by both CLI commands (e.g. sync-mailmap) and the tool runner (e.g. run shfmt) to stamp files with provenance. Format fields: command (full command path) and version (package version).

repomatic.tool_runner.generated_header(command, comment_prefix='# ')[source]

Return a generated-by header block with timestamp.

Parameters:
  • command (str) – Full command path (e.g. repomatic sync-mailmap).

  • comment_prefix (str) – Comment prefix for the target format.

Return type:

str

class repomatic.tool_runner.ArchiveFormat(*values)[source]

Bases: Enum

Archive format for binary tool downloads.

RAW = 'raw'
TAR_GZ = 'tar.gz'
TAR_XZ = 'tar.xz'
ZIP = 'zip'
tarfile_mode()[source]

Return the tarfile.open mode string for this format.

Raises:

ValueError – If called on a non-tar format.

Return type:

Literal['r:gz', 'r:xz']

class repomatic.tool_runner.NativeFormat(*values)[source]

Bases: Enum

Target format for [tool.X] translation.

YAML = 'yaml'
TOML = 'toml'
JSON = 'json'
EDITORCONFIG = 'editorconfig'
serialize(data, tool_name='')[source]

Serialize a config dict to this format’s string representation.

Parameters:
  • data (dict) – Configuration dictionary to serialize.

  • tool_name (str) – Tool name for the generated-by header comment.

Return type:

str

repomatic.tool_runner.PlatformKey

A (platform_or_group, architecture) pair used as binary lookup key.

The platform element can be a single Platform (like MACOS) or a Group (like LINUX, which matches any Linux distribution). The architecture is always a concrete Architecture.

Resolution order in BinarySpec.resolve_platform():

  1. Exact Platform match (current_platform() == key_platform).

  2. Group membership (current_platform() in key_group), preferring the group with fewest members (most specific).

alias of tuple[Platform | Group, Architecture]

class repomatic.tool_runner.BinarySpec(urls, checksums, archive_format, archive_executable=None, strip_components=0)[source]

Bases: object

Platform-specific binary download specification.

Keys are PlatformKey tuples pairing an extra-platforms Platform or Group with an Architecture. This lets callers use broad groups (LINUX matches any distro) or specific platforms (DEBIAN) with full detection heuristics from extra-platforms.

Hint

Structural integrity checks (key types, checksum format, URL placeholders, strip_components consistency) are enforced in test_tool_spec_integrity. If the registry becomes user-configurable in the future, move these checks to __post_init__.

urls: dict[tuple[Platform | Group, Architecture], str]

Platform key to URL template mapping. URLs use {version} placeholders.

checksums: dict[tuple[Platform | Group, Architecture], str]

Platform key to SHA-256 hex digest mapping.

archive_format: ArchiveFormat | dict[tuple[Platform | Group, Architecture] | Platform | Group, ArchiveFormat]

Archive format of the downloaded file.

A single ArchiveFormat applies to every platform. A dict maps platform specifiers to formats, allowing mixed archives in one spec:

archive_format={ALL_PLATFORMS: ArchiveFormat.TAR_GZ, WINDOWS: ArchiveFormat.ZIP}

Dict keys follow the same resolution as resolve_platform(): exact PlatformKey tuple first, then bare Platform equality, then Group membership (smallest group wins).

archive_executable: str | None = None

Path of the executable inside the archive. None defaults to the tool name. For RAW format, used as the final filename.

strip_components: int = 0

Number of leading path components to strip when extracting.

resolve_platform()[source]

Match the current environment against registered platform keys.

Uses current_platform() and current_architecture() from extra-platforms, inheriting its full detection heuristics.

Return type:

tuple[Platform | Group, Architecture]

Returns:

The matching PlatformKey.

Raises:

RuntimeError – If no key matches the current environment.

get_archive_format(key)[source]

Return the archive format for the given platform key.

When archive_format is a single ArchiveFormat, returns it directly. When it is a dict, resolves in order: exact PlatformKey tuple, bare Platform equality, then Group membership (smallest group wins).

Return type:

ArchiveFormat

static platform_cache_key(key)[source]

Derive a filesystem-safe cache path segment from a platform key.

Return type:

str

Returns:

A string like linux-aarch64 or macos-x86_64.

class repomatic.tool_runner.ToolSpec(name, display_name=None, version='', package=None, executable=None, native_config_files=(), config_flag=None, native_format=NativeFormat.YAML, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), with_packages=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, binary=None, source_url=None, config_docs_url=None, cli_docs_url=None)[source]

Bases: object

Specification for an external tool managed by repomatic.

Hint

Structural integrity checks (name format, version format, flag conventions, field consistency) are enforced in test_tool_spec_integrity. If the registry becomes user-configurable in the future, move these checks to __post_init__.

Hint

CLI parser quirks for config_after_subcommand

Tools that use subcommands (tool <subcmd> [flags] [files]) may require config_flag to appear after the subcommand name, depending on the CLI parser framework:

  • clap (Rust): global flags accepted before or after the subcommand. No special handling needed. Used by: ruff, labelmaker.

  • cobra (Go): root-level flags inherited by all subcommands, accepted in both positions. No special handling needed. Used by: gitleaks.

  • click (Python): global flags accepted before or after the subcommand. No special handling needed. Used by: bump-my-version.

  • bpaf (Rust): #[bpaf(external)] fields are scoped inside the subcommand variant, so tool <subcmd> --flag works but tool --flag <subcmd> does not. Set config_after_subcommand=True. Used by: biome.

name: str

Tool identity: CLI name for repomatic run <name>, default PyPI package name, and default executable name.

display_name: str | None = None

Human-readable name with proper casing for documentation (like 'Biome', 'Gitleaks'). None defaults to name.

version: str = ''

Pinned version (e.g., '1.38.0').

package: str | None = None

PyPI package name. None defaults to name. Only set when the package name differs from the tool name.

executable: str | None = None

Executable name if different from the tool name. None defaults to the registry key.

native_config_files: tuple[str, ...] = ()

Config filenames the tool auto-discovers, checked in order.

Paths relative to repo root (e.g., 'zizmor.yaml', '.github/actionlint.yaml'). Empty for tools with no config file.

config_flag: str | None = None

CLI flag to pass a config file path (e.g., '--config', '--config-file'). None if the tool only reads from fixed paths.

native_format: NativeFormat = 'yaml'

Target format for [tool.X] translation.

default_config: str | None = None

Filename in repomatic/data/ for bundled defaults, stored in native_format. None if no bundled default exists.

reads_pyproject: bool = False

Whether the tool natively reads [tool.X] from pyproject.toml.

When True and [tool.X] exists in pyproject.toml, repomatic skips Level 2 translation (the tool reads it directly). Resolution still falls through to Level 3 (bundled default) and Level 4 (bare) when no config is found.

default_flags: tuple[str, ...] = ()

Flags always passed to the tool (e.g., ('--strict',)).

ci_flags: tuple[str, ...] = ()

Flags added only when $GITHUB_ACTIONS is set (e.g., output format).

with_packages: tuple[str, ...] = ()

Extra packages installed alongside the tool (e.g., mdformat plugins).

Passed as --with <pkg> to uvx.

needs_venv: bool = False

If True, use uv run (project venv) instead of uvx (isolated).

Required when the tool imports project code (mypy, pytest).

computed_params: Callable[[Metadata], list[str]] | None = None

Callable that receives a Metadata instance and returns extra CLI args derived from project metadata (e.g., mypy’s --python-version from requires-python). None if no computed params.

config_after_subcommand: bool = False

Insert config_flag after the first token of extra_args.

Needed for tools whose CLI parser (e.g., bpaf) scopes global options inside the subcommand, so tool subcommand --config-path X is valid but tool --config-path X subcommand is not. When True, config_args are spliced after the first element of extra_args (the subcommand name).

post_process: Callable[[Sequence[str]], None] | None = None

Callback invoked on extra_args after the tool exits successfully.

Intended for temporary workarounds that fix known upstream formatting bugs in-place. Remove the callback once upstream ships the fix.

binary: BinarySpec | None = None

Platform-specific binary download spec. When set, the tool is downloaded as a binary instead of installed via uvx or uv run.

source_url: str | None = None

GitHub repository or project homepage URL.

config_docs_url: str | None = None

URL to the tool’s configuration reference.

cli_docs_url: str | None = None

URL to the tool’s CLI usage documentation.

repomatic.tool_runner.get_data_file_path(filename)[source]

Yield the filesystem path of a bundled data file.

Unlike init_project.get_data_content() which returns string content, this yields a Path suitable for passing to external tools via --config <path>. The path is valid only within the context manager.

Return type:

Iterator[Path]

repomatic.tool_runner.load_pyproject_tool_section(tool_name)[source]

Load [tool.<tool_name>] from pyproject.toml in the current directory.

Return type:

dict[str, Any]

Returns:

The tool’s config dict, or empty dict if not found.

repomatic.tool_runner.resolve_config(spec, tool_config=None)[source]

Resolve config for a tool using the 4-level precedence chain.

Parameters:
  • spec (ToolSpec) – Tool specification.

  • tool_config (dict[str, Any] | None) – Pre-loaded [tool.X] config dict. If None, reads from pyproject.toml in the current directory.

Return type:

tuple[list[str], Path | None]

Returns:

Tuple of (extra CLI args for config, path to clean up). The path is None when no cleanup is needed (cache-based configs persist across runs). Non-None paths are CWD files written for tools that have no --config flag.

repomatic.tool_runner.binary_tool_context(name, no_cache=False)[source]

Download a binary tool and yield its executable path.

For tools invoked indirectly by repomatic commands (e.g., labelmaker called by sync-labels) rather than via run_tool(). Downloads once; the binary stays valid for the context’s duration. On a cache hit the yielded path points to the cache and the staging directory is empty.

Parameters:
  • name (str) – Tool name (must be in TOOL_REGISTRY with binary set).

  • no_cache (bool) – Bypass the binary cache when True.

Yields:

Path to the ready-to-run executable.

repomatic.tool_runner.run_tool(name, extra_args=(), version=None, checksum=None, skip_checksum=False, no_cache=False)[source]

Run an external tool with managed config resolution.

Parameters:
  • name (str) – Tool name (must be in TOOL_REGISTRY).

  • extra_args (Sequence[str]) – Extra arguments passed through to the tool.

  • version (str | None) – Override the pinned version.

  • checksum (str | None) – Override the SHA-256 checksum for the current platform.

  • skip_checksum (bool) – Skip SHA-256 verification entirely.

  • no_cache (bool) – Bypass the binary cache when True.

Return type:

int

Returns:

The tool’s exit code.

repomatic.tool_runner.resolve_config_source(spec)[source]

Return a human-readable description of the active config source.

Used by repomatic run --list to show which precedence level is active for each tool in the current repo.

Return type:

str

repomatic.tool_runner.find_unmodified_configs()[source]

Find native config files identical to their bundled defaults.

Iterates over every tool in TOOL_REGISTRY that has a default_config. For each, checks whether any of its native_config_files exists on disk and is content-identical to the bundled default after trailing-whitespace normalization.

The normalization (rstrip() + "\n") matches the convention used by _init_config_files when writing files during init.

Return type:

list[tuple[str, str]]

Returns:

List of (tool_name, relative_path) tuples for each unmodified file found.

repomatic.uv module

uv lock file operations and vulnerability auditing.

This module provides utilities for managing uv.lock files: parsing versions, detecting timestamp noise, computing diff tables, auditing for vulnerabilities, and fetching release notes from GitHub.

repomatic.uv.uv_cmd(subcommand, *, frozen=False)[source]

Build a uv <subcommand> command prefix with standard flags.

Always includes --no-progress. Adds --frozen when requested (appropriate for run, export, sync — not for lock).

Return type:

list[str]

repomatic.uv.uvx_cmd()[source]

Build a uvx command prefix with standard flags.

Return type:

list[str]

repomatic.uv.GITHUB_API_RELEASE_BY_TAG_URL = 'https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}'

GitHub API URL for fetching a single release by tag name.

repomatic.uv.RELEASE_NOTES_MAX_LENGTH = 2000

Maximum characters per package release body before truncation.

class repomatic.uv.VulnerablePackage(name, current_version, advisory_id, advisory_title, fixed_version, advisory_url)[source]

Bases: object

A single vulnerability advisory for a Python package.

name: str

Package name.

current_version: str

Currently resolved version.

advisory_id: str

Advisory identifier (e.g., GHSA-xxxx-xxxx-xxxx).

advisory_title: str

Short description of the vulnerability.

fixed_version: str

Version that contains the fix, or empty string if unknown.

advisory_url: str

URL to the advisory details.

repomatic.uv.parse_uv_audit_output(output)[source]

Parse the text output of uv audit into structured vulnerability data.

Handles multiple advisories per package and packages without a known fix version. Unrecognized lines are silently skipped.

Parameters:

output (str) – Combined stdout/stderr from uv audit.

Return type:

list[VulnerablePackage]

Returns:

A list of VulnerablePackage entries.

repomatic.uv.format_vulnerability_table(vulns)[source]

Format vulnerability data as a markdown table.

Parameters:

vulns (list[VulnerablePackage]) – List of VulnerablePackage entries.

Return type:

str

Returns:

A markdown string with a ### Vulnerabilities heading and table, or an empty string if no vulnerabilities are provided.

repomatic.uv.is_lock_diff_only_timestamp_noise(lock_path)[source]

Check whether the only changes in a lock file are timestamp noise.

Note

This is a workaround for uv writing a new resolved timestamp on every uv lock run even when no packages changed. See uv#18155.

Runs git diff on the given path and inspects every added/removed content line. Returns True only when all changed lines match the exclude-newer-package timestamp pattern (timestamp = / span =).

Parameters:

lock_path (Path) – Path to the lock file to inspect.

Return type:

bool

Returns:

True if the diff contains only timestamp noise, False if there are no changes or any real dependency change is present.

repomatic.uv.revert_lock_if_noise(lock_path)[source]

Revert a lock file if its only changes are timestamp noise.

Calls is_lock_diff_only_timestamp_noise() and, if True, runs git checkout to discard the noise changes.

Note

In Renovate’s postUpgradeTasks context, the revert is ineffective because Renovate captures file content after its own uv lock --upgrade manager step before postUpgradeTasks run, and commits its cached content regardless of working tree changes.

Parameters:

lock_path (Path) – Path to the lock file to inspect and potentially revert.

Return type:

bool

Returns:

True if the file was reverted, False otherwise.

repomatic.uv.add_exclude_newer_packages(pyproject_path, packages)[source]

Add packages to [tool.uv].exclude-newer-package in pyproject.toml.

Persists "0 day" exemptions for the given packages so that subsequent uv lock --upgrade runs (e.g. from the sync-uv-lock job) do not downgrade security-fixed packages back to versions within the exclude-newer cooldown window.

Skips packages that already have an entry. Returns True if the file was modified.

Parameters:
  • pyproject_path (Path) – Path to the pyproject.toml file.

  • packages (set[str]) – Package names to add.

Return type:

bool

Returns:

True if the file was updated, False if no changes were needed.

repomatic.uv.prune_stale_exclude_newer_packages(pyproject_path, lock_path)[source]

Remove stale entries from [tool.uv].exclude-newer-package.

Note

This is a workaround until uv supports native pruning. See uv#18792.

An entry is stale when its locked version’s upload time falls before the exclude-newer cutoff, meaning uv lock --upgrade would resolve to the same (or newer) version without the "0 day" override.

Packages without an upload time in the lock file (git or path sources) are treated as permanent exemptions and never pruned.

Parameters:
  • pyproject_path (Path) – Path to the pyproject.toml file.

  • lock_path (Path) – Path to the uv.lock file.

Return type:

bool

Returns:

True if the file was modified, False otherwise.

repomatic.uv.parse_lock_versions(lock_path)[source]

Parse a uv.lock file and return a mapping of package names to versions.

Parameters:

lock_path (Path) – Path to the uv.lock file.

Return type:

dict[str, str]

Returns:

A dict mapping normalized package names to their version strings.

repomatic.uv.parse_lock_upload_times(lock_path)[source]

Parse a uv.lock file and return a mapping of package names to upload times.

Extracts the upload-time field from each package’s sdist entry.

Parameters:

lock_path (Path) – Path to the uv.lock file.

Return type:

dict[str, str]

Returns:

A dict mapping normalized package names to ISO 8601 upload-time strings. Packages without an sdist or upload-time are omitted.

repomatic.uv.parse_lock_exclude_newer(lock_path)[source]

Parse the exclude-newer timestamp from a uv.lock file.

Parameters:

lock_path (Path) – Path to the uv.lock file.

Return type:

str

Returns:

The exclude-newer ISO 8601 datetime string, or an empty string if not present.

repomatic.uv.load_lock_data(lock_path=None)[source]

Load and parse a uv.lock file.

Parameters:

lock_path (Path | None) – Path to uv.lock file. If None, looks in current directory.

Return type:

dict[str, Any]

Returns:

Parsed TOML data as a dict, or empty dict if the file does not exist.

class repomatic.uv.LockSpecifiers(by_package, by_subgraph)[source]

Bases: object

Dependency specifiers extracted from a uv.lock file.

Two views of the same data, built in a single pass over the lock packages:

by_package

{package_name: {dep_name: specifier}}. Every dependency declared by a package (main and dev) keyed by the declaring package name. Used for edge labels in dependency graphs.

by_subgraph

{subgraph_name: {dep_name: specifier}}. Primary dependencies keyed by dev-group name or extra name. Used for node labels inside subgraphs.

by_package: dict[str, dict[str, str]]
by_subgraph: dict[str, dict[str, str]]
repomatic.uv.parse_lock_specifiers(lock_path=None, *, lock_data=None)[source]

Parse uv.lock and extract dependency specifiers.

A single pass builds two complementary indexes from [package.metadata].requires-dist and [package.metadata.requires-dev]. See LockSpecifiers for the two views returned.

Parameters:
  • lock_path (Path | None) – Path to uv.lock file. If None, looks in current directory. Ignored when lock_data is provided.

  • lock_data (dict[str, Any] | None) – Pre-loaded lock data from load_lock_data(). When provided, skips file I/O.

Return type:

LockSpecifiers

repomatic.uv.diff_lock_versions(before, after)[source]

Compare two version mappings and return the list of changes.

Parameters:
  • before (dict[str, str]) – Package versions before the upgrade.

  • after (dict[str, str]) – Package versions after the upgrade.

Return type:

list[tuple[str, str, str]]

Returns:

A sorted list of (name, old_version, new_version) tuples. old_version is empty for added packages; new_version is empty for removed packages.

repomatic.uv.format_diff_table(changes, upload_times=None, exclude_newer='', comparison_urls=None)[source]

Format version changes as a markdown table with heading.

When upload_times is provided, a “Released” column is added so reviewers can visually verify that all updated packages respect the exclude-newer cutoff. The cutoff itself is shown above the table when exclude_newer is non-empty.

Parameters:
Return type:

str

Returns:

A markdown string with a ### Updated packages heading and table, or an empty string if there are no changes.

repomatic.uv.get_github_release_body(repo_url, version)[source]

Fetch the release notes body for a specific version from GitHub.

Tries v{version} first (most common for Python packages), then the bare {version} tag.

Parameters:
  • repo_url (str) – GitHub repository URL.

  • version (str) – The version string (e.g., 7.13.5).

Return type:

tuple[str, str]

Returns:

A tuple of (tag, body) where tag is the matched tag name and body is the release notes markdown. Both are empty strings if no release is found.

repomatic.uv.fetch_release_notes(changes)[source]

Fetch release notes for all updated packages.

For each package with a new version, discovers the GitHub repository via PyPI and fetches the release notes from GitHub Releases for all versions in the range (old, new]. Falls back to a changelog link from PyPI project_urls when no GitHub Release exists.

Parameters:

changes (list[tuple[str, str, str]]) – List of (name, old_version, new_version) tuples.

Return type:

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

Returns:

A dict mapping package names to (repo_url, versions) tuples where versions is a list of (tag, body) pairs sorted ascending. Only packages with at least one non-empty body are included. When a changelog URL is used as fallback, tag is empty and body contains a markdown link.

repomatic.uv.format_release_notes(notes)[source]

Render release notes as collapsible <details> blocks.

Follows Renovate’s visual pattern: a “Release notes” heading with one collapsible section per package. Long release bodies are truncated to RELEASE_NOTES_MAX_LENGTH characters with a link to the full release.

Parameters:

notes (dict[str, tuple[str, list[tuple[str, str]]]]) – A dict mapping package names to (repo_url, versions) tuples where versions is a list of (tag, body) pairs, as returned by fetch_release_notes().

Return type:

str

Returns:

A markdown string with the release notes section, or an empty string if no notes are available.

repomatic.uv.build_comparison_urls(changes, notes)[source]

Build GitHub comparison URLs from version changes and release notes.

Uses the tag format discovered by fetch_release_notes() to construct comparison URLs. Only packages with both old and new versions and a known GitHub repository are included.

Parameters:
Return type:

dict[str, str]

Returns:

Dict mapping package names to GitHub comparison URLs.

repomatic.uv.fix_vulnerable_deps(lock_path)[source]

Detect vulnerable packages and upgrade them in the lock file.

Runs uv audit to detect vulnerabilities, then upgrades each fixable package with uv lock --upgrade-package using --exclude-newer-package to bypass the exclude-newer cooldown for security fixes. Also persists the exemptions in pyproject.toml so that subsequent uv lock --upgrade runs (e.g. from the sync-uv-lock job) do not downgrade the fixed packages back within the cooldown window.

Parameters:

lock_path (Path) – Path to the uv.lock file.

Return type:

tuple[bool, str]

Returns:

A tuple of (has_fixes, diff_table). has_fixes is True when at least one vulnerable package was upgraded. diff_table is a markdown-formatted string with vulnerability details and version changes, or an empty string if no fixable vulnerabilities were found.

class repomatic.uv.SyncResult(reverted, changes, upload_times, exclude_newer)[source]

Bases: object

Result of a sync-uv-lock operation.

reverted: bool

Whether uv.lock was reverted (only timestamp noise changed).

changes: list[tuple[str, str, str]]

Version changes as (name, old_version, new_version) tuples.

upload_times: dict[str, str]

Package name to ISO 8601 upload-time mapping from the lock file.

exclude_newer: str

The exclude-newer cutoff from the lock file, or empty string.

repomatic.uv.sync_uv_lock(lock_path)[source]

Re-lock with --upgrade and revert if only timestamp noise changed.

First prunes stale exclude-newer-package entries from pyproject.toml (entries whose locked version was uploaded before the exclude-newer cutoff), then runs uv lock --upgrade to update transitive dependencies. If the resulting diff contains only timestamp noise, reverts uv.lock so no spurious changes are committed.

Parameters:

lock_path (Path) – Path to the uv.lock file.

Return type:

SyncResult

Returns:

A SyncResult with structured version change data.

repomatic.virustotal module

Upload release binaries to VirusTotal and update GitHub release notes.

Submits compiled binaries (.bin, .exe) to the VirusTotal API for malware scanning, then appends analysis links to the GitHub release body so users can verify scan results.

Supports two-phase operation: phase 1 uploads files and writes an initial table with scan links, phase 2 polls for analysis completion and replaces the table with detection statistics.

Note

The free-tier API allows 4 requests per minute. All API calls (uploads and polls) are rate-limited with a sleep between each request.

repomatic.virustotal.VIRUSTOTAL_GUI_URL = 'https://www.virustotal.com/gui/file/{sha256}'

URL template for the VirusTotal file analysis page.

repomatic.virustotal.VIRUSTOTAL_SECTION_HEADER = '### 🛡️ VirusTotal scans'

Markdown header identifying the VirusTotal section in release bodies.

Used for idempotency: if this header is already present, the release body is not modified (unless replace=True is passed to update_release_body).

repomatic.virustotal.DETECTION_PENDING = '*pending*'

Placeholder text for the Detections column when analysis is not yet complete.

class repomatic.virustotal.DetectionStats(malicious, suspicious, undetected, harmless)[source]

Bases: object

Detection statistics from a completed VirusTotal analysis.

Stores only the four categories that constitute a definitive verdict. type-unsupported, timeout, and failure from the API response are excluded from the total.

malicious: int

Number of engines that flagged the file as malicious.

suspicious: int

Number of engines that flagged the file as suspicious.

undetected: int

Number of engines that found no threat.

harmless: int

Number of engines that classified the file as harmless.

property flagged: int

Total engines that flagged the file (malicious + suspicious).

property total: int

Total engines that produced a definitive verdict.

class repomatic.virustotal.ScanResult(filename, sha256, analysis_url, detection_stats=None)[source]

Bases: object

Result of uploading a single file to VirusTotal.

filename: str

Original filename of the uploaded binary.

sha256: str

SHA-256 hash of the file content.

analysis_url: str

VirusTotal web GUI URL for the file analysis.

detection_stats: DetectionStats | None = None

Detection statistics, or None if analysis is still pending.

repomatic.virustotal.scan_files(api_key, file_paths, rate_limit=4)[source]

Upload files to VirusTotal and return scan results.

Uses the synchronous vt.Client API. Sleeps between uploads to respect the free-tier rate limit.

Parameters:
  • api_key (str) – VirusTotal API key.

  • file_paths (list[Path]) – Paths to binary files to upload.

  • rate_limit (int) – Maximum requests per minute (free tier: 4).

Return type:

list[ScanResult]

Returns:

List of scan results with analysis URLs.

repomatic.virustotal.poll_detection_stats(api_key, results, rate_limit=4, timeout=600)[source]

Poll VirusTotal for detection statistics of previously uploaded files.

Queries GET /files/{sha256} for each file until analysis completes or the timeout is reached. Respects the free-tier rate limit for all API calls.

Parameters:
  • api_key (str) – VirusTotal API key.

  • results (list[ScanResult]) – Scan results from a previous upload.

  • rate_limit (int) – Maximum API requests per minute (shared with uploads).

  • timeout (int) – Maximum seconds to wait for all analyses to complete.

Return type:

list[ScanResult]

Returns:

Results with detection_stats populated (or None for files whose analysis did not complete before the timeout).

repomatic.virustotal.format_virustotal_section(results, repo='', tag='')[source]

Format scan results as a markdown section for a GitHub release body.

When any result has detection_stats, a three-column table is rendered with a Detections column. Otherwise the simpler two-column format is used.

Parameters:
  • results (list[ScanResult]) – Scan results to format.

  • repo (str) – Repository in owner/repo format. When provided along with tag, binary names are linked to their GitHub release download URLs.

  • tag (str) – Release tag (e.g., v6.11.1).

Return type:

str

Returns:

Markdown string, or empty string if no results.

repomatic.virustotal.update_release_body(repo, tag, results, replace=False)[source]

Append or replace VirusTotal scan links in a GitHub release body.

Parameters:
  • repo (str) – Repository in owner/repo format.

  • tag (str) – Release tag (e.g., v6.11.1).

  • results (list[ScanResult]) – Scan results to write.

  • replace (bool) – When True, replace an existing VirusTotal section instead of skipping. When False (default), skip if the section is already present.

Return type:

bool

Returns:

True if the body was updated, False if skipped.