repomatic.deps.sync module¶
Registry of the cooldown-respecting dependency updaters, and their driver.
The five sync-* dependency bumpers (sync-dep-sources, sync-uv-lock,
sync-tool-versions, sync-action-pins, sync-workflow-pins) share a shape:
discover the latest eligible upstream version, gated by the
[tool.repomatic] minimum-release-age cooldown (or uv’s exclude-newer for
the lock), then rewrite the pin. This module turns that shape into data: one
SyncOperation per bumper, in SYNC_OPERATIONS. The thin
sync-* commands and the aggregate sync-deps command in
repomatic.cli.deps all drive the operations through it.
Resolve then apply
Each operation splits into a read phase and a write phase so sync-deps can run
the slow, network-bound discovery for every operation concurrently, then write
serially:
SyncOperation.resolveperforms the network discovery and computes the new file contents in memory, returning aSyncPlan. It does not touch the repository, so the resolves are safe to run in parallel.SyncOperation.applywrites the planned contents. Two of the five operations rewrite.github/workflows/*.yaml(action pins and workflow literals both live there), so applies must run serially.
sync-uv-lock and sync-dep-sources are the documented exceptions: their
discovery is a mutation (uv lock rewrites uv.lock), so their
SyncOperation.resolve writes during the parallel phase and their
SyncOperation.apply is a no-op. Their shared write domain (uv.lock,
pyproject.toml) is disjoint from every other operation’s, and the two are
serialized against each other through _UV_PROJECT_MUTEX. A --dry-run
resolve snapshots and restores the mutated files so the preview leaves no
trace.
The datasource adapters, version selection, and pure string rewriters live in
repomatic.deps.pins and repomatic.deps.cooldown; this module
composes them with the file I/O and checksum recompute. The markdown report of
a plan is rendered here too (render_plan_markdown()), since the CI jobs
read it. The terminal output stays in the CLI.
- repomatic.deps.sync.ACTIONLINT_MATCHER_URL = 'https://raw.githubusercontent.com/rhysd/actionlint/refs/tags/v{version}/.github/actionlint-matcher.json'¶
Where actionlint keeps its problem matcher, at the tag of a release.
- repomatic.deps.sync.DEPENDENCY_LABEL = '🔗 dependencies'¶
GitHub label applied to every dependency-update PR.
Shared by all five bumpers so a single label filters the whole family. Workflow YAML cannot import Python, so
autofix.yamlrepeats this string literally, and the labeller’s own rule tables (repomatic.github.labels.DEFAULT_CONTENT_RULESandDEFAULT_FILE_RULES) key their dependency rules on the same spelling.tests/test_sync_ops.pyasserts both copies match this constant, andtests/test_labels.pythat it names a labellabels.tomlactually defines: applying an unknown label fails theghcall outright, so a rename in the registry has to reach all of them at once.
- class repomatic.deps.sync.ResolveContext(config, today, release_notes=False, held_back=True, dry_run=False, lockfile=<factory>, jobs=1)[source]¶
Bases:
objectInputs shared by every
SyncOperation.resolve.Each operation reads the subset it needs. The cooldown is derived from config (
minimum-release-agefor the version-sync trio,exclude-newerfrom the lock forsync-uv-lock).
- class repomatic.deps.sync.ToolVersionExtras(binary_overrides=<factory>, actionlint_version=None, jobs=1)[source]¶
Bases:
objectsync-tool-versionswrite extras, applied after the source rewrite.
- class repomatic.deps.sync.UvProjectExtras(exclude_newer='', pins_synced=False, pruned_bypasses=<factory>, frozen_bypasses=<factory>, stale_bypass_comments=<factory>, bypass_forecasts=<factory>, source_swaps=<factory>)[source]¶
Bases:
objectExtras of the uv-project pair (
sync-uv-lock,sync-dep-sources).The pair shares one write domain (
uv.lock,pyproject.toml) and resolves under_UV_PROJECT_MUTEX. Each resolve already wrote those files, so these fields only inform the terminal and PR-body rendering.- pruned_bypasses: list[BypassForecast]¶
Expired
exclude-newer-packageentries removed frompyproject.toml, snapshot with the version and expiry each freeze had.
- stale_bypass_comments: list[str]¶
Pruned entries the comment above
exclude-newer-packagestill names.
- bypass_forecasts: list[BypassForecast]¶
Active cooldown-bypass freezes with their expiry forecasts.
- source_swaps: list[ReleaseSwap]¶
Git-tracked dependencies swapped to their released versions.
- class repomatic.deps.sync.SyncPlan(operation, subject, heading, changes=<factory>, dates=<factory>, released_overrides=<factory>, change_labels=<factory>, name_urls=<factory>, comparison_urls=<factory>, held_back=<factory>, held_back_name_urls=<factory>, 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.', notes_section='', upgrade_section='', cooldown_note='', cutoff=None, reference_date=None, file_writes=<factory>, self_pin_exemptions=<factory>, rebase=None, tool_versions=<factory>, uv_project=<factory>)[source]¶
Bases:
objectThe resolved, not-yet-written outcome of one operation’s read phase.
Carries everything
SyncOperation.applyneeds to write the changes and everything the CLI needs to render the terminal table and the markdown PR body, so the write and the rendering never re-resolve.- changes: list[tuple[str, str, str]]¶
Applied
(name, old, new)triples, in the order the report renders.
- released_overrides: dict[str, str]¶
Name to literal markdown replacing its “Released” table cell.
Marks rows whose version was decided outside the cooldown-checked release listing (the upstream toolkit’s lockstep-aligned pin), so the table shows the exemption instead of a blank cell.
- change_labels: dict[str, str]¶
Name to literal markdown labelling its “Change” table cell.
Marks the uv pin stepped back onto the checksum table of the pinned
setup-uv(see_gate_uv_on_checksums), which the arrow alone renders like any bump.
- held_back: list[HeldBackPackage]¶
Newer releases withheld only by the cooldown.
- held_back_note: str = '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 held-back section (cooldown wording).
- reference_date: date | None = None¶
Reference date for the table’s relative “Released” hints (the run date).
- file_writes: dict[Path, str]¶
Path to its new full text, as computed at resolve time.
sync-tool-versionswrites this text as is. The two.github/pin updaters only read which files it names: their apply replays the rewrite on current disk state throughrebase.
- self_pin_exemptions: list[str]¶
Workflow files that gained a missing self-pin cooldown exemption.
Names only the files whose sole edit was the splice, since a file that also moved a version is already reported through
changes. Kept as a separate list for the same reasonUvProjectExtras.frozen_bypassesis: the rewrite has no(name, old, new)triple to render, yet it still produced a hunk the report has to explain. Without it a splice-only run reads as “nothing to update” and the write is dropped, which is what let an exemption-less downstream pin sit broken indefinitely: the backfill only ever landed on a run that happened to move the version too.
- rebase: Callable[[str], tuple[str, list[tuple[str, str, str]]]] | None = None¶
Replay this operation’s rewriter against a file’s current text.
Set by
_plan_file_rewrites. Applies run serially after every resolve finished, and the two.github/pin updaters routinely plan rewrites of the same workflow files from the same pre-apply snapshot: writingfile_writesverbatim would silently revert whichever sibling applied first. The closure re-runs the pure rewriter on whatever is on disk at apply time instead.
- tool_versions: ToolVersionExtras¶
sync-tool-versionswrite extras (checksum recompute, problem matcher).
- uv_project: UvProjectExtras¶
sync-uv-lockandsync-dep-sourcesrendering extras.
- property has_changes: bool¶
Whether the operation found anything to update.
Cooldown-bypass edits count: a run that only prunes or freezes
exclude-newer-packageentries still rewritespyproject.tomland must produce a report explaining that hunk. A run that only splices a missing self-pin exemption into a workflow counts for the same reason.
- repomatic.deps.sync.render_plan_markdown(plan)[source]¶
Render a plan as the markdown PR-body section every updater shares.
Concatenates the source-swap section (when the plan carries one), the diff table, any release notes, the upgrade-review invitation, the uv cooldown-bypass section, and the held-back section exactly as the individual
sync-*commands do, sosync-depsand the thin commands produce identical output for the same plan. The invitation follows the release notes because those are what it asks the maintainer to work through.Every other section reports what the run did to the working tree, so the held-back one closes the body: it is the only forward-looking section, listing releases the run deliberately left alone. A run that only rewrites
exclude-newer-packageentries moves no version at all, and leading with the forecast would open its PR on the releases it did not adopt instead of thepyproject.tomlhunk it asks to merge.- Return type:
- class repomatic.deps.sync.SyncOperation(name, config_key, resolve, apply, applies_here)[source]¶
Bases:
objectOne cooldown-respecting dependency updater, as data.
The CLI command, workflow job step, PR branch and PR-body template all share
name, which makes a rename a one-line change.- resolve: Callable[[ResolveContext], SyncPlan]¶
Read phase: network discovery, returns a
SyncPlan.
- repomatic.deps.sync.SYNC_OPERATIONS: tuple[SyncOperation, ...] = (SyncOperation(name='sync-dep-sources', config_key='dep-sources.sync', resolve=<function _resolve_dep_sources>, apply=<function _apply_dep_sources>, applies_here=<function _dep_sources_applies>), SyncOperation(name='sync-uv-lock', config_key='uv-lock.sync', resolve=<function _resolve_uv_lock>, apply=<function _apply_uv_lock>, applies_here=<function _uv_lock_applies>), SyncOperation(name='sync-action-pins', config_key='action-pins.sync', resolve=<function _resolve_action_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>), SyncOperation(name='sync-workflow-pins', config_key='workflow-pins.sync', resolve=<function _resolve_workflow_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>), SyncOperation(name='sync-tool-versions', config_key='tool-versions.sync', resolve=<function _resolve_tool_versions>, apply=<function _apply_tool_versions>, applies_here=<function _tool_versions_applies>))¶
The cooldown-respecting dependency updaters, in CI execution order.
sync-dep-sourcesfirst (adopting a release changes what the routine re-lock even does), thensync-uv-lock(its lock churn gates other Python work), then the two workflow-file rewriters, then the upstream-only tool bump last.Only the first four share the
sync-depsjob inautofix.yaml.sync-tool-versionsruns fromself-maintenance.yamlon its own daily schedule, since it rewrites this package’s source and has no downstream meaning; the order still applies to a localrepomatic sync-deps, which runs every enabled operation in one pass.
- repomatic.deps.sync.OPERATIONS_BY_NAME: dict[str, SyncOperation] = {'sync-action-pins': SyncOperation(name='sync-action-pins', config_key='action-pins.sync', resolve=<function _resolve_action_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>), 'sync-dep-sources': SyncOperation(name='sync-dep-sources', config_key='dep-sources.sync', resolve=<function _resolve_dep_sources>, apply=<function _apply_dep_sources>, applies_here=<function _dep_sources_applies>), 'sync-tool-versions': SyncOperation(name='sync-tool-versions', config_key='tool-versions.sync', resolve=<function _resolve_tool_versions>, apply=<function _apply_tool_versions>, applies_here=<function _tool_versions_applies>), 'sync-uv-lock': SyncOperation(name='sync-uv-lock', config_key='uv-lock.sync', resolve=<function _resolve_uv_lock>, apply=<function _apply_uv_lock>, applies_here=<function _uv_lock_applies>), 'sync-workflow-pins': SyncOperation(name='sync-workflow-pins', config_key='workflow-pins.sync', resolve=<function _resolve_workflow_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>)}¶
SYNC_OPERATIONSkeyed bySyncOperation.name.
- repomatic.deps.sync.selected_operations(config, names=None)[source]¶
Return the operations to run, in
SYNC_OPERATIONSorder.The config feature flags are always authoritative: a disabled operation is dropped whether or not it was named (mirrors each standalone
sync-*command, which exits when its flag is off).- Parameters:
config (
Config) – The resolved configuration; disabled operations are dropped.names (
Sequence[str] |None) – When given, restrict to these operation names. Unknown names are ignored (the CLI validates them upstream). With none, operations whoseSyncOperation.applies_hereis false (nouv.lock, no workflow files, not the repomatic checkout) are dropped too: naming an operation is an explicit opt-in that bypasses that probe (the “scope exclusions are defaults, not absolutes” rule inclaude.md).
- Return type:
- repomatic.deps.sync.run_sync_operations(operations, rc, *, spinner_label=None)[source]¶
Resolve operations concurrently, then apply them serially.
The resolve phase fans out through
click_extra.run_jobs()(the work is network-bound and disjoint per operation), sized byResolveContext.jobs. A Ctrl+C drops queued resolves instead of waiting for them. When labelled, anclick_extra.OperationTrailreports each resolve as a✓/✘line and closes with a summary, its rendering tracking the worker count and its elapsed times following--time(click-extra’s own default). The apply phase runs inSYNC_OPERATIONSorder because two of the five rewrite the same workflow files. In--dry-runno apply runs. An operation whose resolve raises is logged and reported with aNoneplan so one failure never blocks the others.- Parameters:
operations (
Sequence[SyncOperation]) – The operations to run (already filtered by the caller).rc (
ResolveContext) – Shared resolve inputs.spinner_label (
str|None) – Present-tense label for the resolve trail (like"Resolving dependency updates"). When set, the trail prints a✓/✘line per operation on any stream, CI logs included, and a TTY also gets a running tally. Unset (programmatic and test calls), the trail is silent.
- Return type:
list[tuple[SyncOperation,SyncPlan|None]]- Returns:
Each operation paired with its plan (or
Noneif its resolve failed), inSYNC_OPERATIONSorder.