repomatic.sync_ops 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 registry is the single source of truth consumed three ways: the thin
sync-* commands and the aggregate sync-deps command in repomatic.cli.main,
and the consolidated CI job emitted by repomatic.github.workflow_sync.
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. Three of the five operations rewrite.github/workflows/*.yaml(action pins, workflow literals, and the actionlint matcher URL all 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.release.version_sync and repomatic.deps.uv; this module composes them
with the file I/O and checksum recompute. Terminal and PR-body rendering stay in
repomatic.cli.main, fed from the SyncPlan.
- repomatic.sync_ops.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.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.sync_ops.ResolveContext(config, today, release_notes=False, held_back=True, dry_run=False, lockfile=<factory>)[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.sync_ops.ToolVersionExtras(binary_overrides=<factory>, actionlint_version=None, checksums_path=None)[source]¶
Bases:
objectsync-tool-versionswrite extras, applied after the source rewrite.
- class repomatic.sync_ops.UvProjectExtras(exclude_newer='', reverted=False, pins_synced=False, pruned_bypasses=<factory>, frozen_bypasses=<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.
- 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.sync_ops.SyncPlan(operation, subject, heading, changes=<factory>, dates=<factory>, released_overrides=<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='', 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 everythingrepomatic.cli.mainneeds 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.
- 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.
Written verbatim by
SyncOperation.applyonly whenrebaseis unset; otherwise it records which files the resolve touched (and what it computed) while the apply replays the rewrite on current disk state.
- 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, matcher URL).
- 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.sync_ops.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 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.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:
- repomatic.sync_ops.print_sync_table(ctx, changes, dates, *, subject, reference_date)[source]¶
Print the shared terminal table for the dependency updaters.
Columns are
{subject} | Old | New | Released, the released date carrying a relative hint. Shared bysync-uv-lockand the threesync-*commands so their terminal output matches, and respects the global--table-format. Old/New stay separate columns (not the mergedChangecell of the markdown PR body) so structured--table-format json/csvoutput stays parseable.- Return type:
- repomatic.sync_ops.print_held_back_table(ctx, held_back, *, subject='Package')[source]¶
Print the shared held-back terminal table for the cooldown-gated updaters.
Columns are subject followed by
HELD_BACK_COLUMNS. Shared bysync-uv-lockand the threesync-*commands, and respects the global--table-format.- Return type:
- repomatic.sync_ops.print_bypass_table(ctx, forecasts)[source]¶
Print the active cooldown-bypass freezes with their expiry forecasts.
Columns are
BYPASS_COLUMNS, mirroring the markdown section fromformat_bypass_section(), and respects the global--table-format.- Return type:
- repomatic.sync_ops.print_plan_tables(ctx, plan, reference_date)[source]¶
Print a resolved plan’s diff, bypass and held-back tables.
The terminal counterpart of
render_plan_markdown(), deliberately beside it and in the same order, so a run’s terminal output and its PR body read the same way and cannot drift apart. Every dependency updater goes through here: the two lockfile commands, the three version-sync commands, and the aggregatesync-deps.Each table respects the global
--table-format, and an empty section prints nothing.- Return type:
- repomatic.sync_ops.emit_lockfile_sync_report(ctx, plan, *, reference_date, table, output, output_format)[source]¶
Emit the terminal tables and markdown report of a lockfile sync.
sync-uv-lockandsync-dep-sourcesshare this tail. They alone can suppress the terminal tables with--no-table(their CI jobs want only the markdown report), and they alone have anexclude-newercutoff to announce, uv’s lock-level cooldown standing in for theminimum-release-agewindow the version-sync trio reports.- Return type:
- repomatic.sync_ops.emit_version_sync_report(ctx, plan, output, output_format)[source]¶
Print a terminal report and optionally write a markdown PR-body report.
Shared by the three
sync-*version updaters. The terminal table and the markdown PR body (diff table, held-back section, release notes) route through the same shared rendererssync-uv-lockandsync-depsuse (render_plan_markdown()), so every dependency updater’s report matches.- Return type:
- repomatic.sync_ops.run_version_sync(ctx, op_name, output, output_format, release_notes, held_back, up_to_date)[source]¶
Shared body of the three version-sync commands.
sync-tool-versions,sync-action-pins, andsync-workflow-pinsdiffer only in their operation, feature flag, and messages: the resolve, apply, and report sequence is identical. The feature-flag guard stays with each command, which knows its own[tool.repomatic]key.- Parameters:
ctx (
Context) – The Click context, exited with0when nothing needs updating.op_name (
str) – TheOPERATIONS_BY_NAMEkey.output_format (
str) – The--output-formatvalue.release_notes (
bool) – Whether to fetch GitHub release notes.held_back (
bool) – Whether to report cooldown-held releases.up_to_date (
str) – Message printed when nothing needs updating.
- Return type:
- repomatic.sync_ops.resolve_lockfile_plan(op_name, config, *, lockfile, table, output, release_notes, held_back)[source]¶
Resolve one of the two lockfile-mutating operations.
sync-uv-lockandsync-dep-sourcesbuild the same resolve context and, unlike the version-sync trio, gate the held-back probe on a consumer being present: that probe costs a second full uv resolution, so a run that prints no table and writes no report must not pay for it.The apply and the narration stay with each command, whose “what happened” lines differ (adopted releases for one, bypass lifecycle for the other).
- Return type:
- Returns:
(operation, resolve_context, plan).
- class repomatic.sync_ops.SyncOperation(name, config_flag, job_name, resolve, apply, applies_here, write_domain, workflow='autofix.yaml', job='sync-deps', ci_flags=())[source]¶
Bases:
objectOne cooldown-respecting dependency updater, as data.
The CLI command, workflow job ID, PR branch and PR-body template all share
name, which makes a rename a one-line change.Note
The CI metadata (
job_name,write_domain,ci_flags) describes the job that runs the operation, but nothing generates that job from it: theautofix.yamlsteps are hand-written, andtests/test_sync_ops.pyis what holds the two descriptions in step.- resolve: Callable[[ResolveContext], SyncPlan]¶
Read phase: network discovery, returns a
SyncPlan.
- job: str = 'sync-deps'¶
Job ID inside
workflowhosting this operation’s steps.Defaults to the consolidated
sync-depsjob, which shares one checkout across every bumper whose write domain exists downstream. An operation that writes only to this repository’s own source belongs in a job of its own, in a workflowrepomatic initnever materializes downstream (seeSELF_MAINTENANCE_WORKFLOWS).
- property consolidated: bool¶
Whether this operation shares the multi-bumper
sync-depsjob.A consolidated operation must reset the working tree before it runs, so the previous bumper’s diff never bleeds into its PR. An operation with a job to itself starts from a clean checkout and needs no reset.
Compared against the
jobfield default rather than against a repeated"sync-deps"literal, so renaming the shared job is a one-line change.
- repomatic.sync_ops.SYNC_OPERATIONS: tuple[SyncOperation, ...] = (SyncOperation(name='sync-dep-sources', config_flag='dep_sources_sync', job_name='🔀 Sync dependency sources', resolve=<function _resolve_dep_sources>, apply=<function _apply_dep_sources>, applies_here=<function _dep_sources_applies>, write_domain=('uv.lock', 'pyproject.toml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--no-table', '--release-notes')), SyncOperation(name='sync-uv-lock', config_flag='uv_lock_sync', job_name='⛓️ Sync uv.lock', resolve=<function _resolve_uv_lock>, apply=<function _apply_uv_lock>, applies_here=<function _uv_lock_applies>, write_domain=('uv.lock', 'pyproject.toml [tool.uv]'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--no-table', '--release-notes')), SyncOperation(name='sync-action-pins', config_flag='action_pins_sync', job_name='📌 Sync action pins', resolve=<function _resolve_action_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>, write_domain=('.github/workflows/*.yaml', '.github/actions/**/*.yaml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--release-notes',)), SyncOperation(name='sync-workflow-pins', config_flag='workflow_pins_sync', job_name='🔖 Sync workflow pins', resolve=<function _resolve_workflow_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>, write_domain=('.github/workflows/*.yaml', '.github/actions/**/*.yaml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--release-notes',)), SyncOperation(name='sync-tool-versions', config_flag='tool_versions_sync', job_name='🔼 Sync tool versions', resolve=<function _resolve_tool_versions>, apply=<function _apply_tool_versions>, applies_here=<function _tool_versions_applies>, write_domain=('repomatic/tooling/tool_registry.py', '.github/workflows/lint.yaml'), workflow='self-maintenance.yaml', job='sync-tool-versions', ci_flags=('--release-notes',)))¶
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.sync_ops.OPERATIONS_BY_NAME: dict[str, SyncOperation] = {'sync-action-pins': SyncOperation(name='sync-action-pins', config_flag='action_pins_sync', job_name='📌 Sync action pins', resolve=<function _resolve_action_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>, write_domain=('.github/workflows/*.yaml', '.github/actions/**/*.yaml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--release-notes',)), 'sync-dep-sources': SyncOperation(name='sync-dep-sources', config_flag='dep_sources_sync', job_name='🔀 Sync dependency sources', resolve=<function _resolve_dep_sources>, apply=<function _apply_dep_sources>, applies_here=<function _dep_sources_applies>, write_domain=('uv.lock', 'pyproject.toml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--no-table', '--release-notes')), 'sync-tool-versions': SyncOperation(name='sync-tool-versions', config_flag='tool_versions_sync', job_name='🔼 Sync tool versions', resolve=<function _resolve_tool_versions>, apply=<function _apply_tool_versions>, applies_here=<function _tool_versions_applies>, write_domain=('repomatic/tooling/tool_registry.py', '.github/workflows/lint.yaml'), workflow='self-maintenance.yaml', job='sync-tool-versions', ci_flags=('--release-notes',)), 'sync-uv-lock': SyncOperation(name='sync-uv-lock', config_flag='uv_lock_sync', job_name='⛓️ Sync uv.lock', resolve=<function _resolve_uv_lock>, apply=<function _apply_uv_lock>, applies_here=<function _uv_lock_applies>, write_domain=('uv.lock', 'pyproject.toml [tool.uv]'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--no-table', '--release-notes')), 'sync-workflow-pins': SyncOperation(name='sync-workflow-pins', config_flag='workflow_pins_sync', job_name='🔖 Sync workflow pins', resolve=<function _resolve_workflow_pins>, apply=<function _apply_file_writes>, applies_here=<function _workflow_files_present>, write_domain=('.github/workflows/*.yaml', '.github/actions/**/*.yaml'), workflow='autofix.yaml', job='sync-deps', ci_flags=('--release-notes',))}¶
SYNC_OPERATIONSkeyed bySyncOperation.name.
- repomatic.sync_ops.selected_operations(config, *, here_only=True, 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.here_only (
bool) – Drop operations whoseSyncOperation.applies_hereis false (nouv.lock, no workflow files, not the repomatic checkout). Ignored when names is given: naming an operation is an explicit opt-in that bypasses the working-tree probe (the “scope exclusions are defaults, not absolutes” rule inclaude.md).names (
Sequence[str] |None) – When given, restrict to these operation names. Unknown names are ignored (the CLI validates them upstream).
- Return type:
- repomatic.sync_ops.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 by the global--jobsoption and sequential when no CLI context is active (as in tests). AtDEBUGverbosity the fan-out also collapses to sequential so per-operation log narration stays coherent, and 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 resolved worker count and its elapsed times following--time(click-extra’s own default). The apply phase runs inSYNC_OPERATIONSorder because three 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 and attached to a TTY, the trail shows a✓/✘line per operation and a running tally; unset (programmatic and test calls) forces it silent, so CI and tests show nothing.
- Return type:
list[tuple[SyncOperation,SyncPlan|None]]- Returns:
Each operation paired with its plan (or
Noneif its resolve failed), inSYNC_OPERATIONSorder.
- repomatic.sync_ops.operation_order(operations)[source]¶
Sort operations into
SYNC_OPERATIONSorder.- Return type: