repomatic.metadata.core 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).

Output shapes

Every key is printed to the environment file as one key=value line. Values take three shapes:

is_python_project=true
doc_files="changelog.md" "readme.md" "docs/license.md"
new_commits_matrix={"commit": ["346ce66…", "6f27db4…"], "include": [{"commit": "346ce66…", "short_sha": "346ce66"}]}

A scalar prints bare. A list prints as space-joined, individually quoted items, not a JSON array: workflow if: conditions test membership with a padded contains() against that string. A matrix prints as inlined JSON for fromJSON() to parse into a job matrix. See Metadata.format_github_value() for the encoding, and Dialect for the other output formats.

The full key inventory is generated from this module rather than listed here, so it cannot go stale: run repomatic metadata --list-keys, or read the rendered table in the workflows documentation.

repomatic.metadata.core.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.

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

Bases: StrEnum

Output dialect for metadata serialization.

github = 'github'
github_json = 'github-json'
json = 'json'
serialize(metadata)[source]

Render metadata in this dialect.

Parameters:

metadata (dict[str, Any]) – Raw key-to-value mapping from Metadata.dump().

Return type:

str

Returns:

The serialized payload.

repomatic.metadata.core.METADATA_KEYS_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Key', 'key'), ('Description', 'description'))

Column definitions for the metadata keys reference table.

repomatic.metadata.core.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. Rows are unsorted: sorting is handled by the CLI’s SortByOption.

Return type:

list[tuple[str, str]]

repomatic.metadata.core.all_metadata_keys()[source]

Returns the set of all valid metadata key names.

Return type:

frozenset[str]

repomatic.metadata.core.METADATA_VALUE_OPTIONS: frozenset[str] = frozenset({'--format', '--output', '--sort-by', '-o'})

Options on the metadata command consuming the token that follows them.

Needed by repomatic.lint_repo.check_metadata_keys() to tell a positional key from an option’s value while reading a workflow’s run: line. The command itself is not importable from there: repomatic.cli.main reads sys.stdout.name at import time, so importing it under a test that has replaced stdout raises.

Listed here rather than derived, and pinned against the real command by repomatic’s own test suite, so an option added later cannot quietly turn its value into a token the lint reports as an unknown key.

class repomatic.metadata.core.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.core.Metadata[source]

Bases: EnvironmentMetadata, GitMetadata, MatrixMetadata, ProjectMetadata

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.

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: Path = PosixPath('pyproject.toml')
sphinx_conf_path: 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.

Delegates to repomatic.github.actions.get_github_event(), the one loader of the payload: the two used to parse the file independently and disagree on whether a missing file was fatal. The tolerant contract wins (an unreadable payload degrades every consumer to its no-event behavior); only the CI-context warning lives here, since the shared loader serves non-CI callers too.

property mailmap_exists: bool[source]
property files: FileInventory[source]

What this repository holds on disk, .gitignore applied.

The inventory is its own concern (repomatic.file_inventory): answering “which Markdown files are there” needs no CI context, no git history and no pyproject.toml. The group properties below forward to it so every metadata output key keeps its name; a caller wanting the argument-taking lookups goes to the inventory itself.

property gitignore_exists: bool

Whether a .gitignore file is present.

property python_files: list[Path]

Python sources, notebooks included.

property json_files: list[Path]

JSON files Biome can format.

property yaml_files: list[Path]

YAML files.

property pyproject_files: list[Path]

Every pyproject.toml in the tree.

property workflow_files: list[Path]

GitHub workflow definitions.

property doc_files: list[Path]

Documentation sources.

property markdown_files: list[Path]

Markdown files.

property image_files: list[Path]

Images the optimizer can losslessly shrink.

property shfmt_files: list[Path]

Shell scripts shfmt formats.

property zsh_files: list[Path]

Zsh scripts, by extension or shebang.

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.

The engine’s create-release job bakes this body into the GitHub release at draft-creation time, so the admonition is present from the start. Doing it there (rather than editing the release from the caller’s fast publish-pypi lane) removes the cross-lane race where the edit ran before create-release had created the release, and so silently dropped the admonition under continue-on-error. The bake is optimistic: it assumes the parallel PyPI upload succeeds, which it does on the normal path; a failed upload surfaces as a red publish-pypi job, not as a wrong admonition the user must catch.

Returns None when the project is not on PyPI, has no changelog, or has no version to release, in which case create-release falls back to the plain release_notes.

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

Todo

Widen the JSON branch beyond an iterable of dict[str, str], the only shape it asserts on today, when a metadata key needs a richer one.

Return type:

str

dump_factories()[source]

Lazy value factories for every metadata key, in output order.

Each value is computed only when its key is included, so keys=("is_python_project",) skips nuitka_matrix and the git history walk it pulls in.

Split out of dump() so the key inventory is inspectable without computing anything: tests/test_metadata.py asserts these names match _METADATA_KEY_DESCRIPTIONS plus _metadata_config_fields(), which is what keeps --list-keys, all_metadata_keys() and the emitted output from drifting apart.

Derived from _METADATA_KEY_DESCRIPTIONS rather than re-listing every key: most keys read the attribute of the same name, so only the handful whose value is not a plain attribute carry an explicit factory.

Return type:

dict[str, Callable[[], Any]]

Returns:

Key name to a zero-argument callable producing its value.

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 computed and included in the output. Filtered-out keys are never accessed, so callers requesting a small subset avoid triggering expensive dependent computations (git history walks, file system scans, build matrix expansion). See dump_factories().

Return type:

str