repomatic.release package¶
The release lane.
Release preparation and version sync, binary building and verification, the binaries page, checksums, attestation, and the VirusTotal scan history.
Submodules¶
repomatic.release.attestation module¶
Naming and packing of the sigstore bundles attached to a release.
actions/attest writes every bundle to the same attestation.json basename,
whatever it signed, so each release job has to rename its own before the files
land in one directory. Three of them did, three different ways: the compiled
binaries appended the suffix to the full filename, the man-page tarball dropped
its .tar.gz first, and the consumer-declared extra assets were named after the
job rather than any file. A release page therefore carried
repomatic-manpages.attestation.json next to repomatic-manpages.tar.gz, and
repomatic-extra-assets.attestation.json next to repomatic-claude-plugin.zip.
This module holds the one rule instead: a bundle is named after the artifact it
attests. The subject list is read back out of the bundle rather than passed in,
so the name is derived from what was actually signed and no caller can spell it
differently. See bundle_filename() for the multi-subject case.
Note
The signing itself stays in actions/attest: it needs the job’s OIDC token, so
it cannot move here. This module runs immediately after it, in the same job.
- repomatic.release.attestation.ATTESTATION_SUFFIX: Final[str] = '.attestation.json'¶
Extension carried by every attestation bundle attached to a release.
Not
.sigstore.json(the ecosystem’s own convention) because these files have been published under this name since the first attested release, and a release asset name is part of the surface users script against.
- repomatic.release.attestation.bundle_subjects(bundle_path)[source]¶
Filenames of the artifacts a sigstore bundle attests.
A bundle wraps a DSSE envelope whose base64 payload is an in-toto Statement, and that statement’s
subjectarray names every file signed in the same call. One entry for a singlesubject-path, several whenactions/attestwas handed a glob: “If multiple subjects are being attested at the same time, a single attestation will be created with references to each of the supplied subjects.”- Parameters:
bundle_path (
Path) – The bundleactions/attestwrote.- Return type:
- Returns:
Subject filenames, in the order the statement lists them.
- Raises:
ValueError – If the file is not a bundle carrying a readable in-toto statement, or names a subject that is not a bare filename.
- repomatic.release.attestation.bundle_filename(subjects, set_name=None)[source]¶
Name a bundle after the artifact it attests.
A single subject gives the bundle its own name, suffix appended to the whole filename so the two sort together on a release page listing assets alphabetically (
papaya.tar.gz, thenpapaya.tar.gz.attestation.json).Several subjects have no such name to borrow, since one bundle covers them all, so set_name is required and should describe the set rather than any member of it. That case only arises when a job hands
actions/attesta glob.- Parameters:
subjects (
Sequence[str]) – Subject filenames, frombundle_subjects().set_name (
str|None) – Stem to use when subjects holds more than one entry.
- Return type:
- Returns:
The bundle’s filename.
- Raises:
ValueError – If subjects is empty, or holds several entries with no set_name to fall back on.
- repomatic.release.attestation.pack_attestation(bundle_path, asset_dir, set_name=None)[source]¶
Name a bundle after its subject and print what to upload with it.
Copies bundle_path into asset_dir under the name
bundle_filename()derives, then returns that bundle alongside every artifact it attests, which is exactly the file list the release upload step has to attach for the provenance to be verifiable offline.Every subject must already sit in asset_dir: a bundle naming a file that is not there means the job attested a different tree than the one it is about to upload, which would publish an asset whose sidecar covers something else. Immutable releases make that unfixable after the fact, so it fails here instead.
Idempotent: re-running copies the same bytes over the same name.
- Parameters:
bundle_path (
Path) – The bundleactions/attestwrote.asset_dir (
Path) – Directory holding the attested artifacts.set_name (
str|None) – Stem for the multi-subject case, seebundle_filename().
- Return type:
- Returns:
Sorted paths to upload, the renamed bundle included.
- Raises:
ValueError – If a subject is missing from asset_dir.
repomatic.release.binaries_page module¶
Generate the binaries catalog: a CSV data file and its docs/binaries.md page.
The catalog inventories every compiled binary the repository ever released, one CSV row per binary: version (linking to the GitHub release), platform target (linking to the direct download), release date, and the VirusTotal detection snapshot (linking to the live analysis). It gives alpha and beta testers a single place to grab binaries from, and the maintainer an overview of how antivirus engines treat each release.
The data lives in docs/assets/binaries.csv, regenerated wholesale on every
release from the GitHub Releases API (the single source of truth for
published assets) and the JSON scan history maintained by scan-virustotal.
The Markdown page renders it through a single csv-table directive and is
otherwise static: it is created once from PAGE_TEMPLATE and only its
marker-delimited region (the detection trend chart) is rewritten afterwards,
so the intro and section prose stay hand-editable per repository.
Note
On the documentation site, the table is searchable and sortable client-side
via the sphinx-datatables extension, which activates on the
sphinx-datatable CSS class. The extension is optional: without it the
csv-table directive still renders a plain table, and on GitHub the CSV
file itself gets the built-in searchable grid viewer.
Note
Development builds are only linked, not cataloged: the rolling dev pre-release is refreshed on every push to the default branch, so any row frozen into the CSV would be stale within hours, while the workflow run artifacts behind the link always are the current builds.
- repomatic.release.binaries_page.CHART_JS_URL = 'https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js'¶
Pinned CDN artifact drawing the detections trend chart.
The one external artifact this module publishes into every downstream repository’s docs, so it carries a checksum beside the pin: bump the version by hand together with
CHART_JS_SRI.
- repomatic.release.binaries_page.CHART_JS_SRI = 'sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y'¶
Subresource Integrity digest of
CHART_JS_URL.The browser refuses the script if the CDN bytes stop matching. Recompute on every version bump as the sha384 of the exact artifact, verified against the same file inside the npm registry tarball before trusting the CDN copy:
hashlib.sha384(artifact_bytes)then base64.
- repomatic.release.binaries_page.CSV_HEADERS = ('Version', 'Platform', 'Released', 'VirusTotal')¶
Column headers of the binaries CSV.
Deliberately compact: the version cell carries the link to the GitHub release, the platform cell the direct binary download, and the VirusTotal cell the analysis link, so no column holds a bare URL, filename, or 64-character checksum.
- repomatic.release.binaries_page.FLAGGED_DANGER_PCT = 10¶
Flagged-verdict share (percent) at which the catalog shield turns red.
Below it, a flagged binary is the routine Nuitka false-positive tail worth a warning tint; from one engine in ten upward, the release deserves a false-positive submission round (see the
/av-false-positiveskill).
- repomatic.release.binaries_page.LEGACY_PAGE_END_MARKER = '<!-- binaries-end -->'¶
Oldest closing marker, migrated to
PAGE_END_MARKERon first touch.
- repomatic.release.binaries_page.LEGACY_PAGE_START_MARKERS = ('<!-- binaries-start -->', '<!-- binaries-chart-start -->')¶
Superseded opening markers, migrated to
PAGE_START_MARKERon first touch.Two generations precede the current bare open: the original
<!-- binaries-start -->, then the<!-- binaries-chart-start -->of the short-lived-start/-endpair. Both collapse toPAGE_START_MARKER, so a page written by any past version refreshes cleanly.
- repomatic.release.binaries_page.PAGE_REGION = 'binaries-chart'¶
Region name spliced by
click_extra.blocks.replace_region().The generated chart lives between the
<!-- binaries-chart -->and<!-- binaries-chart-end -->markers thatPAGE_START_MARKERandPAGE_END_MARKERspell out, following click-extra’s<!-- name --> / <!-- name-end -->marker grammar withname= this value.
- repomatic.release.binaries_page.PAGE_END_MARKER = '<!-- binaries-chart-end -->'¶
Closing marker of the generated chart region in the binaries page.
- repomatic.release.binaries_page.PAGE_START_MARKER = '<!-- binaries-chart -->'¶
Opening marker of the generated chart region in the binaries page.
- repomatic.release.binaries_page.PAGE_TEMPLATE¶
Initial page content, used when the page does not exist yet.
The
{repo_url}placeholder is substituted withstr.replace(notstr.format, which would choke on the csv-table directive’s braces). Everything outside the marker pair is written once and never touched again: repositories can reword the prose without fighting the generator.
- repomatic.release.binaries_page.render_chart_section(records)[source]¶
Render the detection trend across releases as a Chart.js timeline.
Plots the share of antivirus engine verdicts flagging each release’s binaries (all platforms aggregated), using the at-release snapshot of every file, on a true time axis: spacing reflects the actual gaps between releases. Points reuse the catalog shields’ color language, read at view time from sphinx-design’s CSS variables so they match the theme exactly (with hardcoded fallbacks). The data is embedded in the page rather than fetched, so the chart also works on
file://previews; only the Chart.js bundle comes from its CDN, mirroring how the table’s DataTables assets load.- Return type:
- Returns:
A
## VirusTotal detectionssection with arawHTML fence, or an empty string when fewer than two releases have records (a one-point trend is not a trend).
- repomatic.release.binaries_page.render_binaries_csv(repo_slug, releases, records)[source]¶
Render the catalog data as CSV, one row per released binary.
Rows cover every published release carrying compiled binaries, ordered by descending version then filename. Cells hold Markdown links (parsed by MyST inside the
csv-tabledirective): the version to the GitHub release, the platform to the binary download, and the VirusTotal cell to the file’s analysis. The VirusTotal cell renders the at-release snapshot as a green check when no engine flags the binary, and as the flagged-verdict share (tinted byFLAGGED_DANGER_PCT) otherwise.Caution
Only a binary backed by a scan record gets a VirusTotal cell; every other row leaves it empty. A file page exists on VirusTotal solely because the file was submitted, while the catalog spans every release a repository ever published, including those predating
scan-virustotaland those whose upload failed. Deriving the URL from the GitHub asset digest alone therefore sent readers to a blank page for each binary nobody ever uploaded, which was most of the catalog on older projects.Caution
The version and platform cells decorate their links with sphinx-design’s
octiconrole, so the rendering repository needssphinx-designin its documentation build (already true across this ecosystem’s docs stacks).- Parameters:
repo_slug (
str) – Repository inowner/repoform.releases (
Sequence[ReleaseWithAssets]) – Releases fromrepomatic.github.releases.get_releases_with_assets().records (
Sequence[ScanRecord]) – Detection snapshots from the scan history file.
- Return type:
- Returns:
The full CSV content, header row included.
- repomatic.release.binaries_page.update_binaries_csv(csv_path, content)[source]¶
Write the catalog CSV, creating parent directories as needed.
- Parameters:
csv_path (
Path) – Path to the CSV file.content (
str) – Rendered CSV fromrender_binaries_csv().
- Return type:
- Returns:
Truewhen the file was created or its content changed.
- repomatic.release.binaries_page.update_binaries_page(page_path, chart_section, repo_slug)[source]¶
Create the binaries page if missing and refresh its chart region.
A missing page is created (with parent directories) from
PAGE_TEMPLATE. On an existing page only the region betweenPAGE_START_MARKERandPAGE_END_MARKERis replaced byclick_extra.blocks.replace_region(), leaving all surrounding prose untouched. Pages carrying anyLEGACY_PAGE_START_MARKERSopen or theLEGACY_PAGE_END_MARKERclose are migrated to the current markers in the same pass.- Parameters:
page_path (
Path) – Path to the Markdown page.chart_section (
str) – Rendered chart fromrender_chart_section(), or an empty string to leave the region empty.repo_slug (
str) – Repository inowner/repoform, interpolated into the template on first creation.
- Return type:
- Returns:
Truewhen the file was created or its content changed.- Raises:
ValueError – When the page exists but lacks the markers. Loud on purpose: a page not written by this generator must never be overwritten.
repomatic.release.binary module¶
Binary build targets and verification utilities.
Defines the Nuitka compilation targets for all supported platforms and provides native binary verification: architecture and minimum-OS floors are parsed straight from the executables’ ELF, Mach-O and PE headers, so no external tool is needed on runners or inside build containers.
- repomatic.release.binary.BINARY_ASSET_SUFFIXES = ('.bin', '.exe')¶
File extensions identifying compiled binaries among release assets.
The one definition of “a compiled release asset”:
scan-virustotaluploads this set, the release workflow downloads it (--patternflags in_release-engine.yaml),docs/binaries.mdlists it, and the dev-release asset globs derive from it.
- repomatic.release.binary.PYTHON_DIST_SUFFIXES = ('.tar.gz', '.whl')¶
File extensions identifying Python distributions among release assets.
The counterpart of
BINARY_ASSET_SUFFIXES, and the same kind of single definition:pack_binary_assets()excludes this set from the binary upload list (create-releasealready attached those), and the dev-release asset globs add it on top of the compiled binaries.
- repomatic.release.binary.MACHO_MAGIC_64: Final[int] = 4277009103¶
Magic of a 64-bit Mach-O header, in the file’s own (little) endianness.
- repomatic.release.binary.MACHO_FAT_MAGICS: Final[frozenset[int]] = frozenset({3405691582, 3405691583})¶
Big-endian magics of universal (fat) Mach-O containers, 32- and 64-bit.
- class repomatic.release.binary.BinaryFormat(label, extension, floor_label)[source]¶
Bases:
EnumExecutable container format a compiled binary uses.
Each member owns what varies by format: the name messages print, the file extension its executables take, what a target’s floor measures, and the header parser
read_info()dispatches to. The floor label doubles as the enforceability flag, since a format recording no measurable floor has nothing to label.Note
Members carry no magic bytes of their own:
detect()needs to read them in a fixed order (a fat Mach-O and a PE both fail an equality test on the first four bytes), so the probe stays one method rather than a table.- ELF = ('elf', 'bin', 'glibc')¶
- MACHO = ('macho', 'bin', 'macOS')¶
- PE = ('pe', 'exe', None)¶
- label¶
Lowercase format name, as printed in verification messages.
- extension¶
File extension executables of this format take.
- floor_label¶
What a target’s floor measures for this format, or
None.Nonemarks a format whose headers record nothing a scan can check, soBuildTarget.enforced_floorreports no floor to enforce. Only PE is in that case: its version headers are nominal, and the Windows floor is CPython’s own support policy, not a linker artifact.
- read_info(path)[source]¶
Parse a binary’s headers into its machine set and measured floor.
The machine element type follows the format (ELF machine names, Mach-O CPU type integers, the PE machine id or
Nonewhen unparsable), matching whatARCH_MACHINESrecords as the expectation. The floor is the measured requirementverify_binary_floor()compares against the declared one: alwaysNoneon PE, whose headers record nothing enforceable.
- repomatic.release.binary.PLATFORM_FORMATS: Final[dict[Platform | Group, BinaryFormat]] = {Group(id='linux', name='Linux distributions'): BinaryFormat.ELF, Platform(id='macos', name='macOS'): BinaryFormat.MACHO, Platform(id='windows', name='Windows'): BinaryFormat.PE}¶
Executable format each build platform compiles to.
Read through
BuildTarget.binary_format, which is how every caller reaches it.
- repomatic.release.binary.MACHINE_IDS: Final[dict[tuple[BinaryFormat, Architecture], str | int]] = {(BinaryFormat.ELF, Architecture(id='aarch64', name='ARM64 (AArch64)')): 'EM_AARCH64', (BinaryFormat.ELF, Architecture(id='x86_64', name='x86-64 (AMD64)')): 'EM_X86_64', (BinaryFormat.MACHO, Architecture(id='aarch64', name='ARM64 (AArch64)')): 16777228, (BinaryFormat.MACHO, Architecture(id='x86_64', name='x86-64 (AMD64)')): 16777223, (BinaryFormat.PE, Architecture(id='aarch64', name='ARM64 (AArch64)')): 43620, (BinaryFormat.PE, Architecture(id='x86_64', name='x86-64 (AMD64)')): 34404}¶
Machine identifier a binary’s header carries, per format and architecture.
One table rather than three, keyed the way
PlatformKeykeys the tool registry: the value is a pyelftools machine name on ELF and a raw header integer on Mach-O and PE, so a caller compares it against whatever the matching parser returns.
- class repomatic.release.binary.BuildTarget(id, runner, platform, arch, floor, container=None)[source]¶
Bases:
objectOne Nuitka compile target: a runner image, a platform and an architecture.
Carries the metadata every consumer branches on, and the methods that interpret it, so a caller asks the target what its binaries must look like instead of re-deriving it from a platform name.
Platform and architecture are Extra Platforms traits rather than free strings, which is the same vocabulary
PlatformKeykeys the downloaded-binary registry on.as_matrix_entry()renders both back to their ids for the workflow matrix.Every field is irreducible: anything a format decides (the file extension, what the floor measures, which header field to read) lives on
BinaryFormatinstead.- id: str¶
Short target identifier, chosen for user-friendliness.
It names the published release asset, so it must stay stable: download URLs,
docs/install.mdand thebinaries.csvcatalog all match on it.Note
It is deliberately not derived from
platformandarch, even though all six targets currently read{platform}-{short arch}. The asset names froze on the shortx64andarm64spellings while those fields carry the canonical extra-platforms ids, and a future target splitting an existing pair (a musl Linux, say) would need a name the derivation cannot produce.tests/test_binary.pypins the pairing.
- runner: str¶
Runner image, as named in GitHub-hosted runners.
Named for what it holds, not for the
osmatrix key it renders to:ubuntu-26.04-armis an image, andplatformis the operating system.Hint
One compile job per target, each on one of the six runners the test matrix already covers, so a published binary is built on an image the suite is validated against. The targets are exactly
KNOWN_RUNNERS, not a separate selection: an image is added here by widening the test axes, never on its own.
- platform: Platform | Group¶
Operating system the binary runs on.
- arch: Architecture¶
CPU architecture the binary is compiled for.
- floor: str¶
Oldest runtime the binary is built to run on.
What the version counts is the format’s business, named by
BinaryFormat.floor_label: a glibc symbol version on ELF, a macOS deployment target on Mach-O, a Windows release on PE. The first two are measured out of the compiled files byverify_binary_floor(); the third is documentation, since PE version headers record nothing to measure.On macOS the release workflow also exports it as
MACOSX_DEPLOYMENT_TARGETat compile time. Without it, compiled objects and processed dylibs inherit the build runner’s own macOS version.
- container: str | None = None¶
OCI image the Linux compile and self-test jobs run in.
Passed to the
container:key of the release workflow. Compiling insidemanylinux_2_28caps the toolchain at glibc 2.28, so binaries stop inheriting the floor of whatever glibc the current runner image ships.Declared per target rather than derived from
platform, which would assume every Linux target is a manylinux one. Absent on macOS and Windows: GitHub Actions containers do not exist for those runners.
- property binary_format: BinaryFormat¶
Executable format the compiler emits for this target.
- property expected_machine: str | int¶
Machine identifier this target’s binaries carry in their header.
- as_matrix_entry()[source]¶
Flat, JSON-safe mapping of this target, for GitHub matrix inclusion.
Renders
platformandarchback to their extra-platforms ids, the spelling the workflow expressions (matrix.platform_id) andtests.yaml’s runner check compare against, andrunnerback to theoskeyruns-on:reads. A target with no container omits the key, so an entry carries only what applies to it.
- repomatic.release.binary.NUITKA_BUILD_TARGETS: Final[dict[str, BuildTarget]] = {'linux-arm64': BuildTarget(id='linux-arm64', runner='ubuntu-26.04-arm', platform=Group(id='linux', name='Linux distributions'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='2.28', container='quay.io/pypa/manylinux_2_28_aarch64@sha256:e7035406e58d96b7407246af1f6514a3cbd753a0025b42b9adfbeadd3b29ba80'), 'linux-x64': BuildTarget(id='linux-x64', runner='ubuntu-26.04', platform=Group(id='linux', name='Linux distributions'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='2.28', container='quay.io/pypa/manylinux_2_28_x86_64@sha256:fdb9a9c223b215604dc7b6f7e8fff4b39bfea5fbaa7777a2e5544a60dfa437f8'), 'macos-arm64': BuildTarget(id='macos-arm64', runner='macos-26', platform=Platform(id='macos', name='macOS'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='11.0', container=None), 'macos-x64': BuildTarget(id='macos-x64', runner='macos-26-intel', platform=Platform(id='macos', name='macOS'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='10.15', container=None), 'windows-arm64': BuildTarget(id='windows-arm64', runner='windows-11-arm', platform=Platform(id='windows', name='Windows'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='11', container=None), 'windows-x64': BuildTarget(id='windows-x64', runner='windows-2025', platform=Platform(id='windows', name='Windows'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='10', container=None)}¶
GitHub-hosted runner matrix for Nuitka builds, keyed by target name.
The roster is closed: every entry compiles on a runner the test matrix already covers, and the key doubles as the compiled binary’s published identifier. See
BuildTargetfor what each field means and which of them are frozen.
- repomatic.release.binary.FLAT_BUILD_TARGETS: Final[list[dict[str, str]]] = [{'arch': 'aarch64', 'container': 'quay.io/pypa/manylinux_2_28_aarch64@sha256:e7035406e58d96b7407246af1f6514a3cbd753a0025b42b9adfbeadd3b29ba80', 'extension': 'bin', 'floor': '2.28', 'os': 'ubuntu-26.04-arm', 'platform_id': 'linux', 'target': 'linux-arm64'}, {'arch': 'x86_64', 'container': 'quay.io/pypa/manylinux_2_28_x86_64@sha256:fdb9a9c223b215604dc7b6f7e8fff4b39bfea5fbaa7777a2e5544a60dfa437f8', 'extension': 'bin', 'floor': '2.28', 'os': 'ubuntu-26.04', 'platform_id': 'linux', 'target': 'linux-x64'}, {'arch': 'aarch64', 'extension': 'bin', 'floor': '11.0', 'os': 'macos-26', 'platform_id': 'macos', 'target': 'macos-arm64'}, {'arch': 'x86_64', 'extension': 'bin', 'floor': '10.15', 'os': 'macos-26-intel', 'platform_id': 'macos', 'target': 'macos-x64'}, {'arch': 'aarch64', 'extension': 'exe', 'floor': '11', 'os': 'windows-11-arm', 'platform_id': 'windows', 'target': 'windows-arm64'}, {'arch': 'x86_64', 'extension': 'exe', 'floor': '10', 'os': 'windows-2025', 'platform_id': 'windows', 'target': 'windows-x64'}]¶
List of build targets in a flat format, suitable for matrix inclusion.
- repomatic.release.binary.binary_name(package, target, version=None)[source]¶
Compose a compiled binary’s release-asset filename.
The one definition of the naming convention:
{package}-{version}-{target}.{ext}for the versioned upload, and with no version the stable alias ({package}-{target}.{ext}) backing thereleases/latest/downloadURLs. The extension comes fromNUITKA_BUILD_TARGETS.- Return type:
- repomatic.release.binary.versionless_alias(filename, version)[source]¶
Map a versioned binary filename to its stable alias, or
None.Strips the
-{version}-segment (papaya-1.2.3-linux-arm64.binbecomespapaya-linux-arm64.bin). ReturnsNonefor filenames that carry no such segment or are not compiled binaries, so callers can filter and map in one pass.
- repomatic.release.binary.binary_filename_re(package)[source]¶
Match a package binary filename, versioned or versionless.
Captures
targetandext, both alternations derived fromNUITKA_BUILD_TARGETSso a new build target extends the pattern without anyone editing a regex. The release freeze rewrites both spellings onto the versioned form through this;tests/test_platform_keys.pypins the pattern against every target.
- repomatic.release.binary.pack_binary_assets(dist_dir, version)[source]¶
Pack a release’s upload list, materializing the versionless aliases.
Mirrors what the release engine’s upload step needs: every file in dist_dir except the Python distributions (
create-releasealready uploaded those), plus a byte-identical versionless alias copied beside each versioned binary so the stablereleases/latest/downloadURLs always resolve. Aliases share their sibling’s digest, which is what lets artifact attestations verify them unchanged and the binaries catalog collapse them (seebinaries_page._binary_assets).Idempotent: re-running overwrites the same aliases with the same bytes.
- repomatic.release.binary.BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = ('.github/workflows/_release-engine.yaml', '.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]inpyproject.toml) are added dynamically bybinary_affecting_paths.The release workflow entries cover both layouts: upstream keeps the
_release-engine.yamllane (which defines the Nuitka compile and binary self-test jobs) in-repo, while downstream repos call the engine cross-repo from their generatedrelease.yaml, so a pin bump there rightly triggers a rebuild.
- repomatic.release.binary.SKIP_BINARY_BUILD_BRANCHES: Final[frozenset[str]] = frozenset({'format-images', 'format-json', 'format-markdown', 'format-shell', 'sync-gitignore', 'sync-mailmap', 'update-dep-graph'})¶
Autofix branches whose changes cannot affect compiled binaries.
Members are PR branch names produced by autofix jobs that touch only repository housekeeping (
.mailmap,.gitignore, JSON, Markdown, images, shell scripts, dependency graph). The binary output is unchanged, soskip_binary_buildreturnsTruewhen the PR head branch matches a member, saving an expensive Nuitka compilation.Note
This set is intentionally disjoint from
repomatic.git_ops.VERSION_BUMP_BRANCHES: version-bump branches do change binaries (they rewrite the version string baked into the build), so they belong to a different policy.
- repomatic.release.binary.LC_VERSION_MIN_MACOSX: Final[int] = 36¶
Mach-O load command carrying the minimum macOS version (pre-10.14 SDKs).
- repomatic.release.binary.LC_BUILD_VERSION: Final[int] = 50¶
Mach-O load command carrying the platform and minimum OS (10.14+ SDKs).
- repomatic.release.binary.MACHO_PLATFORM_MACOS: Final[int] = 1¶
platformfield value naming macOS inside anLC_BUILD_VERSIONcommand.
- repomatic.release.binary.verify_binary_arch(target, binary_path)[source]¶
Verify that a binary matches the expected architecture for a target.
Parses the executable’s own headers, so it needs no external tool and behaves identically on runner VMs and inside build containers.
- Parameters:
- Raises:
ValueError – If target is unknown.
ValueError – If binary format or architecture does not match.
- Return type:
- repomatic.release.binary.verify_binary_floor(target, binary_path, dist_dirs=())[source]¶
Verify the binary and its dist tree stay within the target’s OS floor.
Scans the onefile binary itself plus every native library of the given Nuitka dist directories (whose content the onefile payload repacks), and compares each file’s measured requirement to the target’s declared floor:
What the floor counts comes from the format, per
BinaryFormat.floor_label:ELF: the highest
GLIBC_x.yversion requirement of each file. A higher requirement means a compiled object picked up symbols newer than the build container provides for, and the binary would die at load time on the distributions the floor promises.Mach-O: the
minosof each file, against the deployment target the build exports asMACOSX_DEPLOYMENT_TARGET.PE: nothing. Its version headers are nominal, so
BuildTarget.enforced_floorreports no floor and this returns early; the Windows floor is CPython’s own support policy, tracked in the docs.
- Parameters:
- Raises:
ValueError – If target is unknown.
ValueError – If any scanned file exceeds the declared floor.
- Return type:
repomatic.release.checksums module¶
Recompute SHA-256 checksums for the binary tool registry.
Iterates every TOOL_REGISTRY entry with a binary spec, downloads each
platform’s release artifact, and rewrites stale hashes in-place in
tool_registry.py (alongside the VERSIONS stamps). Driven by
repomatic update-checksums and, with a version override, by
sync-tool-versions so a version bump and its matching checksums land in one
pass.
- repomatic.release.checksums.update_registry_checksums(registry_path, version_overrides=None)[source]¶
Recompute binary checksums and version stamps in
tool_registry.py.Iterates every
TOOL_REGISTRYentry with abinaryspec, downloads each platform URL (concurrently, sized by the global--jobsoption and sequential atDEBUGverbosity or without an active CLI context), computes its SHA-256, and replaces stale hashes in-place. Also reconciles each tool’sVERSIONSstamp with the version the checksums were computed for, the basis of the offline staleness test.- Parameters:
registry_path (
Path) – Path totool_registry.py.version_overrides (
dict[str,str] |None) – Optional mapping of tool name to a version to download instead of the in-memoryToolSpec.version.sync-tool-versionspasses this so it can bump the version in the source and refresh the checksums in a single process: the in-memory registry still holds the pre-bump version because the file was edited, not reimported.
- Return type:
- Returns:
List of
(url, old_hash, new_hash)for each updated checksum. Empty if all checksums are already correct.
repomatic.release.prepare_release module¶
Prepare a release by updating changelog, citation, install guide, and workflow files.
A release cycle produces exactly two commits that must be merged via “Rebase and merge” (never squash):
Freeze commit (
[changelog] Release vX.Y.Z):Strips the
.dev0suffix from the version.Finalizes the changelog date and comparison URL.
Freezes workflow action references:
@main→@vX.Y.Z.Freezes CLI invocations:
uv run --frozen -- repomatic(from the lockfile) →uvx 'repomatic==X.Y.Z'(from PyPI, for downstream repos).Freezes the install guide’s binary download URLs to versioned release paths.
Pins the install guide’s versioned CLI examples to the release.
Sets the release date in
citation.cff.
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
.dev0suffix.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.
Caution
Rebase-merging the two commits delivers them in a single push, and GitHub
Actions reads workflow files from that push’s head: the unfreeze commit. So the
release lane of this repository always executes the unfrozen workflow
content, running LOCAL_CLI_INVOCATION against uv.lock, even while
building the freeze commit named in release_commits_matrix. Every job calling
the CLI therefore needs its own checkout of matrix.commit, and a job written
on the assumption that the frozen uvx 'repomatic==X.Y.Z' form is what runs
will fail with Failed to spawn: repomatic.
Only downstream repositories, which call the reusable workflow at its vX.Y.Z
tag, ever execute the frozen form. tests/test_workflows.py locks the checkout
requirement across every workflow.
Both operations are idempotent: re-running on an already-frozen or already-unfrozen tree is a no-op.
- repomatic.release.prepare_release.SELF_PIN_COOLDOWN_EXEMPTION = '--exclude-newer-package repomatic=P0D'¶
uv escape hatch letting a just-published repomatic install under the cooldown.
Every workflow exports a
UV_EXCLUDE_NEWERcovering all package resolution (seeclaude.md§ Cooldown on every install), and it applies to the frozen'repomatic==X.Y.Z'self-pin like any other requirement. That pin moves in lockstep with theuses:refs pointing at the same tag, so the version it names is always minutes old: without an exemption every downstream repo would fail to resolve it until the window elapsed. A zero-length window sets that one package’s cutoff to “now”, leaving the rest of the tree gated.uv exposes no environment variable for
--exclude-newer-package, so the exemption has to ride on the command line, which is why the freeze splices it in beside the pin instead of the workflows declaring it once.Note
A
uvxresolution reads no project configuration at all, so moving the exemption into[tool.uv]or an adjacentuv.tomlwould not work either: both are ignored. Seeclaude.md§ Per-ecosystem knobs.Todo
Declare the exemption once, instead of splicing it onto every frozen command line, as soon as uv grows a configuration or environment knob for
--exclude-newer-package: uv#20995.Spelled as the ISO 8601
P0Drather than the"0 day"used inpyproject.toml’sexclude-newer-packagetable: the flag travels through YAML folded scalars into a shell, where the space in0 daywould need quoting that survives both.P0Dneeds none.Caution
Every character here lands on 80-odd already-long workflow lines at freeze time.
tests/test_prepare_release.pysimulates a freeze and fails if the result breaches yamllint’s 120-column cap, so lengthening this string means reflowing the workflows that no longer fit.
- repomatic.release.prepare_release.LOCAL_CLI_INVOCATION = 'uv --no-progress run --frozen -- repomatic'¶
How every workflow on
mainruns the CLI, before the freeze rewrites it.Resolving from
uv.lockrather than from the index is what keeps the cooldown off the critical path here. A lockfile entry is pinned and hash-verified, so it is strictly stronger than a publication-age gate, and it cannot be made unsatisfiable by one:uvx --from .re-resolves[project.dependencies]on every call, and reads neitheruv.locknor[tool.uv] exclude-newer-package, so a floor naming a release younger than the window took every workflow down at once with nowhere to record the bypass.--frozenuses the lockfile as-is instead of asserting it is current, which is deliberate:--lockedwould fail every job the momentpyproject.tomldrifted ahead ofuv.lock, including thesync-uv-lockjob whose whole purpose is to close that gap.
- class repomatic.release.prepare_release.PrepareRelease(changelog_path=None, citation_path=None, workflow_dir=None, install_path=None, marketplace_path=None, default_branch='main')[source]¶
Bases:
objectPrepare files for a release by updating dates, URLs, and removing warnings.
- property current_version: str[source]¶
Extract current version from the bump-my-version config.
Delegates discovery to
Metadata.get_current_version(), which searches.bumpversion.tomlthenpyproject.toml.
- property package_name: str | None[source]¶
Canonical PyPI package name, used to spot pinned install examples.
Delegates discovery to
Metadata.package_name, which readspyproject.toml.
- set_citation_release_date()[source]¶
Update the
date-releasedfield in citation.cff.- Return type:
- Returns:
True if the file was modified.
- property composite_action_names: list[str][source]¶
Discover composite action directories under
.github/actions/.Enumerates every
.github/actions/*/action.yaml(or.yml) and returns the directory names. New composite actions automatically participate in freeze/unfreeze without requiring code changes here.- Returns:
Sorted list of composite action directory names.
- 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.
- Return type:
- Returns:
Number of files modified.
- freeze_install_download_urls(version)[source]¶
Replace binary download URLs in the install guide with versioned paths.
This is part of the freeze step: it freezes the install guide’s download links to a specific GitHub release so users get explicit, versioned URLs instead of the
/releases/latest/download/redirect. Both spellings resolve, since every release also carries versionless alias copies of its binaries (seepack_binary_assets()); the frozen URL is preferred because it names the version the reader is installing, and keeps working once a later release moveslatest.Handles two input forms:
Initial (never frozen):
/releases/latest/download/repomatic-linux-arm64.binPreviously 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.binNote
No unfreeze method is needed. Unlike workflow URLs (which toggle
@main↔@vX.Y.Z), download URLs ratchet forward: they always point to a specific release. After unfreeze, the install guide still shows the last release’s URLs, which is what users wanting stable binaries need.Caution
The freeze runs before the binaries exist, since it is the freeze commit that triggers the build. So it pins the version optimistically, and a release whose binary lane fails leaves the install guide linking six URLs that 404 until the next release ratchets past it. Re-point the guide at the last release that carries binaries when that happens, by calling this method with that version.
- freeze_marketplace_archive_url(version)[source]¶
Pin the plugin marketplace’s archive URL to this release.
This is part of the freeze step. The
archivesource in.claude-plugin/marketplace.jsonpoints at the release asset named byARCHIVE_NAME, and pinning the tag is what makes a marketplace ref meaningful: adding the catalog atkdeldycke/repomatic@v6.0.0then installs v6.0.0’s plugin, where alatestredirect would hand over whatever shipped most recently regardless of the ref asked for.Handles the same two input forms as
freeze_install_download_urls():Initial (never frozen):
/releases/latest/download/repomatic-claude-plugin.zipPreviously frozen:
/releases/download/v6.0.0/repomatic-claude-plugin.zip
Note
The trailing filename is rewritten too, not just the tag, so
ARCHIVE_NAMEis the single source of truth for the whole URL. Renaming the asset would otherwise leave the checked-in URL naming a file the next release no longer publishes, and the mismatch would only surface as a failed/plugin install. Rewriting both together also keeps the default branch installable across the rename: the URL still names the asset the last published release actually carries until this method flips tag and filename in the same commit.Note
No unfreeze method, for the same reason download URLs have none: the URL ratchets forward. The post-release
.devNbump leaves it alone, so the default branch keeps pointing at the newest published release rather than at avX.Y.Z.dev0tag that was never created. That is what makes every state of this file installable, which a bump-my-version entry rewriting it on both commits could not achieve.
- freeze_install_cli_version(version)[source]¶
Pin the install guide’s versioned CLI examples to the release.
This is part of the freeze step: the install guide’s Specific version``tab demonstrates a pinned invocation (``uvx {package}@X.Y.Z or a
{package}==X.Y.Zrequirement), which must always showcase the latest release. Without this pass the pinned example silently rots (click-extra’s install guide sat on a 14-releases-old pin).Note
Like
freeze_install_download_urls(), this ratchets forward with no unfreeze: after a release the examples keep demonstrating that release, which is what readers should copy until the next one ships.
- freeze_cli_version(version)[source]¶
Replace local source CLI invocations with a frozen PyPI version.
This is part of the freeze step: it freezes
repomaticinvocations 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
uv --no-progress run --frozen -- repomaticwithuvx --no-progress 'repomatic=={version}'in all workflow YAML files. Comment lines (starting with#) are skipped to avoid corrupting explanatory comments.The two halves are not symmetric by accident. On
mainthe CLI runs fromuv.lock, which is pinned and hash-verified, and which no cooldown can make unsatisfiable. A downstream repo has no such lockfile for this project, so its copy has to resolve the published package from the index, which is whatuvxdoes.The pin is spliced in behind
SELF_PIN_COOLDOWN_EXEMPTION, which is what keeps a release installable the minute it is published despite the workflow-wide cooldown. Local source needs no exemption, somaincarries none between releases.
- unfreeze_cli_version()[source]¶
Replace frozen PyPI CLI invocations with local source.
This is part of the unfreeze step: it reverts
repomaticinvocations back to local source (--from . repomatic) for the next development cycle onmain.Replaces
uvx --no-progress 'repomatic==X.Y.Z'withLOCAL_CLI_INVOCATION, takingSELF_PIN_COOLDOWN_EXEMPTIONwith it when the freeze put one there: the lockfile resolves from the working tree, so it never needs the escape hatch. The exemption is optional in the pattern so a workflow frozen by an older release still unfreezes cleanly. Comment lines are skipped (seefreeze_cli_version()).- Return type:
- 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, across the same reference set as
freeze_workflow_urls().- Return type:
- Returns:
Number of files modified.
- prepare_release(update_workflows=False)[source]¶
Run all freeze steps to prepare the release commit.
repomatic.release.version_sync module¶
Self-hosted dependency-version updaters: the replacement for Renovate.
Backs the sync-tool-versions, sync-action-pins, and sync-workflow-pins
commands. Each discovers the latest eligible upstream version from a datasource
(GitHub releases, PyPI, or npm), gated by the shared [tool.repomatic]
minimum-release-age cooldown (the GitHub/PyPI/npm counterpart to uv’s
exclude-newer, which guards sync-uv-lock), then rewrites the pinned version
in place.
The datasource adapters and version selection live here; the file I/O and
checksum recompute that the commands drive stay in repomatic.cli.main. The
string-level helpers (set_tool_version, find_action_pins,
find_workflow_literals, and the apply_* rewriters) are pure so they can be
unit-tested without network access.
- repomatic.release.version_sync.MINIMUM_RELEASE_AGE_URL = 'https://repomatic.net/configuration#minimum-release-age'¶
Docs anchor for the
minimum-release-agecooldown, linked from PR bodies.
- repomatic.release.version_sync.MIN_AGE_HELD_BACK_NOTE = 'Newer releases already published but withheld because they are still inside the [`minimum-release-age`](https://repomatic.net/configuration#minimum-release-age) cooldown window.'¶
Intro paragraph for the version-sync held-back section.
The GitHub/PyPI/npm counterpart to
repomatic.deps.dep_report.EXCLUDE_NEWER_HELD_BACK_NOTE.
- repomatic.release.version_sync.ACTION_PIN_RE = re.compile('(?P<prefix>uses:\\s*)(?P<slug>[\\w.-]+/[\\w.-]+)@(?P<sha>[0-9a-f]{40})(?P<gap>\\s*#\\s*)(?P<ref>v?\\d[\\w.-]*)')¶
Match a SHA-pinned GitHub Action
uses:reference with its version comment.The
slug/slug@<40-hex>shape only matchesowner/repoactions, so local./…refs and reusable-workflow refs carrying a subpath (owner/repo/.github/workflows/x.yaml@…) are skipped automatically.
- repomatic.release.version_sync.SETUP_UV_PACKAGE = 'uv'¶
PyPI project backing the
astral-sh/setup-uvversion pin.
- repomatic.release.version_sync.SETUP_UV_SLUG = 'astral-sh/setup-uv'¶
Action slug provisioning uv, whose own pin decides what CI can verify.
- repomatic.release.version_sync.SETUP_UV_CHECKSUMS_PATH = 'src/download/checksum/known-checksums.ts'¶
Path of the checksum table inside the
SETUP_UV_SLUGrepository.
- repomatic.release.version_sync.GITHUB_API_CONTENTS_URL = 'https://api.github.com/repos/{slug}/contents/{path}?ref={ref}'¶
GitHub API URL for reading one file at one commit.
Requested with the
rawmedia type, so the body arrives verbatim: the JSON form base64-encodes it and caps out at 1 MB, and the checksum table is already past half of that.
- class repomatic.release.version_sync.Candidate(version: str, date: str, ref: str)[source]¶
Bases:
NamedTupleA single release version offered by a datasource.
Create new instance of Candidate(version, date, ref)
- class repomatic.release.version_sync.ActionPin(slug: str, sha: str, ref: str)[source]¶
Bases:
NamedTupleA SHA-pinned GitHub Action reference found in a workflow file.
Create new instance of ActionPin(slug, sha, ref)
- class repomatic.release.version_sync.UpstreamRefPin(version: str, sha: str | None)[source]¶
Bases:
NamedTupleAn upstream thin-caller
uses:ref found in a workflow file.The counterpart of
ActionPinfor the upstream repo’s own reusable workflows and composite actions. Those refs carry a subpath (owner/repo/.github/workflows/x.yaml@…), whichACTION_PIN_REdeliberately does not match, so they need their own parser.Create new instance of UpstreamRefPin(version, sha)
- class repomatic.release.version_sync.WorkflowLiteral(ecosystem: str, package: str, version: str)[source]¶
Bases:
NamedTupleA version literal embedded in a workflow command.
Create new instance of WorkflowLiteral(ecosystem, package, version)
- repomatic.release.version_sync.parse_min_age(value)[source]¶
Parse a
minimum-release-agevalue into atimedelta.Accepts the friendly relative durations uv allows for
exclude-newer(8 days,2 weeks,36 hours). An unrecognized value logs a warning and yields no cooldown.
- repomatic.release.version_sync.min_release_age_days(value)[source]¶
Convert a
minimum-release-agevalue to whole days for npm’s cooldown.npm’s min-release-age resolver option (npm 11.10.0+) refuses any package version younger than the given number of days, across the whole resolved tree, transitive dependencies included. It is the runtime, transitive-tree counterpart to the pin cooldown
parse_min_age()feedssync-workflow-pins: the sameminimum-release-agewindow, enforced by npm at install time.Sub-day remainders round up, so a cooldown always over-protects rather than collapsing to
0, npm’s “no cooldown” sentinel.
- repomatic.release.version_sync.exclude_newer_cutoff(value, today)[source]¶
uv
--exclude-newercutoff date for aminimum-release-agevalue.uv’s cooldown knob is an absolute date, so the relative window is resolved live against today: packages uploaded on or after the returned date drop out of resolution. This gates ad-hoc
uvxtool installs (viarepomatic.tooling.tool_runner.run_tool()) by the same windowsync-workflow-pinsapplies to pins. The uv counterpart tomin_release_age_days()(npm).
- repomatic.release.version_sync.format_cooldown_note(age_label, cutoff)[source]¶
Render the
minimum-release-agecutoff sentence for a diff table.The version-sync counterpart to
repomatic.deps.dep_report.format_exclude_newer_note(). uv records an absoluteexclude-newertimestamp; here the cooldown is a relative span, so the effective cutoff istoday - min_age, recomputed each run rather than stored.- Parameters:
- Return type:
- Returns:
A one-line markdown note for
repomatic.deps.dep_report.format_diff_table().
- repomatic.release.version_sync.cleared_cooldown(released, cutoff)[source]¶
Whether a release dated released is safely older than cutoff.
The comparison is strict, and that one character is load-bearing. Datasources report a release date, while the cooldown this gates is enforced downstream at instant granularity: uv’s
--exclude-newercutoff isnow - min_age, carrying the run’s time of day. An inclusive<=therefore adopts a release published on the cutoff day but later in the day than the run’s own clock, which uv then refuses to resolve, pinning a version that cannot be installed until it ages out.That is not hypothetical: a
sync-workflow-pinsrun at05:20UTC adopted a release published at17:07on the cutoff date, and every binary build failed onNo solution founduntil the window elapsed.Being strict costs up to 24 hours of extra window and guarantees correctness, since a release dated before the cutoff day is older than any instant on it. Same “over-protect rather than under-protect” convention as
min_release_age_days().
- repomatic.release.version_sync.select_latest(candidates, min_age, today, *, allow_prerelease=False)[source]¶
Return the highest version old enough to clear the cooldown.
Candidates published more recently than min_age are held back, then the highest remaining PEP 440 version wins. Prereleases and versions that do not parse are skipped.
- Parameters:
- Return type:
- Returns:
The winning
Candidate, orNonewhen none qualify.
- repomatic.release.version_sync.select_held_back(candidates, pinned, min_age, today, *, allow_prerelease=False)[source]¶
Return the highest release withheld from pinned only by the cooldown.
The counterpart to
select_latest(): among candidates strictly newer than pinned, keep those still inside the cooldown window (published more recently than min_age) and return the highest. These are the releases a later run adopts once they age out, surfaced in the ## ⏸️ Held back by cooldown PR section. No extra network call is needed: the candidates are already in hand from theselect_latest()sweep.- Parameters:
candidates (
list[Candidate]) – Versions offered by a datasource.pinned (
str) – The version this run settled on; only strictly newer candidates can be held back.min_age (
timedelta) – The stabilization window fromminimum-release-age.today (
date) – Reference date for the cooldown computation.allow_prerelease (
bool) – Keep prerelease versions whenTrue.
- Return type:
- Returns:
The withheld
Candidate, orNonewhen nothing newer is inside the cooldown.
- repomatic.release.version_sync.pin_inside_cooldown(candidates, pinned, min_age, today)[source]¶
The release date of pinned when it has not yet cleared the cooldown.
Audits a pin already written to disk, where
select_latest()only ever judges one it is about to write. The two ask the same question throughcleared_cooldown(), so an audit can never disagree with the decision that produced the pin.Worth auditing separately because a pin can enter the tree without passing the selector at all: hand-edited, merged from a branch, restored from a revert, or written by an older release whose selector had a different boundary. Such a pin resolves through
uvxin CI, where no per-package exemption is reachable, so it fails the whole job until it ages out.- Parameters:
candidates (
list[Candidate]) – Versions offered by the datasource, as already fetched for the selection pass.pinned (
str) – The version currently written in the workflow.min_age (
timedelta) – The stabilization window fromminimum-release-age.today (
date) – Reference date for the cooldown computation.
- Return type:
- Returns:
The release date when pinned is still inside the window, or
Nonewhen it has cleared, is not among candidates, or carries an unparsable date.
- repomatic.release.version_sync.github_candidates(repo_url, tag_pattern=None)[source]¶
Collect release candidates from a GitHub repository.
- Parameters:
repo_url (
str) – The repository URL.tag_pattern (
str|None) – Per-tool version-extraction regex (seerepomatic.tooling.tool_registry.ToolSpec.tag_pattern).
- Return type:
- Returns:
One
Candidateper release whose tag yields a version. Empty when the API is unavailable (logged, never raised).
- repomatic.release.version_sync.pypi_candidates(package)[source]¶
Collect non-yanked release candidates from PyPI.
- repomatic.release.version_sync.npm_candidates(package)[source]¶
Collect release candidates from the npm registry.
- repomatic.release.version_sync.setup_uv_verified_versions(shas)[source]¶
uv releases every pinned
setup-uvcommit can checksum-verify.setup-uvverifies a download against a checksum table bundled into the action release. A version absent from that table is not refused: it is installed with no verification at all, on acore.debugline no CI log shows by default (src/download/checksum/checksum.ts). uv ships weekly andsetup-uvroughly monthly, andsync-action-pinsandsync-workflow-pinswalk the two pins independently, so the uv pin drifts past the table on its own. Measured on 2026-08-20:setup-uvv9.0.0stopped at uv0.11.30while every workflow here pinned0.12.3, five releases later.Intersecting rather than picking one table keeps a repository mid-bump honest: while
sync-action-pinshas landed on some files and not others, the only uv a whole fleet can verify is one both tables carry.- Parameters:
shas (
Iterable[str]) – Every distinctSETUP_UV_SLUGcommit pinned in the repository.- Return type:
- Returns:
The uv versions verifiable by all of them, or
Nonewhen no table could be read (no pin found, or every fetch failed), which leaves the caller ungated rather than blocked.
- repomatic.release.version_sync.set_tool_version(content, name, new_version)[source]¶
Rewrite a tool’s
version=field in thetool_registry.pysource.Targets the first
version="…"inside the namedToolSpec(entry, stopping at the next entry so a later tool is never touched.
- repomatic.release.version_sync.set_with_package_version(content, package, new_version)[source]¶
Rewrite a
with_packagespin in thetool_registry.pysource.Targets the
"{package}=={version}"literal wherever it appears, unlikeset_tool_version(), which is scoped to oneToolSpec(entry. Two tools pinning the same package therefore converge on one version rather than drifting apart, matching how_widest_changes()collapses a name pinned at several versions elsewhere.
- repomatic.release.version_sync.find_action_pins(content)[source]¶
Find every SHA-pinned GitHub Action reference in a workflow file.
- repomatic.release.version_sync.apply_action_pins(content, resolved)[source]¶
Rewrite SHA-pinned actions to their resolved SHA and version comment.
- Parameters:
- Return type:
- Returns:
The updated text and a list of
(slug, old_ref, new_ref)changes actually applied (entries whose SHA already matched are skipped).
- repomatic.release.version_sync.find_workflow_literals(content)[source]¶
Find npm and PyPI version literals embedded in a workflow file.
- Return type:
- repomatic.release.version_sync.self_pin_exemption_re(package: str) Pattern[str][source]¶
Match a
uvxcommand pinning package, capturing the flags before it.Memoized: the splice runs once per workflow file for the same package.
- repomatic.release.version_sync.frozen_cli_invocation(package, version, exemption)[source]¶
Render the frozen, cooldown-exempt
uvxinvocation of package.The one spelling both writers emit: the release freeze (
PrepareRelease.freeze_cli_version) writes it wholesale, andapply_self_pin_exemption()converges an exemption-less command onto the same byte sequence, so the unfreeze pattern has exactly one shape to recognize.- Return type:
- repomatic.release.version_sync.apply_self_pin_exemption(content, package, exemption)[source]¶
Splice a cooldown exemption into every
uvxcommand pinning package.The upstream toolkit’s inline pin moves in lockstep with the
uses:refs, regardless of the cooldown, so the version it names can be minutes old. Every workflow exports aUV_EXCLUDE_NEWERcovering all resolution, anduvxreads no per-package exemption from the environment or frompyproject.toml, so without the flag on the command line the freshly aligned pin fails to resolve until the window elapses.Idempotent: a command already carrying the exemption is left untouched.
- repomatic.release.version_sync.apply_workflow_literals(content, resolved, self_pin=None)[source]¶
Rewrite npm/PyPI version literals to their resolved version.
- Parameters:
content (
str) – The workflow file text.resolved (
dict[tuple[str,str],str]) – Mapping of(ecosystem, package)to the new version.self_pin (
tuple[str,str] |None) – Optional(package, exemption_flag)for the upstream toolkit’s own pin, whose rewrite bypasses the cooldown and therefore needsapply_self_pin_exemption()on the resulting command.
- Return type:
- Returns:
The updated text and a list of (package, old_version, new_version) changes actually applied.
Important
The returned list covers version moves only. A self_pin splice edits the text while reporting nothing, because it names a package rather than moving a version, so a caller deciding whether to write must compare the returned text against its input rather than test the list. Gating on the list silently discards the backfill, which is what stranded downstream repos already pinned at the newest release.
- repomatic.release.version_sync.find_upstream_ref_pins(content, upstream_repo)[source]¶
Extract the
uses:refs of the upstream repo’s workflows, with their SHAs.Matches reusable-workflow and composite-action refs of upstream_repo, both SHA-pinned with a trailing version comment (
owner/repo/.github/workflows/lint.yaml@abc123 # v1.2.3) and directly tag-pinned (...@v1.2.3, which yields aNoneSHA).Shared by
lint-repo’s inline-pin lockstep check,sync-workflow-pins’ upstream-pin alignment andinit’s pin floor (_highest_upstream_pin()), so all three read the refs the same way.- Return type:
- repomatic.release.version_sync.find_upstream_ref_versions(content, upstream_repo)[source]¶
Extract the bare
uses:ref versions of the upstream repo’s workflows.The version-only view of
find_upstream_ref_pins().
repomatic.release.virustotal module¶
Upload release binaries to VirusTotal and record detection snapshots.
Submits compiled binaries (.bin, .exe) to the VirusTotal API for malware
scanning. This seeds antivirus vendor databases with the signatures of freshly
built binaries, which keeps false-positive rates in check for downstream
distributors.
Detection statistics polled after an upload are appended to a CSV history, one
record per binary per scan date. The sync-binaries command renders that
history into the binaries catalog page (docs/binaries.md).
Note
Scan results are deliberately kept out of GitHub release notes: a raw
flagged / total count next to a download link reads as a malware verdict to
visitors, when it is almost always Nuitka onefile false positives. See
kdeldycke/meta-package-manager#1911
for the confusion this caused. The catalog page provides the context release
notes cannot.
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.release.virustotal.FREE_TIER_RATE_LIMIT = 4¶
VirusTotal free-tier request budget, in API calls per minute.
The single source for the upload and polling pace: the
scan-virustotalCLI default and both client functions below derive from it.
- repomatic.release.virustotal.SCAN_HEADERS = ('tag', 'filename', 'sha256', 'scanned', 'malicious', 'suspicious', 'undetected', 'harmless')¶
Columns of the committed scan history, in file order.
The release and the file it identifies first, then the four verdict counts, so the table reads left to right from what was scanned to what came back. Rows are ordered by release version rather than alphabetically, which a plain sort of the tag strings would get wrong past
v9.
- repomatic.release.virustotal.VIRUSTOTAL_GUI_URL = 'https://www.virustotal.com/gui/file/{sha256}'¶
URL template for the VirusTotal file analysis page.
- class repomatic.release.virustotal.DetectionStats(malicious, suspicious, undetected, harmless)[source]¶
Bases:
objectDetection statistics from a completed VirusTotal analysis.
Stores only the four categories that constitute a definitive verdict.
type-unsupported,timeout, andfailurefrom the API response are excluded from the total.
- class repomatic.release.virustotal.ScanResult(filename, sha256, analysis_url, detection_stats=None)[source]¶
Bases:
objectResult of uploading a single file to VirusTotal.
- detection_stats: DetectionStats | None = None¶
Detection statistics, or
Noneif analysis is still pending.
- class repomatic.release.virustotal.ScanRecord(tag, filename, sha256, scanned, stats)[source]¶
Bases:
objectA detection snapshot for one binary, taken on a given date.
Records accumulate in a CSV history (see
upsert_scan_records()) committed to the repository. Each record freezes theflagged / totalverdict counts at scan time, so the history supports trend analysis across releases even after VirusTotal re-analyzes the files or vendors process false-positive reports.- stats: DetectionStats¶
Detection statistics at scan time.
- repomatic.release.virustotal.scan_files(api_key, file_paths, rate_limit=4)[source]¶
Upload files to VirusTotal and return scan results.
Uses the synchronous
vt.ClientAPI. Sleeps between uploads to respect the free-tier rate limit.
- repomatic.release.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:
- Returns:
Results with
detection_statspopulated (orNonefor files whose analysis did not complete before the timeout).
- repomatic.release.virustotal.records_from_results(results, tag, scanned=None)[source]¶
Build history records from scan results whose analysis completed.
Results still pending (no detection statistics) are skipped: a record without verdict counts carries no information the release assets don’t already provide.
- Parameters:
results (
list[ScanResult]) – Scan results, typically frompoll_detection_stats().tag (
str) – Git tag of the release the binaries belong to.scanned (
str|None) – Snapshot date inYYYY-MM-DDformat. Today (UTC) whenNone.
- Return type:
- Returns:
One record per result with detection statistics.
- repomatic.release.virustotal.records_from_release_notes(body, tag, scanned)[source]¶
Recover detection snapshots from a legacy release-notes table.
Before the scan history file existed, the release pipeline appended a VirusTotal table to GitHub release notes, with a
flagged / totalDetections cell frozen minutes after publication. Those cells are genuine at-release snapshots, sosync-binaries --backfill-recordsharvests them to seed the history for releases that predate the file.Note
The legacy table only recorded the flagged and total aggregates, not the malicious/suspicious/undetected/harmless split. The split is rebuilt as flagged = malicious and the remainder = undetected, which is lossless for everything the catalog consumes (
flaggedandtotal).- Parameters:
- Return type:
- Returns:
One record per table row carrying a numeric Detections cell.
- repomatic.release.virustotal.load_scan_records(path)[source]¶
Load scan records from the CSV history file.
Note
A repository whose history predates the CSV store carries the same records in a sibling
.json, and is read from there when the CSV is absent. The nextupsert_scan_records()write lands as CSV, so a repository migrates on its first release after upgrading without anyone converting anything. The stale.jsonis then inert and can be deleted.- Parameters:
path (
Path) – Path to the CSV file.- Return type:
- Returns:
The records, or an empty list when neither file exists.
- Raises:
ValueError – When a file exists but cannot be parsed. Loud on purpose: a corrupt history must never be silently clobbered by the next
upsert_scan_records()write.
- repomatic.release.virustotal.upsert_scan_records(path, new_records)[source]¶
Merge new records into the CSV history at path.
Records sharing the same
(sha256, scanned)identity are replaced, so re-running a scan the same day is idempotent. The file is created (with its parent directories) when missing, and always rewritten in normalized form, sorted by version, filename and scan date.CSV also keeps the store out of the autofix lane: nothing there rewrites a
.csv, where a committed JSON file has to match whatever layout Biome is configured for orformat-jsonreformats it right back. Seeclaude.md§ Naming conventions rule 8 for the rest of that reasoning.- Parameters:
path (
Path) – Path to the CSV history.new_records (
list[ScanRecord]) – Records to merge in.
- Return type:
- Returns:
Truewhen the file content changed.