repomatic.github package¶
GitHub integration package: API clients, gh CLI wrapper, and helpers
for every GitHub surface repomatic touches (releases, issues, PRs, tokens,
workflows). See each submodule’s docstring for its scope.
Submodules¶
repomatic.github.actions module¶
GitHub Actions output formatting, annotations, and workflow events.
This module provides utilities for working with GitHub Actions: multiline output formatting, workflow annotations, event payload loading, and GitHub-specific constants and enums shared across multiple modules.
Note
Concurrency quirks addressed by the workflows
SHA-based groups (``release.yaml``): the block sits on the
push-triggered entry workflow, not the reusable _release-engine.yaml it
calls. GitHub decides run cancellation from the entry workflow’s group, and
a block on the engine lane (reached via needs: build) joins its group only
after the build lane finishes, too late to cancel queued or building runs.
cancel-in-progress is evaluated on the new workflow, not the old one. If
a regular commit is pushed while a release workflow is running, the new
workflow would cancel it (same group). Solution: release commits (freeze and
unfreeze) get a unique group keyed by github.sha, so they can never be
cancelled.
Event-scoped groups (``changelog.yaml``): changelog.yaml has
both push and workflow_run triggers. Without event_name in
the concurrency group, a fast-completing workflow_run event would
cancel the push event’s prepare-release job, then skip
prepare-release itself (guarded by if: event_name != 'workflow_run'),
so it would never run. Including event_name prevents cross-event
cancellation.
``workflow_run`` checkout ref: Always use github.sha (latest
commit on the default branch), never workflow_run.head_sha (the
commit that triggered the upstream workflow). After a release cycle
adds commits (freeze + unfreeze), head_sha is stale and produces
a tree that conflicts with current main.
- repomatic.github.actions.NULL_SHA = '0000000000000000000000000000000000000000'¶
The null SHA used by Git to represent a non-existent commit.
GitHub sends this value as the
beforeSHA when a tag is created, since there is no previous commit to compare against.
- class repomatic.github.actions.WorkflowEvent(*values)[source]¶
Bases:
StrEnumWorkflow events that cause a workflow to run.
- branch_protection_rule = 'branch_protection_rule'¶
- check_run = 'check_run'¶
- check_suite = 'check_suite'¶
- create = 'create'¶
- delete = 'delete'¶
- deployment = 'deployment'¶
- deployment_status = 'deployment_status'¶
- discussion = 'discussion'¶
- discussion_comment = 'discussion_comment'¶
- fork = 'fork'¶
- gollum = 'gollum'¶
- issue_comment = 'issue_comment'¶
- issues = 'issues'¶
- label = 'label'¶
- merge_group = 'merge_group'¶
- milestone = 'milestone'¶
- page_build = 'page_build'¶
- project = 'project'¶
- project_card = 'project_card'¶
- project_column = 'project_column'¶
- public = 'public'¶
- pull_request = 'pull_request'¶
- pull_request_comment = 'pull_request_comment'¶
- pull_request_review = 'pull_request_review'¶
- pull_request_review_comment = 'pull_request_review_comment'¶
- pull_request_target = 'pull_request_target'¶
- push = 'push'¶
- registry_package = 'registry_package'¶
- release = 'release'¶
- repository_dispatch = 'repository_dispatch'¶
- schedule = 'schedule'¶
- status = 'status'¶
- watch = 'watch'¶
- workflow_call = 'workflow_call'¶
- workflow_dispatch = 'workflow_dispatch'¶
- workflow_run = 'workflow_run'¶
- class repomatic.github.actions.AnnotationLevel(*values)[source]¶
Bases:
EnumAnnotation levels for GitHub Actions workflow commands.
- ERROR = 'error'¶
- WARNING = 'warning'¶
- NOTICE = 'notice'¶
- repomatic.github.actions.generate_delimiter()[source]¶
Generate a unique delimiter for GitHub Actions multiline output.
GitHub Actions requires a unique delimiter to encode multiline values in
$GITHUB_OUTPUT. This function generates a random delimiter that is extremely unlikely to appear in the output content.The delimiter format is
GHA_DELIMITER_NNNNNNNNNwhere N is a digit, producing a 9-digit random suffix.- Return type:
- Returns:
A unique delimiter string.
- repomatic.github.actions.format_multiline_output(name, value)[source]¶
Format a multiline value for GitHub Actions output.
Produces output in the heredoc format required by
$GITHUB_OUTPUT:name<<GHA_DELIMITER_NNNNNNNNN value line 1 value line 2 GHA_DELIMITER_NNNNNNNNN
- repomatic.github.actions.emit_annotation(level, message)[source]¶
Emit a GitHub Actions workflow annotation.
Prints a workflow command that creates an annotation visible in the GitHub Actions UI and PR checks.
- Parameters:
level (
AnnotationLevel|Literal['error','warning','notice']) – The annotation level (error, warning, or notice).message (
str) – The annotation message.
- Return type:
- repomatic.github.actions.get_github_event() dict[str, Any][source]¶
Load the GitHub event payload from
GITHUB_EVENT_PATH.
- repomatic.github.actions.cancel_superseded_runs(branch, current_run_id)[source]¶
Cancel the in-progress and queued workflow runs of branch.
Backs the
cancel-runscommand, fired when a pull request closes: GitHub’sconcurrencymechanism only cancels a run when a new run enters the same group, and closing a PR fires no such run, so the branch’s live runs would otherwise burn CI minutes to completion.Every listed run except current_run_id (the cancelling run itself) is cancelled. A run that fails to cancel (already finished, insufficient token scope) is logged and skipped so one straggler never aborts the sweep. The repository is resolved by the
ghCLI fromGH_REPOor the checkout, matching every othergh apicall.
repomatic.github.dev_release module¶
Sync a rolling dev pre-release on GitHub.
Maintains a single draft pre-release that mirrors the unreleased
changelog section and always carries the latest successful dev binaries
and Python package. The dev tag (e.g. v6.1.1.dev0) is force-updated
to point to the latest main commit — no tag proliferation.
When the current version’s dev release already exists, it is edited
(not deleted and recreated) so that previously uploaded assets — especially
compiled binaries — survive pushes that skip binary compilation (e.g.
documentation-only changes). The upload_release_assets() function deletes
all existing assets before uploading new ones, preventing stale files from
accumulating when the naming scheme changes. Stale dev releases from previous
versions are always deleted.
Note
Dev releases are created as drafts so they remain mutable even
when GitHub’s immutable releases setting is enabled. Immutability
only blocks asset uploads on published releases — deletion still
works. But because the workflow needs to upload binaries after
creation, the release must stay as a draft throughout its lifetime
to allow asset uploads. See CLAUDE.md § Immutable releases.
- repomatic.github.dev_release.DEV_ASSET_PATTERNS = ('*.bin', '*.exe', '*.whl', '*.tar.gz')¶
Glob patterns for dev release assets.
Note
Bare extensions (no
repomatic-prefix) keep patterns generic so downstream repositories can reuse the same logic regardless of their package name.
- repomatic.github.dev_release.sync_dev_release(changelog_path, version, nwo, dry_run=True, asset_dir=None)[source]¶
Create or update the dev pre-release on GitHub.
Reads the changelog, renders the release body for the given version via
build_expected_body(), then either edits the existing dev release or creates a new one. Stale dev releases from previous versions are always cleaned up.Existing releases are edited (not deleted and recreated) to preserve assets like compiled binaries from previous successful builds. When
asset_diris provided, existing assets are deleted and new ones uploaded viaupload_release_assets().- Parameters:
changelog_path (
Path) – Path tochangelog.md.version (
str) – Current version string (e.g.6.1.1.dev0).nwo (
str) – Repository name-with-owner (e.g.user/repo).dry_run (
bool) – IfTrue, report without making changes.asset_dir (
Path|None) – Directory containing assets to upload. IfNone, no asset upload is performed.
- Return type:
- Returns:
Trueif the release was synced (or would be in dry-run),Falseif the changelog section is empty.
- repomatic.github.dev_release.upload_release_assets(tag, nwo, asset_dir)[source]¶
Upload assets to a GitHub release.
Scans
asset_dirfor files matchingDEV_ASSET_PATTERNS. If no matching files are found, returns immediately without modifying the release — this preserves existing assets for documentation-only pushes. When files are found, all existing assets are deleted first to prevent stale files from accumulating when the naming scheme changes.
- repomatic.github.dev_release.cleanup_dev_releases(nwo, *, keep_tag=None)[source]¶
Delete stale dev pre-releases from GitHub.
Lists all releases and deletes any whose tag ends with
.dev0, exceptkeep_tagwhich is preserved so its assets (e.g. compiled binaries) survive. This handles stale dev releases left behind after version bumps. Silently succeeds if no dev releases exist or if individual deletions fail.
- repomatic.github.dev_release.delete_dev_release(version, nwo)[source]¶
Delete the dev pre-release and its tag from GitHub.
Silently succeeds if no dev release exists. This is used during real releases to clean up the dev pre-release for the version being released.
repomatic.github.gh module¶
Generic wrapper for the gh CLI.
Note
Workflow steps must set GH_TOKEN explicitly: GITHUB_TOKEN is a
secret expression in GitHub Actions, not an automatic environment variable.
The standard pattern is GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }}
for steps that prefer a PAT, or GH_TOKEN: ${{ github.token }} otherwise.
As defense-in-depth, run_gh_command() promotes REPOMATIC_PAT to
GH_TOKEN when set, and promotes GITHUB_TOKEN to GH_TOKEN when
GH_TOKEN is absent. On a 401 from the primary token (either Bad
credentials` from an expired or revoked PAT, or Requires
authentication` from a GitHub-side auth incident or a fine-grained PAT
scope quirk) it first retries with the **same** token after a short
back-off (catching transient flaps that clear on their own), then with
``GITHUB_TOKEN if available and different. When every retry path is
exhausted, the raised RuntimeError is annotated with the current
githubstatus.com summary so operators
are not sent chasing PAT scopes during an upstream incident.
- repomatic.github.gh.resolve_gh_token()[source]¶
Return the GitHub token from environment variables.
The canonical lookup order for every GitHub API access in the package:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN. Empty string when no variable is set.- Return type:
- repomatic.github.gh.run_gh_command(args)[source]¶
Run a
ghCLI command and return stdout.Token priority:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN. TheghCLI does not recognizeREPOMATIC_PAT, so when set it is injected asGH_TOKEN. On a 401 from the primary token (Bad credentials` orRequires authentication`) the command is first retried with the **same** token after a short bounded back-off (see ``_TRANSIENT_AUTH_BACKOFF_SECONDS), absorbing transient GitHub auth flaps that resolve on their own. If 401s persist, the command is then retried withGITHUB_TOKENif available and different, letting CI jobs degrade gracefully to the standard Actions token instead of failing outright on a stale PAT. When every retry path is exhausted, the raisedRuntimeErrorcarries a githubstatus.com annotation when an incident is active.- Parameters:
- Return type:
- Returns:
The stdout output from the command.
- Raises:
RuntimeError – If the command fails (after retries and fallback, if attempted).
- repomatic.github.gh.iter_graphql_nodes(query, connection_path, variables=None, *, page_size_var='', page_size=0, max_nodes=None)[source]¶
Iterate a GraphQL connection’s nodes, following cursor pagination.
The shared
gh api graphqlpagination loop: run the query, walk the response to the connection object, yield each node, then followpageInfo.hasNextPage/endCursoruntil the connection is exhausted. The query must declare a$cursor: Stringvariable and pass it asafter: $cursor, and its connection must selectpageInfo { hasNextPage endCursor }.Null nodes (which GitHub’s
searchconnection can emit) are skipped.- Parameters:
query (
str) – The GraphQL query string.connection_path (
Sequence[str]) – Keys from the response’sdataobject down to the connection (like("search",)or (“user”, “sponsorshipsAsMaintainer”)).variables (
Mapping[str,str|int|bool] |None) – Query variables. Strings are passed with-f; ints and bools with-F, so they keep their GraphQL type.page_size_var (
str) – When set, inject the page size into this query variable on every request (the query then controlsfirst:with it). Leave empty for queries with a hard-coded page size.page_size (
int) – Nodes requested per page; only used with page_size_var. The last page shrinks to the max_nodes remainder so the budget is never over-fetched.max_nodes (
int|None) – Stop after yielding this many nodes.Nonemeans every node in the connection.
- Yields:
Each node dict, in API order.
- Raises:
RuntimeError – When a
ghinvocation fails (seerun_gh_command()).
repomatic.github.issue module¶
GitHub issue lifecycle management.
Generic primitives for listing, creating, updating, closing, and triaging
GitHub issues via the gh CLI. Used by broken_links and potentially
other modules that manage bot-created issues.
We need to manually manage the life-cycle of issues created in CI jobs because the
create-issue-from-file action blindly creates issues ad-nauseam.
See: - https://github.com/peter-evans/create-issue-from-file/issues/298 - https://github.com/lycheeverse/lychee-action/issues/74#issuecomment-1587089689
- repomatic.github.issue.list_issues(title='')[source]¶
List all issues (open and closed), optionally filtered by title.
Note
No
--authorfilter is applied. WhenREPOMATIC_PATis configured,ghauthenticates as the token owner (notgithub-actions[bot]), so issues may be authored by either identity. Filtering by author would miss issues created under the other identity, breaking deduplication. The caller (triage_issues()) already matches by exact title, so author-agnostic listing is safe.
- repomatic.github.issue.triage_issues(issues, title, needed)[source]¶
Triage issues matching a title for deduplication.
- Parameters:
- Return type:
- Returns:
A tuple of (issue_needed, issue_to_update, issue_state, issues_to_close).
If
neededisTrue, the most recent matching issue is kept asissue_to_update(with itsissue_state) and all older matching issues are collected inissues_to_close. IfneededisFalse, all open matching issues are placed inissues_to_close(already-closed issues are skipped).
- repomatic.github.issue.manage_issue_lifecycle(has_issues, body_file, labels, title, no_issues_comment='No more issues.')[source]¶
Manage the full issue lifecycle: list, triage, close, create/update.
This function handles:
Listing all issues (open and closed) via
gh issue list.Triaging matching issues (keep newest if needed, close duplicates).
Closing duplicate open issues via
gh issue close.Creating, updating, or reopening the main issue via
gh issue create,gh issue edit, orgh issue reopen.
When
has_issuesisTrueand the most recent matching issue is closed, it is reopened and updated rather than creating a duplicate.- Parameters:
has_issues (
bool) – Whether issues were found that warrant an open issue.body_file (
Path) – Path to the file containing the issue body.labels (
list[str]) – Labels to apply when creating a new issue.title (
str) – Issue title to match and create.no_issues_comment (
str) – Comment to add when closing issues because the condition no longer applies.
- Return type:
repomatic.github.matrix module¶
GitHub Actions job-matrix model: variations, includes, excludes, and their
expansion into the JSON payload workflow strategy.matrix keys consume.
- class repomatic.github.matrix.Matrix(*args, **kwargs)[source]¶
Bases:
objectA matrix as defined by GitHub’s actions workflows.
See GitHub official documentation on how-to implement variations of jobs in a workflow.
Note
Why matrices are pre-computed in the
metadatajobGitHub Actions matrix outputs are not cumulative — the last job in a matrix wins (community discussion). This makes a matrix-based job terminal in a dependency graph: no downstream job can depend on its aggregated outputs.
The workaround is a single preliminary
metadatajob that computes all matrices upfront. Downstream jobs depend on that job and consume the pre-built matrices, rather than computing them themselves.This Matrix behave like a
dictand works everywhere adictwould. Only that it is immutable and based onFrozenDict. If you want to populate the matrix you have to use the following methods:The implementation respects the order in which items were inserted. This provides a natural and visual sorting that should ease the inspection and debugging of large matrix.
- matrix(ignore_includes=False, ignore_excludes=False)[source]¶
Returns a copy of the matrix.
The special
includeandexcludesdirectives will be added by default. You can selectively ignore them by passing the corresponding boolean parameters.
- replace_variation_value(variation_id, old, new)[source]¶
Replace a single value within a variation axis.
The new value takes the position of the old value. If the new value already exists elsewhere in the axis, the duplicate is removed by
boltons.iterutils.unique().Silently skips if the axis does not exist or does not contain the old value, making the operation idempotent.
- Return type:
- remove_variation_value(variation_id, value)[source]¶
Remove a single value from a variation axis.
If the axis becomes empty after removal, it is deleted entirely.
Silently skips if the axis does not exist or does not contain the value, making the operation idempotent.
- Return type:
- add_includes(*new_includes)[source]¶
Add one or more
includespecial directives to the matrix.- Return type:
- add_excludes(*new_excludes)[source]¶
Add one or more
excludespecial directives to the matrix.- Return type:
- prune()[source]¶
Remove no-op exclude directives and log about them.
An exclude is a no-op when it references a key that is not a variation axis at all, or when the key exists but the value is not present in that axis. Either way the exclude can never match any combination produced by
product(), and GitHub Actions rejects excludes that reference non-existent matrix keys.- Return type:
- all_variations(with_matrix=True, with_includes=False, with_excludes=False)[source]¶
Collect all variations encountered in the matrix.
Extra variations mentioned in the special
includeandexcludedirectives will be ignored by default.You can selectively expand or restrict the resulting inventory of variations by passing the corresponding
with_matrix,with_includesandwith_excludesboolean filter parameters.
- product(with_includes=False, with_excludes=False)[source]¶
Only returns the combinations of the base matrix by default.
You can optionally add any variation referenced in the
includeandexcludespecial directives.Respects the order of variations and their values.
- solve(strict=False)[source]¶
Expand the matrix to explicit jobs, applying
excludetheninclude.Reproduces GitHub’s documented matrix algorithm:
Build the cross-product of the base variations.
Drop every combination matching an
excludedirective. A directive matches when all of its keys equal the combination’s, so a partial directive removes a whole slice.Process
includedirectives in order. Each is merged into every product combination it does not conflict with (it conflicts when it would overwrite an original axis value). A directive merging into no combination is appended as a new standalone job.
Note
includedirectives augment combinations from the base cross-product only, never jobs created by an earlierinclude. An excluded combination is resurrected solely when anincludefully re-specifies it, so it merges into nothing and is appended: a partialincludethat augments surviving jobs does not bring excluded slices back. GitHub remains the authoritative expander, but this follows its documented rules so downstreamfull-includejob lists (whichmatrix()serializes verbatim) match what GitHub would run.
- pivot(row_axis='python-version', col_axis='os', cell_key='state', missing='—')[source]¶
Pivot the solved matrix into a 2D grid keyed by two axes.
Expands the matrix with
solve(), then arranges the resulting jobs into a grid: one row per distinctrow_axisvalue, one column per distinctcol_axisvalue. Each cell holds the job’scell_keyvalue at that intersection (itsstate, by default), ormissingwhen no job occupies it (an excluded combination).- Parameters:
- Return type:
- Returns:
A
(col_values, rows)pair.col_valuesis the ordered tuple of column values (distinctcol_axisvalues). Each entry inrowsis(row_value, cell, …), with one cell percol_valuesentry.
Note
Axis values keep first-seen order in the solved job stream. That matches the declared axis order for a base cross-product matrix, and the emitted job order for a flattened
full-includematrix.When several jobs share one (row, col) intersection (a matrix carrying extra axes, such as a
click-versionvariation), their distinctcell_keyvalues are joined with, ``. A matrix with only the ``osandpython-versionaxes has exactly one job per cell.
repomatic.github.pr module¶
GitHub pull request lifecycle helpers.
Generic primitives for querying and closing pull requests via the
gh wrapper. Used by workflows that need to
reconcile automation-managed PRs (like the bump-version job)
when their target state changes mid-flight.
- repomatic.github.pr.list_open_prs_by_branch(branch)[source]¶
List open pull requests whose head branch matches
branch.
- repomatic.github.pr.close_pr(number, comment, delete_branch=True)[source]¶
Close a pull request with a comment.
repomatic.github.pr_body module¶
Generate PR body with workflow metadata for auto-created pull requests.
Callers inject a Metadata instance for CI
context to produce a collapsible <details> block containing a metadata
table (the injection keeps this module import-cycle-free: Metadata pulls
in half the package). Template prefixes are loaded from markdown files in
repomatic/templates/, optionally with YAML frontmatter for templates that
require arguments.
Note
load_template() and the render_* helpers also accept a
Path to read a template from disk. Downstream repos can ship
project-specific templates and feed them through repomatic pr-body
–template-file path/to/template.md (paired with one or more
--template-arg KEY=VALUE entries to fill the placeholders) without forking
repomatic. External templates should set footer: false in their frontmatter
to avoid duplicating the attribution footer that already ships with the
metadata block.
Also provides two helpers for embedding externally-sourced markdown in PR or
issue bodies: sanitize_markdown_mentions() neutralizes @mentions,
#issue refs, and GitHub URLs, and demote_markdown_headings() pushes
the embedded content’s headings below the embedding document’s own sections.
- repomatic.github.pr_body.GITHUB_BODY_MAX_CHARS = 65536¶
GitHub’s maximum PR and issue body size, in UTF-16 code units.
GitHub’s API rejects longer bodies, and peter-evans/create-pull-request pre-empts the rejection by hard-truncating the end of the body (
main.ts:inputs.body.substring(0, 65536)), silently dropping whatever sits last: the refresh tip, the metadata block, and the attribution footer.build_pr_body()(PRs) andfit_issue_body()(issues) trim the leading content instead, so the tail always survives.
- repomatic.github.pr_body.sanitize_markdown_mentions(text)[source]¶
Neutralize
@mentions,#issuerefs, and GitHub URLs in markdown.Prevents GitHub from auto-linking mentions and issue references in externally-sourced markdown (upstream release notes, third-party tool output) that would cause notification spam or accidental issue closure.
Uses a placeholder extraction approach: fenced code blocks and inline code spans are temporarily replaced with unique placeholders before sanitization, then restored afterward. This avoids the fragile “sanitize then restore” pattern that caused bugs in both Dependabot (2019 code-fence regression, dependabot/dependabot-core#1421) and Renovate (ongoing restoration pass edge cases, renovatebot/renovate#8823, renovatebot/renovate#2554).
Inserts a Unicode zero-width space (U+200B) after
@and#to break GitHub’s mention and issue parsers without affecting visual rendering. Rewritesgithub.comURLs toredirect.github.comto prevent backlink cross-references on upstream issues.- Parameters:
text (
str) – Raw markdown text from an external source.- Return type:
- Returns:
Sanitized markdown safe for embedding in a GitHub PR or issue body.
Note
Only call this on externally-sourced content (upstream release notes, third-party tool output). Do not call on content authored by the repository owner where mentions are intentional.
- repomatic.github.pr_body.demote_markdown_headings(text, floor)[source]¶
Demote ATX headings so the shallowest one lands at level floor.
Externally-sourced markdown (upstream release notes) carries its own
#and##headings, which GitHub renders at full size even inside a<details>block, so they compete with the embedding document’s section hierarchy. All headings are shifted deeper by the uniform offset that puts the shallowest at floor, preserving the body’s internal structure; levels past######(h6, markdown’s deepest) are clamped. Headings already at or below floor are left alone: this function never promotes.Fenced code blocks are shielded with the same placeholder extraction as
sanitize_markdown_mentions(), so# commentsin shell samples survive. Only ATX headings are rewritten: setext headings (underlined with===or---), rare in release notes, pass through unchanged.
- repomatic.github.pr_body.load_template(name)[source]¶
Load a PR body template by name or filesystem path.
Dispatch is type-based:
str(e.g."bump-version"): looked up as a packaged resource underrepomatic.templates. Tries{name}.md.noformatfirst, then{name}.md. The.md.noformatextension is used for templates whosestring.Templateplaceholders confuse mdformat (e.g.$rerun_entryprefixed to a list line is parsed as literal text, breaking the list structure). Seepr-metadata.md.noformatfor the canonical example.Path: read directly from the filesystem. Lets downstream repos ship project-specific templates without forking repomatic.
- repomatic.github.pr_body.render_template(*names, **kwargs)[source]¶
Load and render one or more templates with variable substitution.
When multiple template names are given, each is rendered and joined with a blank line. The
generated-footerattribution is appended once at the end if any of the templates wants it (i.e. does not havefooter: falsein its frontmatter).Static templates (no
$variableplaceholders) are returned as-is. Dynamic templates usestring.Template($variablesyntax) to avoid conflicts with markdown braces like[tool.repomatic].Consecutive blank lines left by empty variables are collapsed to a single blank line.
- repomatic.github.pr_body.render_title(name, **kwargs)[source]¶
Load and render a template’s PR title with variable substitution.
- repomatic.github.pr_body.render_commit_message(name, **kwargs)[source]¶
Load and render a template’s commit message with variable substitution.
Falls back to the
titleif nocommit_messageis defined, and to an empty string if neither is set (templates without a title or commit message render only their body).- Parameters:
- Return type:
- Returns:
The rendered commit message string, or an empty string when the template defines neither
commit_messagenortitle.
- repomatic.github.pr_body.template_args(name)[source]¶
Return the list of required arguments for a template.
- repomatic.github.pr_body.template_docs_url(name)[source]¶
Return a template’s documentation deep link, if it declares one.
PR templates carry a
docs:frontmatter field pointing at their job’s section of the hosted workflows reference, surfaced as theDocumentationentry of the metadata block now that PR bodies have no description section.
- repomatic.github.pr_body.get_template_names()[source]¶
Discover all available template names from the templates package.
- repomatic.github.pr_body.extract_workflow_filename(workflow_ref)[source]¶
Extract the workflow filename from
GITHUB_WORKFLOW_REF.
- repomatic.github.pr_body.generate_pr_metadata_block(md, docs_url='', docs_name='')[source]¶
Generate a collapsible metadata block from CI context.
Reads the
GITHUB_*environment context from md and returns a markdown<details>block listing the workflow metadata fields.- Parameters:
md (
Metadata) – TheMetadatainstance to read CI context from.docs_url (
str) – Optional deep link to the job’s section of the hosted workflows reference, rendered as the leadingDocumentationentry. Comes from the PR template’sdocs:frontmatter field (seetemplate_docs_url()).docs_name (
str) – Label for the documentation link, usually the template (operation) name. Without it the raw URL renders as an autolink.
- Return type:
- Returns:
A markdown string with the metadata block.
- repomatic.github.pr_body.generate_refresh_tip(md)[source]¶
Generate a tip admonition inviting users to refresh the PR manually.
Reads the repository URL and
GITHUB_WORKFLOW_REFfrom md to build the workflow dispatch URL.
- repomatic.github.pr_body.build_pr_body(prefix, metadata_block, refresh_tip='')[source]¶
Concatenate prefix, refresh tip, and metadata block into a PR body.
The
metadata_blockalready includes the attribution footer (appended automatically byrender_template()); the refresh_tip comes pre-rendered fromgenerate_refresh_tip()(empty to omit it).Bodies over
GITHUB_BODY_MAX_CHARShave their prefix trimmed to fit, replacing the dropped lines with a caution admonition, so the refresh tip, metadata block, and attribution footer always survive. Left alone, GitHub-side truncation would chop the body from the end instead.- Parameters:
prefix (
str) – Content to prepend before the metadata block. Can be empty.metadata_block (
str) – The collapsible metadata block fromgenerate_pr_metadata_block(), with footer.refresh_tip (
str) – Pre-rendered refresh-tip admonition, or empty.
- Return type:
- Returns:
The complete PR body string.
- repomatic.github.pr_body.fit_issue_body(body)[source]¶
Trim an oversized issue body, keeping the attribution footer.
The
build_pr_body()counterpart for issue bodies rendered straight from a footer-carrying template (broken-links report, setup guide). The GitHub API rejects oversized issue bodies outright (gh issue createandgh issue editfail), so the content above the attribution footer is trimmed on line boundaries, with a caution admonition marking the cut.
repomatic.github.release_sync module¶
Sync GitHub release notes from changelog.md.
Compares each GitHub release body against the corresponding
changelog.md section and updates any that have drifted.
changelog.md is the single source of truth.
- class repomatic.github.release_sync.SyncAction(*values)[source]¶
Bases:
EnumAction taken (or to be taken) on a release body.
- DRY_RUN = 'dry_run'¶
- FAILED = 'failed'¶
- SKIPPED = 'skipped'¶
- UPDATED = 'updated'¶
- class repomatic.github.release_sync.SyncRow(action, version, release_url)[source]¶
Bases:
objectPer-release detail for the markdown report table.
- action: SyncAction¶
- class repomatic.github.release_sync.SyncResult(dry_run=True, rows=<factory>, total=0, in_sync=0, drifted=0, updated=0, failed=0, missing_changelog=0)[source]¶
Bases:
objectAccumulated results from a release-notes sync run.
- repomatic.github.release_sync.sync_github_releases(repo_url, changelog_path, dry_run=True)[source]¶
Sync GitHub release bodies from
changelog.md.For each released version in the changelog, compares the expected body (from
changelog.md) with the actual GitHub release body. In live mode, updates drifted releases viagh release edit.- Parameters:
- Return type:
- Returns:
Structured sync results.
- repomatic.github.release_sync.render_sync_report(result)[source]¶
Render a markdown report from sync results.
- Parameters:
result (
SyncResult) – Structured results from the sync run.- Return type:
- Returns:
Markdown report string.
repomatic.github.releases module¶
GitHub Releases API client.
The single home for reading GitHub Releases: raw cached API access (tags,
versions, single bodies), tag-to-version extraction, tag-to-SHA resolution, and
the range-to-release-notes fetch shared by the dependency updaters. The
repomatic.version_sync adapters and repomatic.uv release-notes
helper build on top of these reads.
- repomatic.github.releases.GITHUB_API_RELEASES_URL = 'https://api.github.com/repos/{owner}/{repo}/releases'¶
GitHub API URL for fetching all releases for a repository.
- repomatic.github.releases.GITHUB_API_TAG_REF_URL = 'https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag}'¶
GitHub API URL for resolving a tag name to its git object.
- repomatic.github.releases.GITHUB_API_TAG_OBJECT_URL = 'https://api.github.com/repos/{owner}/{repo}/git/tags/{sha}'¶
GitHub API URL for dereferencing an annotated tag object to its commit.
- repomatic.github.releases.GITHUB_API_RELEASE_BY_TAG_URL = 'https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}'¶
GitHub API URL for fetching a single release by tag name.
- repomatic.github.releases.owner_repo(repo_url)[source]¶
Extract
(owner, repo)from a GitHub repository URL.
Bases:
RuntimeErrorRaised when the GitHub Releases API call could not complete cleanly.
Signals a transient failure (network error, timeout, JSON parse error, or pagination breaking mid-stream) where the result cannot safely be treated as “no releases.”
Callers that drive destructive operations (rewriting the changelog, deleting tags, etc.) must catch this and refuse to act, rather than silently rewriting state with a corrupted or empty view of release history.
- class repomatic.github.releases.GitHubRelease(date: str, body: str)[source]¶
Bases:
NamedTupleRelease metadata for a single version from GitHub.
Create new instance of GitHubRelease(date, body)
- class repomatic.github.releases.ReleaseAsset(name: str, size: int, sha256: str, download_url: str)[source]¶
Bases:
NamedTupleA single downloadable asset attached to a GitHub release.
Create new instance of ReleaseAsset(name, size, sha256, download_url)
- class repomatic.github.releases.ReleaseWithAssets(tag: str, date: str, draft: bool, prerelease: bool, assets: tuple[ReleaseAsset, ...], body: str = '')[source]¶
Bases:
NamedTupleFull release metadata including its assets and visibility flags.
Create new instance of ReleaseWithAssets(tag, date, draft, prerelease, assets, body)
- assets: tuple[ReleaseAsset, ...]¶
Assets attached to the release, in API order.
- repomatic.github.releases.get_github_releases(repo_url)[source]¶
Get versions and dates for all GitHub releases.
Fetches all releases via the GitHub API with pagination. Extracts version numbers by stripping the
vprefix from tag names. Usespublished_at(falling back tocreated_at) for the date.- Parameters:
repo_url (
str) – Repository URL (e.g.https://github.com/user/repo).- Return type:
- Returns:
Dict mapping version strings to
GitHubReleasetuples. Empty dict only when the repository genuinely has no releases (the API returned an empty page) or whenrepo_urldoes not parse to anowner/repopair.- Raises:
GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON. An empty return value from this function means “the repo has no releases”; a raised exception means “we don’t know.”
- repomatic.github.releases.get_release_tags(repo_url)[source]¶
Get all releases keyed by their raw, unstripped tag name.
get_github_releases()keeps onlyv-prefixed tags (and strips thev), which drops tools whose release tags use another scheme (lychee’slychee-v…, biome’s@biomejs/biome@…).sync-tool-versionsandsync-action-pinsneed every tag, so the version can be extracted with a per-tool pattern.- Parameters:
repo_url (
str) – Repository URL.- Return type:
- Returns:
Dict mapping raw tag names to
GitHubReleasetuples. Empty only when the repository has no releases orrepo_urldoes not parse to anowner/repopair.- Raises:
GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON.
- repomatic.github.releases.get_releases_with_assets(repo_url)[source]¶
Get every release with its assets, visibility flags, and digests.
Deliberately uncached, unlike
get_github_releases(): the main consumer issync-binaries, which runs minutes after a release is published and must see the assets that were just uploaded. A cached view would regenerate the binaries page from a pre-release snapshot.- Parameters:
repo_url (
str) – Repository URL (e.g.https://github.com/user/repo).- Return type:
- Returns:
One
ReleaseWithAssetsper release (drafts and pre-releases included, for the caller to filter), in API order (newest first). Empty when the repository has no releases or repo_url does not parse to anowner/repopair.- Raises:
GitHubReleasesUnavailable – When any page fetch fails or returns unparsable JSON.
- repomatic.github.releases.resolve_tag_to_sha(repo_url, tag)[source]¶
Resolve a release tag to its 40-character commit SHA.
Reads the tag’s git reference. An annotated tag points at an intermediate tag object, dereferenced one hop to the commit it targets; a lightweight tag points straight at the commit.
- repomatic.github.releases.extract_version(tag, tag_pattern)[source]¶
Extract a version from a GitHub release tag.
- repomatic.github.releases.get_github_release_body(repo_url, version)[source]¶
Fetch the release notes body for a specific version from GitHub.
Tries
v:version:first (most common for Python packages), then the bare{version}tag.
- repomatic.github.releases.fetch_github_release_notes(items)[source]¶
Fetch GitHub release notes for a batch of version bumps.
For each item, lists the repository’s releases (a cached call, already warm from a prior candidate sweep) and keeps those whose extracted version lands in the half-open range
(old, new], oldest first. Non-GitHub datasources (npm, PyPI workflow literals) contribute no item here and render no notes.- Parameters:
items (
list[tuple[str,str,str,str,str|None]]) – One(name, repo_url, old, new, tag_pattern)tuple per bumped pin, where old and new are bare versions and tag_pattern is the per-tool extraction regex (orNonefor thevX.Y.Zscheme).- Return type:
- Returns:
A dict mapping names to
(repo_url, versions)tuples, the same shaperepomatic.uv.fetch_release_notes()returns, sorepomatic.uv.format_release_notes()renders it unchanged. Only entries with at least one non-empty release body are included.
repomatic.github.sponsor module¶
Check if a GitHub user is a sponsor of another user or organization.
Uses the GitHub GraphQL API via the gh CLI to query sponsorship data.
Supports both user and organization owners, with pagination for accounts
that have more than 100 sponsors.
When run in GitHub Actions, defaults are read from
Metadata for owner and repository, and from
GITHUB_EVENT_PATH for the author and issue/PR number.
- repomatic.github.sponsor.get_default_owner()[source]¶
Get the repository owner from CI context.
Delegates to
Metadata.repo_owner.
- repomatic.github.sponsor.get_default_author()[source]¶
Get the issue/PR author from the GitHub event payload.
- repomatic.github.sponsor.get_default_number()[source]¶
Get the issue/PR number from the GitHub event payload.
- repomatic.github.sponsor.is_pull_request()[source]¶
Check if the current event is a pull request.
- Return type:
- repomatic.github.sponsor.get_sponsors(owner: str) frozenset[str][source]¶
Get all sponsors for a user or organization.
Tries the user query first, then falls back to organization query.
Results are cached to avoid redundant API calls within the same process.
repomatic.github.status module¶
Probe githubstatus.com on API failures.
When a gh or REST call fails with an opaque error, callers can ask this
module whether GitHub is reporting a live incident. The status page is the
source of truth for outages affecting authentication, the REST API, and
Actions, so surfacing it in error messages saves operators from chasing
PAT scopes that aren’t actually broken.
The HTTP probe is memoized for the lifetime of the process: a single CLI
invocation that fails ten gh calls in a row hits the status endpoint
once. Failures (DNS, timeout, JSON parse) collapse to None so the
probe itself never masks the original error.
- repomatic.github.status.GITHUB_STATUS_SUMMARY_URL = 'https://www.githubstatus.com/api/v2/status.json'¶
Status summary endpoint exposed by Statuspage.
Returns a JSON document with a top-level
statusobject containing anindicator(none,minor,major,critical,maintenance) and a human-readabledescription.
- class repomatic.github.status.GitHubStatus(indicator, description)[source]¶
Bases:
objectSnapshot of the githubstatus.com summary.
- Parameters:
- repomatic.github.status.get_github_status() GitHubStatus | None[source]¶
Fetch the current githubstatus.com summary.
Memoized for the lifetime of the process: only the first call hits the network. Returns
Nonewhen the probe cannot complete cleanly (network error, timeout, malformed JSON, missing fields), so callers can treat the probe as best-effort and never let it mask the underlying error they were trying to annotate.- Return type:
- repomatic.github.status.status_annotation()[source]¶
Return a one-line incident annotation, or empty string when healthy.
Convenience wrapper around
get_github_status()for the common case where callers want to append a string to an error message without branching onNone.- Return type:
repomatic.github.token module¶
GitHub token validation utilities.
Provides early validation for CLI commands that depend on the GitHub API, so users get clear error messages at startup rather than opaque failures mid-execution.
Note
Why REPOMATIC_PAT is needed
GitHub’s GITHUB_TOKEN cannot modify workflow files in .github/.
Neither contents: write, actions: write, nor permissions:
write-all grant this ability. The only way to push changes to workflow
YAML files is via a fine-grained Personal Access Token with the
Workflows permission. Without it, pushes are rejected with:
! [remote rejected] branch_xxx -> branch_xxx (refusing to allow a
GitHub App to create or update workflow
``.github/workflows/my_workflow.yaml`` without ``workflows`` permission)
Additionally, events triggered by GITHUB_TOKEN do not start new
workflow runs (see GitHub docs),
so tag pushes also need the PAT to trigger downstream workflows.
The Settings → Actions → General → Workflow permissions setting has no effect on this limitation — it’s a hard security boundary enforced by GitHub regardless of repository-level settings.
Jobs that use REPOMATIC_PAT:
autofix.yaml: fix-typos, sync-repomatic, sync-action-pins, sync-workflow-pins (PRs touching.github/workflows/files), sync-tool-versions (upstream-only dependency PRs), fix-vulnerable-deps (reads the GitHub Advisory Database).changelog.yaml: prepare-release (freezes versions in workflow files).release.yaml: create-tag (push triggerson.push.tags), create-release (triggers downstream workflows).
All jobs fall back to GITHUB_TOKEN when the PAT is unavailable
(secrets.REPOMATIC_PAT || github.token), but
operations requiring the workflows permission or workflow triggering
will silently fail.
Token permission mapping:
Workflows — PRs that touch
.github/workflows/files.Contents — Tag pushes, release publishing, PR branch creation.
Pull requests — All PR-creating jobs.
Dependabot alerts — fix-vulnerable-deps reads vulnerability alerts.
Issues — Setup guide issue.
Metadata — Required for all fine-grained token API operations.
- class repomatic.github.token.PatProbe(field: str, permission: str, endpoint: str, success: str, not_found: str = '')[source]¶
Bases:
NamedTupleOne fine-grained PAT permission probe.
A read-only API call whose
HTTP 403unambiguously identifies the missing fine-grained permission. Rows live inPAT_PERMISSION_PROBES.Create new instance of PatProbe(field, permission, endpoint, success, not_found)
- field: str¶
The
PatPermissionResultsfield receiving this probe’s result.
- repomatic.github.token.PAT_PERMISSION_PROBES: tuple[PatProbe, ...] = (('contents', 'Contents: Read and Write', 'repos/{repo}/contents/.github', 'Contents: token has access', ''), ('issues', 'Issues: Read and Write', 'repos/{repo}/issues?per_page=1&state=all', 'Issues: token has access', ''), ('pull_requests', 'Pull requests: Read and Write', 'repos/{repo}/pulls?per_page=1&state=all', 'Pull requests: token has access', ''), ('vulnerability_alerts', 'Dependabot alerts: Read-only', 'repos/{repo}/dependabot/alerts?per_page=1', 'Dependabot alerts: token has access, alerts enabled', 'Vulnerability alerts are not enabled on the repository. Enable them: gh api repos/{repo}/vulnerability-alerts --method PUT'), ('workflows', 'Workflows: Read and Write', 'repos/{repo}/actions/workflows?per_page=1', 'Workflows: token has access', ''))¶
The PAT permission probes, one per
PatPermissionResultsfield.
- repomatic.github.token.probe_pat_permission(repo, probe)[source]¶
Run one PAT permission probe against repo.
- class repomatic.github.token.PatPermissionResults(contents, issues, pull_requests, vulnerability_alerts, workflows)[source]¶
Bases:
objectResults of all PAT permission checks.
Each field holds a
(passed, message)tuple from the correspondingcheck_pat_*function.- contents: tuple[bool, str]¶
Result of the
contentsPAT_PERMISSION_PROBESrow.
- issues: tuple[bool, str]¶
Result of the
issuesPAT_PERMISSION_PROBESrow.
- pull_requests: tuple[bool, str]¶
Result of the
pull_requestsPAT_PERMISSION_PROBESrow.
- vulnerability_alerts: tuple[bool, str]¶
Result of the
vulnerability_alertsPAT_PERMISSION_PROBESrow.
- workflows: tuple[bool, str]¶
Result of the
workflowsPAT_PERMISSION_PROBESrow.
- repomatic.github.token.check_all_pat_permissions(repo)[source]¶
Run all PAT permission checks and return structured results.
This is the single entry point for PAT permission validation. Both
lint-repoandsetup-guidecall this function so that adding a new permission check benefits all consumers automatically.- Parameters:
repo (
str) – Repository in ‘owner/repo’ format.- Return type:
- Returns:
PatPermissionResultswith all check outcomes.
- repomatic.github.token.validate_gh_token_env()[source]¶
Check that a GitHub token environment variable is set.
Lookup order:
REPOMATIC_PAT>GH_TOKEN>GITHUB_TOKEN, matchingrun_gh_command.- Raises:
RuntimeError – If no variable is set.
- Return type:
- repomatic.github.token.validate_gh_api_access()[source]¶
Smoke-test the GitHub API and return parsed response.
Calls
GET https://api.github.com/rate_limitwith the token from environment variables.
- repomatic.github.token.validate_classic_pat_scope(required_scope)[source]¶
Validate that the GitHub token is a classic PAT with the required scope.
Checks:
A GitHub token environment variable is set.
GitHub API is reachable (smoke-test GET).
Token is a classic PAT (has
X-OAuth-Scopesheader).Token has the required scope.
- Parameters:
required_scope (
str) – The OAuth scope to require (e.g."notifications").- Return type:
- Returns:
The full list of scopes on the token.
- Raises:
RuntimeError – If any check fails.
repomatic.github.unsubscribe module¶
Unsubscribe from closed, inactive GitHub notification threads.
Processes notification threads in two phases:
REST notification threads — Fetches all Issue/PullRequest notification threads via
/notifications, inspects each for closed + stale status, and unsubscribes viaDELETE+PATCH.GraphQL threadless subscriptions — Searches for closed issues/PRs the user is involved in but that lack notification threads, and unsubscribes via the
updateSubscriptionmutation.
Requires the gh CLI to be installed and authenticated with a token that
has the notifications scope (classic PAT) or equivalent fine-grained
permissions.
- repomatic.github.unsubscribe.GRAPHQL_PAGE_SIZE = 25¶
Per-page count for GraphQL search results.
- repomatic.github.unsubscribe.NOTIFICATION_PAGE_SIZE = 50¶
Per-page count for REST
/notificationsresults.
- repomatic.github.unsubscribe.NOTIFICATION_SUBJECT_TYPES = frozenset({'Issue', 'PullRequest'})¶
Notification subject types to process.
- class repomatic.github.unsubscribe.ItemAction(*values)[source]¶
Bases:
EnumAction taken (or to be taken) on a notification item.
- DRY_RUN = 'dry_run'¶
- FAILED = 'failed'¶
- UNSUBSCRIBED = 'unsubscribed'¶
- class repomatic.github.unsubscribe.DetailRow(action, html_url, number, repo, title, updated_at)[source]¶
Bases:
objectPer-item detail for the markdown report table.
- action: ItemAction¶
- class repomatic.github.unsubscribe.Phase1Result(batch_size=0, cutoff=None, newest_updated=None, oldest_updated=None, rows=<factory>, threads_failed=0, threads_inspected=0, threads_skipped_open=0, threads_skipped_recent=0, threads_skipped_unknown=0, threads_total=0, threads_unsubscribed=0)[source]¶
Bases:
objectAccumulated counts and details from REST notification phase.
- class repomatic.github.unsubscribe.Phase2Result(batch_size=0, cutoff=None, graphql_failed=0, graphql_not_subscribed=0, graphql_skipped_recent=0, graphql_total=0, graphql_unsubscribed=0, rows=<factory>, search_query='', skipped=False, skip_reason='')[source]¶
Bases:
objectAccumulated counts and details from GraphQL threadless phase.
- class repomatic.github.unsubscribe.UnsubscribeResult(dry_run=False, months=3, phase1=<factory>, phase2=<factory>)[source]¶
Bases:
objectAccumulated results from both unsubscribe phases.
- phase1: Phase1Result¶
- phase2: Phase2Result¶
- repomatic.github.unsubscribe.render_report(result)[source]¶
Render a markdown report from unsubscribe results.
Pure function that produces the same markdown structure as the downstream
unsubscribe.yamlworkflow’s$GITHUB_STEP_SUMMARY.- Parameters:
result (
UnsubscribeResult) – Structured results from both phases.- Return type:
- Returns:
Markdown report string.
- repomatic.github.unsubscribe.unsubscribe_threads(months, batch_size, dry_run)[source]¶
Unsubscribe from closed, inactive notification threads.
Runs two phases:
REST notification threads — Fetches notification threads, inspects each subject for closed + stale status, and unsubscribes.
GraphQL threadless subscriptions — Searches for closed issues/PRs the user is involved in and unsubscribes via mutation.
- Parameters:
- Return type:
- Returns:
Structured results from both phases.
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.
See WorkflowFormat for available output formats and their behavior.
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.
- class repomatic.github.workflow_sync.WorkflowFormat(*values)[source]¶
Bases:
StrEnumOutput format for generated workflow files.
- FULL_COPY = 'full-copy'¶
Verbatim copy of the canonical workflow file.
Creates or overwrites the target with the full upstream content. Useful for workflows that need no downstream customization.
- HEADER_ONLY = 'header-only'¶
Sync only the header (
name,on,concurrency) from upstream.Replaces everything before the
jobs:line in an existing downstream file with the canonical header. The downstreamjobs:section is preserved. Requires the target file to already exist; does not create new files.
- SYMLINK = 'symlink'¶
Create a symbolic link to the canonical workflow file.
Creates or overwrites the target as a symlink pointing to the upstream workflow in the bundled data directory.
- THIN_CALLER = 'thin-caller'¶
Generate a minimal caller that delegates to the reusable upstream workflow.
Creates or overwrites the target with a lightweight workflow containing only
name,ontriggers, and ajobs:section that calls the upstream workflow viaworkflow_call. Only works for reusable workflows (those with aworkflow_calltrigger).When the target file already exists and contains extra jobs beyond the managed caller job, those jobs are preserved and appended after the regenerated content.
- default_names()[source]¶
The workflow set a bare
createorsynctargets in this format.Thin callers cover every reusable workflow, header-only syncs cover the non-reusable ones, and the copy modes cover everything.
- write_workflow(filename, target, *, repo, version, spec, commit_sha)[source]¶
Write target in this format from the canonical filename.
Benign skips (a non-reusable workflow in thin-caller mode, a missing downstream file in header-only mode) log a warning and count as success; failures log an error.
- Parameters:
filename (
str) – Canonical workflow filename (e.g.tests.yaml).target (
Path) – Destination path to write.repo (
str) – Upstream repository for thin-calleruses:refs.version (
str) – Version reference for thin-calleruses:refs.spec (
PathsSpec) – Paths-adaptation spec for thin callers and headers.commit_sha (
str|None) – Full commit SHA for SHA-pinneduses:refs.
- Return type:
- Returns:
Falsewhen the file could not be written,Trueotherwise.
- 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.extract_trigger_info(filename)[source]¶
Extract trigger information from a bundled canonical workflow.
Parses the workflow YAML and separates
workflow_callconfiguration from other triggers.- 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)[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. :return: Complete YAML content for the thin caller workflow. :raises ValueError: If the workflow does not supportworkflow_call.
- Return type:
- 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.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.check_concurrency_match(workflow_path, canonical_filename)[source]¶
Check that a thin caller’s concurrency block matches the canonical workflow.
Compares parsed concurrency dicts so formatting differences are ignored.
- 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.
- repomatic.github.workflow_sync.generate_workflows(names, output_format, version, repo, output_dir, overwrite, commit_sha=None, paths_spec=None)[source]¶
Generate workflow files in the specified format.
Shared logic for the
createandsyncsubcommands.- Parameters:
names (
tuple[str,...]) – Workflow filenames to generate. Empty tuple means all.output_format (
WorkflowFormat) – SeeWorkflowFormatfor available formats.version (
str) – Version reference for thin callers.repo (
str) – Upstream repository.output_dir (
Path) – Directory to write files to.overwrite (
bool) – Whether to overwrite existing files.commit_sha (
str|None) – Full 40-character commit SHA for SHA-pinneduses:references. Passed through togenerate_thin_caller().paths_spec (
PathsSpec|None) – Full paths-adaptation spec; defaults to no adaptation.
- Return type:
- Returns:
Exit code (0 for success, 1 for errors).