Security

Supply chain security

repomatic implements most of the practices described in Astral’s Open Source Security at Astral post, baked into a drop-in setup that any maintainer can inherit by pointing their workflows at the reusable callers.

Astral practice

How repomatic covers it

Ban dangerous triggers (pull_request_target, workflow_run)

The lint-workflow-security job runs zizmor on every push: see .github/workflows/lint.yaml

Minimal workflow permissions

check_workflow_permissions parses every workflow file and warns when a custom-step workflow omits the top-level permissions key

Pinned actions

All uses: refs pinned to full commit SHAs (with the semver tag preserved as a trailing comment) via the sync-action-pins autofix job

No force-pushes to main

check_branch_ruleset_on_default verifies an active branch ruleset exists, and the setup guide walks users through creating one

Immutable release tags

check_immutable_releases verifies GitHub immutable releases is enabled, and the release workflow rewrites @main refs to @vX.Y.Z during freeze: see tagged workflow URLs

Dependency cooldowns

minimum-release-age shared cooldown for sync-tool-versions, sync-action-pins, and sync-workflow-pins; uv --exclude-newer for Python packages via sync-uv-lock, with a per-package escape hatch for CVE fixes: see minimum-release-age and cooldowns

Trusted Publishing

PyPI uploads via OIDC with no long-lived token. The publish-pypi job in each downstream caller workflow invokes the upstream publish-pypi composite action, which inherits the caller’s OIDC context. This sidesteps pypi/warehouse#11096, where reusable workflows mint an OIDC token whose job_workflow_ref does not match the downstream’s PyPI Trusted Publisher config. check_pypi_trusted_publisher reads PEP 740 provenance for the latest published file and warns when no bundle names this repo’s own release.yaml; the setup guide walks through the registration

Cryptographic attestations

Every binary and wheel is attested to the workflow run that built it via attest-build-provenance: see the Generate build attestations steps in .github/workflows/_release-engine.yaml

Checksums in installer scripts

The update-checksums CLI command regenerates SHA-256 checksums for every binary tool; invoked automatically by sync-tool-versions whenever a tool version is bumped

Fork PR approval policy

check_fork_pr_approval_policy warns when the policy is weaker than first_time_contributors, and the setup guide ships a pre-filled gh api one-liner to fix it

Warning

Known gap: multi-person release approval. Astral gates releases behind a dedicated GitHub deployment environment with required reviewers, so that a single compromised account cannot publish. repomatic does not enforce this, but if the repository has multiple maintainers, I recommend adding an environment: release key to the caller-side publish-pypi job (and to the upstream create-release job, if the caller exposes it) in the downstream workflow and configuring required reviewers on that environment in repo settings.

Important

One-time PyPI Trusted Publisher setup. Each downstream repository must register a Trusted Publisher entry on PyPI for its own caller workflow. The publisher config matches against the OIDC job_workflow_ref claim, which names the downstream’s workflow file (typically .github/workflows/release.yaml). Without this registration, the first PyPI upload after migration fails cleanly with a publisher mismatch error. See the PyPI Trusted Publishers documentation for the registration steps.

Third-party action minimization

Every third-party GitHub Action executes with access to GITHUB_TOKEN and repository secrets. Each action is a trust delegation: you depend on the maintainer’s security practices, their CI pipeline, and their transitive dependencies. A compromised action can steal secrets, inject code into builds, or tamper with releases.

repomatic has systematically eliminated 18 third-party actions since late 2025, replacing them with internal CLI commands, SHA-256-verified binary downloads, and runner built-in tools:

Removed action

Replacement

Strategy

calibreapp/image-actions

repomatic format-images

Internal CLI

crazy-max/ghaction-virustotal

repomatic scan-virustotal

Internal CLI

AndreasAugustin/actions-template-sync

repomatic init

Internal CLI

JasonEtco/is-sponsor-label-action

repomatic sponsor-label

Internal CLI

lycheeverse/lychee-action

repomatic run lychee

Direct binary + SHA-256

crate-ci/typos

repomatic run typos

