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 outright, so an oversized body has to be trimmed before it is posted. The obvious trim is the wrong one: cutting the end (what a plain
body[:65536]does, and what peter-evans/create-pull-request did beforerepomatic.github.prreplaced it) drops whatever sits last, which here is the refresh tip, the metadata block and the attribution footer: the navigational parts a reader needs most when a report is too long to read.build_pr_body()(PRs) andfit_github_body()(issues) therefore 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, cached for the process (see_load_bundled_template()). 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, never cached, so a downstream repo iterating on a project-specific template sees each edit.
- 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.
Note
The footer’s version is
__version__as read from the working tree, which makes it cosmetically wrong on the version-machinery PRs. That is accepted rather than fixed. Upstream runs the CLI from its own checkout (LOCAL_CLI_INVOCATION), and both thebump-versionandprepare-releasejobs rewrite__version__withbump-my-versionbefore this renders, so each of those bodies advertises the version its own PR produces (theminorormajorbump target, or the post-release patch bump) instead of the identical code that rendered it. Both fixes cost more than the tag is worth: rendering the body before the bump means splittingpr-bodyback out ofpr-sync, and feeding the pre-bump version in means a CLI option that exists only to work around step ordering. Downstream never sees it, since a frozen workflow runsuvx 'repomatic==X.Y.Z'and takes its version from the installed distribution.
- 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_labels(name)[source]¶
Return the labels a template’s pull request should carry.
Read from the
labels:frontmatter key, accepting a YAML list or a single string. The template is the one place that knows which operation it fronts, so its labels live beside its title and commit message rather than being repeated in every workflow step that opens the PR.
- repomatic.github.pr_body.template_draft(name)[source]¶
Return whether a template’s pull request should be held in draft.
Read from the
draft:frontmatter key. Both the YAML boolean and its quoted spelling are honored, mirroring thefooter:key.
- 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.template_stem(filename)[source]¶
Return a template’s name, shorn of its
.mdor.md.noformatextension.The one place that knows how template filenames decompose:
.md.noformatfiles are renamed.mdfiles hidden from mdformat (seeload_template()), so both extensions strip down to the same name. Callers derive a PR branch or a documentation label from a--template-filepath with it, andget_template_names()names the bundled templates through it.
- repomatic.github.pr_body.get_template_names()[source]¶
Discover all available template names from the templates package.
- 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_release_review_steps(md, version)[source]¶
Build the two optional review steps of the prepare-release checklist.
The
How-to releaselist opens with two review steps that render only when their GitHub data is reachable, so the checklist degrades to the bare merge instructions offline (or when the dev pre-release is disabled):Dev pre-release review: links the rolling
v{version}.dev0draft through itshtml_url(drafts have no public tag URL). Omitted when no such draft is visible.Full-changes review: links the
v{previous}...maincomparison. Omitted when no prior release exists to compare against.
Both come from a single
dev_release_url_and_previous_version()lookup. Each returned string is a complete ordered-list line ending in a newline (or empty), written with a1.marker so the surrounding lazily numbered list renumbers correctly however many steps survive.
- 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_github_body(body)[source]¶
Trim an oversized issue or pull-request body, keeping the footer.
The
build_pr_body()counterpart for bodies rendered straight from a footer-carrying template (broken-links report, setup guide), and the safety netupsert_pr()runs over an explicit--bodythat never went throughbuild_pr_body(). The GitHub API rejects oversized bodies outright (gh issue create,gh issue editandgh pr createall fail), so the content above the attribution footer is trimmed on line boundaries, with a caution admonition marking the cut.
- repomatic.github.pr_body.temp_body_file(body)[source]¶
Materialize a rendered body as a temporary file, then remove it.
The
ghCLI takes a body only through--body-file, so every write path against an issue or a pull request needs one on disk. Owning the temp file at this layer keeps callers working in the currency they actually produce (rendered markdown) instead of each repeating the same write /try/unlinkenvelope.