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