@@ -101,8 +101,26 @@ def _substitute(text: str, kwargs: dict[str, str]) -> str:
101101 return text
102102
103103
104- def render_template (name : str , ** kwargs : str ) -> str :
105- """Load and render a PR body template with variable substitution.
104+ def _render_single (name : str , kwargs : dict [str , str ]) -> tuple [str , bool ]:
105+ """Render a single template and return its body with footer preference.
106+
107+ :param name: Template name without ``.md`` extension.
108+ :param kwargs: Variables to substitute into the template.
109+ :return: A tuple of (rendered body, wants_footer).
110+ """
111+ meta , body = load_template (name )
112+ result = _substitute (body , kwargs ).strip ()
113+ wants_footer = meta .get ("footer" ) != "false" and name != "generated-footer"
114+ return result , wants_footer
115+
116+
117+ def render_template (* names : str , ** kwargs : str ) -> str :
118+ """Load and render one or more templates with variable substitution.
119+
120+ When multiple template names are given, each is rendered and joined with
121+ a blank line. The ``generated-footer`` attribution is appended
122+ once at the end if **any** of the templates wants it (i.e. does not have
123+ ``footer: false`` in its frontmatter).
106124
107125 Static templates (no ``$variable`` placeholders) are returned as-is.
108126 Dynamic templates use ``string.Template`` (``$variable`` syntax) to avoid
@@ -111,15 +129,24 @@ def render_template(name: str, **kwargs: str) -> str:
111129 Consecutive blank lines left by empty variables are collapsed to a single
112130 blank line.
113131
114- :param name: Template name without ``.md`` extension.
115- :param kwargs: Variables to substitute into the template .
132+ :param names: One or more template names without ``.md`` extension.
133+ :param kwargs: Variables to substitute into all templates .
116134 :return: The rendered markdown string.
117135 """
118- _meta , body = load_template (name )
119- result = _substitute (body , kwargs )
120- # Collapse runs of 3+ newlines (from empty variable substitutions) into
121- # a single blank line, and strip leading/trailing whitespace.
122- return re .sub (r"\n{3,}" , "\n \n " , result ).strip ()
136+ parts = []
137+ append_footer = False
138+ for name in names :
139+ body , wants_footer = _render_single (name , kwargs )
140+ parts .append (body )
141+ if wants_footer :
142+ append_footer = True
143+ result = "\n \n " .join (parts )
144+ if append_footer :
145+ result += "\n \n " + generated_footer ()
146+ result = re .sub (r"\n{3,}" , "\n \n " , result )
147+ if append_footer :
148+ result += "\n "
149+ return result
123150
124151
125152def render_title (name : str , ** kwargs : str ) -> str :
@@ -243,7 +270,7 @@ def generate_pr_metadata_block() -> str:
243270 table_format = TableFormat .GITHUB ,
244271 )
245272
246- return render_template ("generated-footer " , table = table )
273+ return render_template ("pr-metadata " , table = table )
247274
248275
249276def _repo_url () -> str :
@@ -274,13 +301,23 @@ def generate_refresh_tip() -> str:
274301 return render_template ("refresh-tip" , workflow_dispatch_url = workflow_dispatch_url )
275302
276303
304+ def generated_footer () -> str :
305+ """Render the attribution footer from the ``generated-footer`` template.
306+
307+ Single source of truth for the attribution line appended to all PR and
308+ issue bodies produced by repomatic.
309+
310+ :return: A markdown string with a horizontal rule and the attribution line.
311+ """
312+ return "---\n \n " + render_template ("generated-footer" )
313+
314+
277315def build_pr_body (prefix : str , metadata_block : str ) -> str :
278- """Concatenate prefix, refresh tip, and metadata footer into a PR body.
316+ """Concatenate prefix, refresh tip, metadata block, and footer into a PR body.
279317
280318 :param prefix: Content to prepend before the metadata block. Can be empty.
281- :param metadata_block: The generated footer from
282- :func:`generate_pr_metadata_block`, which includes the collapsible
283- metadata table and the attribution line.
319+ :param metadata_block: The collapsible metadata block from
320+ :func:`generate_pr_metadata_block`.
284321 :return: The complete PR body string.
285322 """
286323 parts : list [str ] = []
@@ -290,4 +327,5 @@ def build_pr_body(prefix: str, metadata_block: str) -> str:
290327 if tip :
291328 parts .append (tip )
292329 parts .append (metadata_block )
330+ parts .append (generated_footer ())
293331 return "\n \n \n " .join (parts )
0 commit comments