repomatic.github.workflow_sync module¶
Generation, sync, and lint for downstream workflows.
Downstream repositories consuming reusable workflows from kdeldycke/repomatic
manually write caller workflows that often miss triggers like
workflow_dispatch. This module provides tools to generate, synchronize, and
lint those callers by parsing the canonical workflow definitions.
render_thin_caller_for_target() is the single entry point that turns a
canonical workflow into a downstream file on disk; repomatic init drives it.
Generating and reshaping workflow content in Python, rather than
hand-maintaining YAML, keeps logic out of the platform-specific GitHub Actions
surface: a tested generator that fails loudly beats a static YAML artifact that
can silently drift, and the smaller GHA surface eases a future migration to
another CI platform. _render_publish_pypi_job derives each downstream
publish-pypi job from the canonical release.yaml this way.
Caution
PyYAML destroys formatting and comments on round-trip. Until we find a layout-preserving YAML parsing and rendering solution, we use raw text extraction to manipulate workflow files while preserving formatting and comments.
- repomatic.github.workflow_sync.cooldown_env_block()[source]¶
Render the supply-chain cooldown
env:block every workflow carries.Rendered from
minimum_release_agerather than written by hand, so the literal in the YAML has exactly one source. The same text is asserted verbatim against every checked-in workflow bytests/test_workflows.py, and emitted into the downstreamrelease.yamlcaller by_generate_release_caller().Caution
The comment travels into every downstream repository, so it must read true there too. It deliberately does not name
tests/test_workflows.py: that file exists only here, and a synced copy would point its readers at a path they do not have. Keep any wording added below equally context-free, and name a repomatic-private path only in a comment that never ships.Note
A workflow-level
env:block cannot referenceneeds, which is why the window is a literal here instead of ametadatajob output: themetadatajob runsuvxto compute its own outputs, so anything sourced from it would leave that bootstrap install ungated. Seeclaude.md§ Cooldown on every install.- Return type:
- Returns:
The comment and
env:mapping, newline-terminated, ready to splice above a workflow’sjobs:line.
- repomatic.github.workflow_sync.PERMISSION_RANK: Final[dict[str, int]] = {'none': 0, 'read': 1, 'write': 2}¶
Relative strength of the
permissions:levels GitHub accepts.Used to union the same scope granted at different levels across the jobs of a canonical workflow, keeping the most permissive one.
- repomatic.github.workflow_sync.DEFAULT_VERSION: Final[str] = 'main'¶
Default version reference for upstream workflows.
For release builds (e.g.,
repomatic==5.11.0), this resolves to the corresponding tag (v5.11.0). For development builds (5.11.1.dev0), it falls back tomainsince the tag does not exist yet.
- class repomatic.github.workflow_sync.WorkflowTriggerInfo(name, filename, non_call_triggers, call_inputs, call_secrets, has_workflow_call, concurrency, raw_concurrency)[source]¶
Bases:
objectParsed trigger information from a canonical workflow.
- class repomatic.github.workflow_sync.LintResult(message, is_issue, level=AnnotationLevel.WARNING)[source]¶
Bases:
objectResult of a single lint check.
- level: AnnotationLevel = 'warning'¶
Severity level for GitHub Actions annotations.
- repomatic.github.workflow_sync.workflow_triggers(data)[source]¶
Extract a parsed workflow’s
on:mapping.Note
PyYAML follows YAML 1.1, where a bare
onkey parses as the booleanTruewhile a quoted"on"stays a string. Both spellings occur in the wild, so every reader of a workflow’s triggers has to try the boolean key first and the string key second. Resolving that here once keeps the quirk from being re-remembered at each call site.
- repomatic.github.workflow_sync.canonical_caller_permissions(filename: str) dict[str, str][source]¶
Union the job-level
permissions:scopes of a canonical workflow.Memoized like
extract_trigger_info(), and under the same read-only contract on the shared result.A caller job hands its own permissions down to the reusable workflow it calls, and the called workflow’s jobs are capped by them: they cannot escalate beyond what the caller granted. The canonical workflows pin a top-level
permissions: {}, so a job without its own block needs nothing and the union of the job-level blocks is the complete set the caller has to forward.A scope appearing at different levels across jobs resolves to the most permissive one, so no job is starved by another’s narrower grant.
- Parameters:
filename (
str) – Canonical workflow filename (e.g.,autofix.yaml).- Return type:
- Returns:
Scope-to-level mapping, sorted by scope. Empty when no job declares permissions, meaning the caller forwards nothing.
- Raises:
FileNotFoundError – If the workflow file is not bundled.
- repomatic.github.workflow_sync.extract_trigger_info(filename: str) WorkflowTriggerInfo[source]¶
Extract trigger information from a bundled canonical workflow.
Parses the workflow YAML and separates
workflow_callconfiguration from other triggers.Memoized: the workflow lint asks for the same canonical’s triggers once per check, per downstream file.
Caution
The returned
WorkflowTriggerInfois shared between callers and holds mutable dicts: treat it as read-only, the way every reader does today.- Parameters:
filename (
str) – Workflow filename (e.g.,release.yaml).- Return type:
- Returns:
Parsed trigger information.
- Raises:
FileNotFoundError – If the workflow file is not bundled.
- class repomatic.github.workflow_sync.PathsSpec(source_paths=None, extra_paths=<factory>, ignore_paths=<factory>, workflow_paths=<factory>)[source]¶
Bases:
objectBundle of downstream
paths:adaptation knobs.Each field maps to a
[tool.repomatic.workflow]option.- Parameters:
source_paths (
list[str] |None) – Substituted in for the canonicalrepomatic/**glob in every workflow that references it.Nonedrops the glob without substitution.extra_paths (
list[str]) – Appended to every workflow’spaths:list (after source substitution andignore_pathsfiltering, before render). Skipped for workflows listed in workflow_paths.ignore_paths (
list[str]) – Removed from every workflow’spaths:list by exact string match. Skipped for workflows listed in workflow_paths.workflow_paths (
dict[str,list[str]]) – Per-workflow override keyed by filename. The value is treated as the completepaths:list for that workflow; the other knobs do not apply.
- repomatic.github.workflow_sync.generate_thin_caller(filename, repo='kdeldycke/repomatic', version='main', commit_sha=None, paths_spec=None, with_permissions=False, existing=None)[source]¶
Generate a thin caller workflow for a reusable canonical workflow.
The generated caller mirrors the canonical workflow’s non-
workflow_calltriggers verbatim and delegates to the upstream workflow viauses:.workflow_dispatchis not injected: workflows that should expose manual dispatch declare it in the canonical definition. Declaredworkflow_callinputs and secrets are forwarded explicitly viawith:andsecrets:.Canonical
paths:filters are adapted via paths_spec (seePathsSpec).When commit_sha is provided, the
uses:reference is SHA-pinned (@sha # version), secure-by-default from the first commit. Thesync-action-pinsjob bumps it once a newer release clears the cooldown.- Parameters:
filename (
str) – Canonical workflow filename (e.g.,release.yaml).repo (
str) – Upstream repository (default:kdeldycke/repomatic).version (
str) – Version reference (default:main).commit_sha (
str|None) – Full 40-character commit SHA for the version tag. When provided, produces@sha # version. WhenNone, produces@version.paths_spec (
PathsSpec|None) – Full paths-adaptation spec; defaults to no adaptation.with_permissions (
bool) – Emit an explicit permissions contract: a top-levelpermissions: {}plus, on the managed job, the scopes the reusable workflow needs (seecanonical_caller_permissions()). Set when the downstream file carries extra jobs of its own, whose customsteps:are what make the top-level key worth pinning. Both halves ship together: the top-level{}alone would starve the managed call, which GitHub aborts at startup the moment a nested job asks for a scope the caller never granted.existing (
str|None) – Current content of the downstream file, when it already exists. Onlyrelease.yamlreads it, to carry over the extraneeds:edges of itsreleaselane; every other caller regenerates whole.
- Return type:
- Returns:
Complete YAML content for the thin caller workflow.
- Raises:
ValueError – If the workflow does not support
workflow_call.
- repomatic.github.workflow_sync.EXTRA_JOBS_SEPARATOR: Final[str] = '\n\n'¶
Gap between the last managed lane and the downstream extras below it.
Exactly one blank line, matching how
_generate_release_caller()separates its own jobs. Both sides are trimmed before it is applied, because neither end is stable on its own: the release caller ends on a trailing blank line where a plain thin caller does not, andextract_extra_jobs()slices from the end of the last managed job body, so it returns however many blank lines the file already had. Joining those two as-is added one blank line per sync, without bound.
- repomatic.github.workflow_sync.render_thin_caller_for_target(filename, target, *, repo='kdeldycke/repomatic', version='main', commit_sha=None, paths_spec=None)[source]¶
Render the complete downstream content of target, extras included.
The single seam between a canonical workflow and a file on disk: read what is already there, carry over what only the downstream copy knows, render the managed lanes, and re-attach the extras.
repomatic initis the only caller, so a preservation argument can only ever be wired up once.Caution
Do not inline this back into a caller. It previously existed as two near-identical copies, and the
existingargument that carries a consumer’sneeds:edges across a sync reached only one of them: every downstreamrepomatic initsilently dropped the edge while the test suite, which drove the other copy, stayed green.tests/test_workflow_sync.pypins the seam to a single call site.Reads target itself rather than taking its content, so a caller cannot forget to hand over the state that preservation depends on.
- Parameters:
filename (
str) – Canonical workflow filename (e.g.release.yaml).target (
Path) – Destination path, read when it already exists.repo (
str) – Upstream repository for theuses:refs.version (
str) – Version reference for theuses:refs.commit_sha (
str|None) – Full 40-character commit SHA for SHA-pinned refs.paths_spec (
PathsSpec|None) – Full paths-adaptation spec; defaults to no adaptation.
- Return type:
- Returns:
The content to write, and the current content of target (
Nonewhen it does not exist yet) so a caller can skip an unchanged write.- Raises:
ValueError – If filename declares no
workflow_calltrigger.
- repomatic.github.workflow_sync.GENERATED_CALLER_JOBS: Final[frozenset[str]] = frozenset({'build', 'publish-pypi', 'release'})¶
Every job the generated downstream
release.yamldefines.The canonical entry may hold repomatic-local jobs beyond these three (its own
pack-plugin, for one), and only these three are copied downstream. Anything the canonicalreleaselane names inneeds:outside this set has to be dropped, or the generated file would reference a job that does not exist there. See_merge_release_needs().
- repomatic.github.workflow_sync.identify_canonical_workflow(workflow_path, repo='kdeldycke/repomatic')[source]¶
Identify if a workflow is a thin caller for a canonical upstream workflow.
Scans jobs for a
uses:reference matching the upstream repository pattern.
- repomatic.github.workflow_sync.extract_extra_jobs(content, repo='kdeldycke/repomatic')[source]¶
Extract extra downstream jobs from an existing thin-caller workflow.
Parses the file with YAML to identify the managed thin-caller job (the one whose
uses:references the upstream repository), then returns all raw text after that job: blank lines, comments, and additional job definitions.Uses raw text slicing (not YAML round-tripping) to preserve formatting and comments, consistent with the rest of the module.
- repomatic.github.workflow_sync.extras_define_jobs(extra)[source]¶
Whether an extras fragment holds actual job definitions.
A fragment can be comments and blank lines only (a trailing note kept after the managed lanes): that content is worth carrying over verbatim, but it must not flip the caller into the explicit-permissions contract reserved for real downstream jobs.
- Return type:
- repomatic.github.workflow_sync.check_has_workflow_dispatch(workflow_path)[source]¶
Check that a workflow has a
workflow_dispatchtrigger.- Parameters:
workflow_path (
Path) – Path to the workflow file.- Return type:
- Returns:
Lint result.
- repomatic.github.workflow_sync.check_version_pinned(workflow_path, repo='kdeldycke/repomatic')[source]¶
Check that a thin caller pins to a version tag, not
@main.- Parameters:
- Return type:
- Returns:
Lint result.
- repomatic.github.workflow_sync.check_triggers_match(workflow_path, canonical_filename)[source]¶
Check that a thin caller’s triggers match the canonical workflow.
Verifies that the caller includes all non-
workflow_calltriggers defined in the canonical workflow.- Parameters:
- Return type:
- Returns:
Lint result.
- repomatic.github.workflow_sync.check_secrets_passed(workflow_path, canonical_filename)[source]¶
Check that a thin caller passes all required secrets explicitly.
Verifies that every secret declared by the canonical workflow is forwarded by the caller, either via explicit
secrets:mapping or viasecrets: inherit.- Parameters:
- Return type:
- Returns:
Lint result.
- repomatic.github.workflow_sync.generate_workflow_header(filename, paths_spec=None)[source]¶
Return the raw header of a canonical workflow.
The header is everything before the
jobs:line:name,ontriggers,concurrency, and any comments.Each
paths:block in the header is rewritten using paths_spec: upstream source references substituted, optional extras appended, ignored entries stripped, or replaced wholesale via a per-workflow override (seePathsSpec). When the resulting list is empty, the entirepaths:block is removed. Comments outside the rewritten blocks are preserved verbatim; comments inside an entry block are not supported.- Parameters:
- Return type:
- Returns:
Raw header text.
- Raises:
FileNotFoundError – If the workflow file is not bundled.
ValueError – If no
jobs:line is found.
- repomatic.github.workflow_sync.run_workflow_lint(workflow_dir, repo='kdeldycke/repomatic', fatal=False)[source]¶
Lint all workflow files in a directory.
For thin callers (workflows that delegate to a canonical upstream workflow via
uses:), runs caller-specific checks: version pinning, trigger match, and secrets passed. For standalone workflows, runscheck_has_workflow_dispatch()to flag missing manual triggers.Thin callers are exempt from
check_has_workflow_dispatch()becausecheck_triggers_match()is authoritative: a thin caller mirrors its canonical workflow exactly, and some canonical workflows (e.g.,cancel-runs.yaml) intentionally lackworkflow_dispatch.