Direct binary + SHA-256

biomejs/setup-biome

repomatic run biome

Direct binary + SHA-256

gitleaks/gitleaks-action

repomatic run gitleaks

Direct binary + SHA-256

julb/action-manage-label

repomatic run labelmaker

Direct binary + SHA-256

taiki-e/install-action

Direct curl + checksum

Direct binary + SHA-256

softprops/action-gh-release

gh release create

Runner built-in

actions/github-script

Bash + gh CLI

Runner built-in

actions-rust-lang/setup-rust-toolchain

Runner built-in Rust

Runner built-in

actions/setup-python

astral-sh/setup-uv

Consolidated

peaceiris/actions-gh-pages

actions/deploy-pages

First-party replacement

codecov/codecov-action

codecov-cli via uvx

Pinned CLI

codecov/test-results-action

None (feature dropped)

Removed entirely

GitHubSecurityLab/actions-permissions

Explicit permissions: key

Removed entirely

The remaining third-party actions (4 of 14 total) are:

Action

Purpose

astral-sh/setup-uv

Core toolchain: installs uv

peter-evans/create-pull-request

Creates autofix PRs

dessant/lock-threads

Locks inactive issues

crazy-max/ghaction-dump-context

Debug diagnostics (no secrets access)

Replacement strategies, ordered from most to least isolated:

  1. Internal CLI: the operation runs inside repomatic Python code with no external process.

  2. Direct binary download: checksummed binary fetched from a GitHub release URL, no action code path involved.

  3. Runner built-in: uses tools pre-installed on the GitHub Actions runner (gh, Rust toolchain).

  4. First-party replacement: swaps a community action for an official actions/* equivalent maintained by GitHub.

Ruff consolidation

Eight separate Python linters and formatters have been absorbed into ruff, eliminating eight runtime or dev dependencies:

Removed tool

What it did

Replaced

pylint

Static analysis and linting

Feb 2023

pydocstyle

Docstring convention enforcement

Feb 2023

pycln

Unused import removal

Feb 2023

pyupgrade

Python syntax modernization

Feb 2023

isort

Import sorting

Feb 2023

black

Code formatting

Sep 2023

docformatter

Docstring formatting

Jan 2024

blacken-docs

Python formatting in Markdown code blocks

Feb 2026

The mdformat-black plugin was also swapped for mdformat-ruff (Aug 2024): same dependency count, but aligns the Markdown pipeline with ruff’s formatting rules.

autopep8 is the only legacy formatter still in use: it handles long-line comment wrapping that ruff does not yet cover.

uv consolidation

Five separate packaging and install tools have been absorbed into uv, which now handles dependency management, builds, publishing, auditing, and Python version installation:

Removed tool

What it did

Replaced

poetry

Dependency management, lock files, virtual environments

Jun 2024

build / python -m build

Package building (wheels and sdists)

Sep 2024

twine

PyPI uploads

Jan 2025

check-wheel-contents

Wheel validation

Jan 2025

pip-audit

Vulnerability scanning

Mar 2026

uv also consolidated command-line usage that previously required separate tools: pip install became uv pip install / uv sync, pipx became uvx, and actions/setup-python was replaced by astral-sh/setup-uv (counted in the action minimization table above).

Two other Python packages were eliminated outside the ruff/uv consolidations: pipdeptree (replaced by an internal deps-graph implementation) and gitignore-parser (replaced by py-walk).

Permissions and token

Several workflows need a REPOMATIC_PAT secret to create PRs that modify files in .github/workflows/ and to trigger downstream workflows. Without it, those jobs silently fall back to the default GITHUB_TOKEN, which lacks the required permissions.

After your first push, the setup-guide job automatically opens an issue with step-by-step instructions to create and configure the token.

Concurrency and cancellation

All workflows use a concurrency directive to prevent redundant runs and save CI resources. When a new commit is pushed, any in-progress workflow runs for the same branch or PR are automatically cancelled.

Workflows are grouped by:

  • Pull requests: {workflow-name}-{pr-number} — Multiple commits to the same PR cancel previous runs

  • Branch pushes: {workflow-name}-{branch-ref} — Multiple pushes to the same branch cancel previous runs

release.yaml uses a stronger protection: release commits get a unique concurrency group based on the commit SHA, so they can never be cancelled. This ensures tagging, PyPI publishing, and GitHub release creation complete successfully.

Additionally, cancel-runs.yaml actively cancels in-progress and queued runs when a PR is closed. This complements passive concurrency groups, which only trigger cancellation when a new run enters the same group — closing a PR doesn’t produce such an event.

Tip

For implementation details on how concurrency groups are computed and why release.yaml needs special handling, see the repomatic.github.actions module docstring.

AV false-positive submissions

Compiled Python binaries (built with Nuitka --onefile) are frequently flagged as malicious by heuristic AV engines. The onefile packaging technique (self-extracting archive with embedded Python runtime) triggers generic “packed/suspicious” signatures. This is a known issue across the Nuitka ecosystem.

The scan-virustotal job in _release-engine.yaml uploads all compiled binaries to VirusTotal on every release. This seeds AV vendor databases to reduce false positive rates for downstream distributors (Chocolatey, Scoop, etc.). Each release’s flagged / total snapshot is recorded in docs/assets/virustotal-scans.json and rendered, along with the full catalog of released binaries and their analysis links, on the binaries page. Detection counts are deliberately kept out of GitHub release notes, where they read as a malware verdict without context (see kdeldycke/meta-package-manager#1911).

When a release is flagged, the /av-false-positive skill generates per-vendor submission files with pre-written text and form field mappings. The vendor details below document the process for manual reference.

Why binaries get flagged

Nuitka --onefile creates a self-extracting archive that decompresses an embedded Python runtime to a temporary directory and executes it at launch. This “drop and execute from temp” pattern is behaviorally identical to trojan droppers, which triggers heuristic and ML-based detections. Two more factors compound it: Nuitka is popular with malware authors for source code protection, which poisons AV heuristics for all Nuitka-compiled binaries, and Microsoft has gone as far as suspending an Artifact Signing account over Nuitka onefile binaries.

The detection profile is consistent across projects: Linux binaries scan clean, macOS ones pick up the occasional ML false positive, Windows ARM64 stays low (fewer ARM64 heuristics in AV engines), and Windows x64 attracts the bulk of the detections through generic signatures like Gen:Variant.Application.tedy (BitDefender family), Trojan:Win32/Sabsik (Microsoft), Python/Packed.Nuitka.AL (ESET), and various ML classifiers. Pure-Python .whl and .tar.gz distributions scan clean.

The Nuitka project tracks the situation in Nuitka/Nuitka#2685, Nuitka/Nuitka#2495, Nuitka/Nuitka#2757, and Nuitka/Nuitka#3842.

Vendor portals

Vendor

Engines covered

Portal

Format

Turnaround

Microsoft

Microsoft

WDSI file submission

One file per form, 1900 char limit on additional info

Fastest

BitDefender

BitDefender, ALYac, Arcabit, Emsisoft, GData, MicroWorld-eScan, VIPRE

bitdefender.com/submit

One file per form, screenshot mandatory

Fast

ESET

ESET-NOD32

Email to samples@eset.com

Single email, password-protected ZIP (infected), ~24 MB limit

Reliable

Symantec

Symantec

symsubmit.symantec.com

Hash submission only (no .exe/.bin upload), one hash per form, 5000 char limit

3-7 business days

Avast/AVG

Avast, AVG

avast.com/submit-a-sample

One file per form, shared engine

Medium

Sophos

Sophos

sophos.com filesubmission

One file per form, 25 MB max per submission

Up to 15 business days

Complete directories of vendor false-positive contacts are maintained by VirusTotal and False-Positive-Center.

Submission priority

Submit in this order to maximize impact:

  1. Microsoft: most influential engine. ML detections (Sabsik, Wacatac) have the broadest downstream effect.

  2. BitDefender: powers ~6 downstream vendor engines. Highest detection-removal-per-submission ratio.

  3. ESET: email-based channel with no portal dependency. The most reliable submission path.

  4. Symantec: ML detections (ML.Attribute.*) may take longer to process.

  5. Avast/AVG: shared engine, so one submission covers both.

  6. Sophos: PUA detections require justification of the software’s legitimate purpose.

Submission content

Every false-positive submission should include:

  • The binary’s VirusTotal report link.

  • VirusTotal links for the clean .whl and .tar.gz source distributions (as comparison evidence).

  • The GitHub release link and direct download URL for the binary.

  • Project homepage and PyPI URL.

  • License from pyproject.toml.

  • Reference to any prior false-positive issue in the repository.

All submission text should mention that the binary is compiled with Nuitka --onefile from an open-source project.

Known portal issues

  • Microsoft: CORS errors or stuck progress modals during upload (auth session expiring). Workaround: sign out, clear cookies for microsoft.com, sign back in, submit immediately.

  • BitDefender: form sometimes returns “Your request could not be registered!” with no details. Retry later.

  • Avast: form sometimes returns “An internal error occurred while sending the form.” Retry later.

Long-term mitigations

False-positive submissions are a per-release moving target. The structural fixes:

  • Code signing with an EV certificate would reduce heuristic detections across the board, especially from Microsoft and Symantec ML models.

  • Switching from --onefile to --standalone would eliminate the self-extracting pattern entirely, at the cost of distributing a directory instead of a single .exe.

  • Nuitka Commercial claims proprietary AV-mitigation techniques but offers no guarantees.

repomatic.virustotal API

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 JSON history file, 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.virustotal.VIRUSTOTAL_GUI_URL = 'https://www.virustotal.com/gui/file/{sha256}'

URL template for the VirusTotal file analysis page.

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.

class repomatic.virustotal.ScanRecord(tag, filename, sha256, scanned, stats)[source]

Bases: object

A detection snapshot for one binary, taken on a given date.

Records accumulate in a JSON history file (see upsert_scan_records()) committed to the repository. Each record freezes the flagged / total verdict 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.

tag: str

Git tag of the release the binary belongs to (e.g. v1.2.3).

filename: str

Filename of the scanned binary.

sha256: str

SHA-256 hash of the file content.

scanned: str

Scan date in YYYY-MM-DD format.

stats: DetectionStats

Detection statistics at scan time.

property key: tuple[str, str]

Deduplication identity: the same file scanned on the same day.

to_dict()[source]

Flatten to a JSON-ready mapping, detection stats inlined.

Return type:

dict[str, int | str]

classmethod from_dict(data)[source]

Rebuild a record from its flattened JSON mapping.

Return type:

ScanRecord

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.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 from poll_detection_stats().

  • tag (str) – Git tag of the release the binaries belong to.

  • scanned (str | None) – Snapshot date in YYYY-MM-DD format. Today (UTC) when None.

Return type:

list[ScanRecord]

Returns:

One record per result with detection statistics.

repomatic.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 / total Detections cell frozen minutes after publication. Those cells are genuine at-release snapshots, so sync-binaries --backfill-records harvests 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 (flagged and total).

Parameters:
  • body (str) – Release notes markdown.

  • tag (str) – Git tag of the release.

  • scanned (str) – Snapshot date, normally the release publication date.

Return type:

list[ScanRecord]

Returns:

One record per table row carrying a numeric Detections cell.

repomatic.virustotal.load_scan_records(path)[source]

Load scan records from a JSON history file.

Parameters:

path (Path) – Path to the JSON file.

Return type:

list[ScanRecord]

Returns:

The records, or an empty list when the file does not exist.

Raises:

ValueError – When the 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.virustotal.upsert_scan_records(path, new_records)[source]

Merge new records into the JSON history file 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, serialized with the same layout Biome’s JSON formatter produces so the format-json autofix job never rewrites it.

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

  • new_records (list[ScanRecord]) – Records to merge in.

Return type:

bool

Returns:

True when the file content changed.

repomatic.checksums API

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_runner.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.checksums.update_registry_checksums(tool_runner_path, version_overrides=None)[source]

Recompute binary checksums and version stamps in tool_runner.py.

Iterates every TOOL_REGISTRY entry with a binary spec, downloads each platform URL (concurrently, sized by the global --jobs option and sequential at DEBUG verbosity or without an active CLI context), computes its SHA-256, and replaces stale hashes in-place. Also reconciles each tool’s VERSIONS stamp with the version the checksums were computed for, the basis of the offline staleness test.

Parameters:
  • tool_runner_path (Path) – Path to tool_runner.py.

  • version_overrides (dict[str, str] | None) – Optional mapping of tool name to a version to download instead of the in-memory ToolSpec.version. sync-tool-versions passes 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:

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.binary API

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:

  • os: Operating system name, as used in GitHub-hosted runners.

    Hint

    We choose to run the compilation only on the latest supported version of each OS, for each architecture. Note that macOS and Windows do not have the latest version available for each architecture.

  • platform_id: Platform identifier, as defined by Extra Platform.

  • arch: Architecture identifier.

    Note

    Maybe we should just adopt target triple.

  • extension: File extension of the compiled binary.

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-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] in pyproject.toml) are added dynamically by binary_affecting_paths.

The release workflow entries cover both layouts: upstream keeps the _release-engine.yaml lane (which defines the Nuitka compile and binary self-test jobs) in-repo, while downstream repos call the engine cross-repo from their generated release.yaml, so a pin bump there rightly triggers a rebuild.

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'})

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, so skip_binary_build returns True when the PR head branch matches a member, saving an expensive Nuitka compilation.

Note

This set is intentionally disjoint from 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.binary.VERSION_BUMP_BRANCHES: Final[frozenset[str]] = frozenset({'major-version-increment', 'minor-version-increment', 'prepare-release'})

PR branches that carry only automated version-bump and lockfile churn.

Members are bot-authored draft PRs created by the bump-version and prepare-release jobs in changelog.yaml. Their working tree is byte-identical to main except for the version string in pyproject.toml, **/__init__.py, changelog.md, citation.cff, and uv.lock. Heavy PR-time workflows (tests.yaml, lint.yaml, labels.yaml) list these branches under pull_request.branches-ignore so the matrix doesn’t burn CI minutes for a guaranteed-passing run.

Note

These branches are not binary-neutral: the rewritten version string is baked into the Nuitka binary, so they are deliberately absent from SKIP_BINARY_BUILD_BRANCHES. Post-merge release artifacts on main are still produced.

repomatic.binary.MANUAL_VERSION_BUMP_COMMIT_PREFIXES: Final[frozenset[str]] = frozenset({'Bump major version to ', 'Bump minor version to '})

Head-commit-message prefixes for user-initiated version bumps.

Members are the bump-version job’s Bump $part version to \``v$version\ commit messages (rendered from the bump-version template’s title). These merges land as a single commit on main and carry no other payload, so workflows can short-circuit on them safely.

The release-cycle prefix [changelog] Post-release bump `` is deliberately absent from this set because the ``prepare-release merge bundles the post-release-bump commit with the actual release commit ([changelog] Release vX.Y.Z) in a single push. Workflows that gate on the head commit message (tests.yaml, release.yaml::compile-binaries) must run on those pushes to test the release commit and build its binary — so they consult only this subset.

repomatic.binary.VERSION_BUMP_COMMIT_PREFIXES: Final[frozenset[str]] = frozenset({'Bump major version to ', 'Bump minor version to ', '[changelog] Post-release bump '})

Full set of head-commit-message prefixes that mark a version-bump push.

Combines MANUAL_VERSION_BUMP_COMMIT_PREFIXES with the [changelog] Post-release bump `` prefix produced by ``prepare-release merges. Workflows without a release-artifact dependency (lint.yaml, labels.yaml) use this full set in their metadata job’s if: gate so the entire job graph skips for any push generated by the version-bump PR family. Workflows that do produce release artifacts on the same push use MANUAL_VERSION_BUMP_COMMIT_PREFIXES instead.

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