This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 040e69c  Migrate template variables from brackets to double braces
040e69c is described below

commit 040e69c87c47b30e18cc7da46cc4440eed8804f6
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Dec 31 17:29:59 2025 +0000

    Migrate template variables from brackets to double braces
---
 atr/construct.py                                |  54 +++++------
 atr/get/projects.py                             |   4 +-
 atr/models/sql.py                               |  24 ++---
 atr/render.py                                   |   4 +-
 migrations/versions/0033_2025.12.31_f2d97d96.py | 113 ++++++++++++++++++++++++
 5 files changed, 156 insertions(+), 43 deletions(-)

diff --git a/atr/construct.py b/atr/construct.py
index 3c86c08..2066ffc 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -99,14 +99,14 @@ async def announce_release_body(body: str, options: 
AnnounceReleaseOptions) -> s
     download_url = f"https://{host}{download_path}";
 
     # Perform substitutions in the body
-    body = body.replace("[COMMITTEE]", committee.display_name)
-    body = body.replace("[DOWNLOAD_URL]", download_url)
-    body = body.replace("[PROJECT]", options.project_name)
-    body = body.replace("[REVISION]", revision_number)
-    body = body.replace("[TAG]", revision_tag)
-    body = body.replace("[VERSION]", options.version_name)
-    body = body.replace("[YOUR_ASF_ID]", options.asfuid)
-    body = body.replace("[YOUR_FULL_NAME]", options.fullname)
+    body = body.replace("{{COMMITTEE}}", committee.display_name)
+    body = body.replace("{{DOWNLOAD_URL}}", download_url)
+    body = body.replace("{{PROJECT}}", options.project_name)
+    body = body.replace("{{REVISION}}", revision_number)
+    body = body.replace("{{TAG}}", revision_tag)
+    body = body.replace("{{VERSION}}", options.version_name)
+    body = body.replace("{{YOUR_ASF_ID}}", options.asfuid)
+    body = body.replace("{{YOUR_FULL_NAME}}", options.fullname)
 
     return body
 
@@ -143,12 +143,12 @@ def checklist_body(
     review_path = util.as_url(vote.selected, project_name=project.name, 
version_name=version_name)
     review_url = f"https://{host}{review_path}";
 
-    markdown = markdown.replace("[COMMITTEE]", committee.display_name)
-    markdown = markdown.replace("[PROJECT]", project.short_display_name)
-    markdown = markdown.replace("[REVIEW_URL]", review_url)
-    markdown = markdown.replace("[REVISION]", revision_number)
-    markdown = markdown.replace("[TAG]", revision_tag)
-    markdown = markdown.replace("[VERSION]", version_name)
+    markdown = markdown.replace("{{COMMITTEE}}", committee.display_name)
+    markdown = markdown.replace("{{PROJECT}}", project.short_display_name)
+    markdown = markdown.replace("{{REVIEW_URL}}", review_url)
+    markdown = markdown.replace("{{REVISION}}", revision_number)
+    markdown = markdown.replace("{{TAG}}", revision_tag)
+    markdown = markdown.replace("{{VERSION}}", version_name)
     return markdown
 
 
@@ -218,19 +218,19 @@ async def start_vote_body(body: str, options: 
StartVoteOptions) -> str:
 
     # Perform substitutions in the body
     # TODO: Handle the DURATION == 0 case
-    body = body.replace("[CHECKLIST_URL]", checklist_url)
-    body = body.replace("[COMMITTEE]", committee.display_name)
-    body = body.replace("[DURATION]", str(options.vote_duration))
-    body = body.replace("[KEYS_FILE]", keys_file or "[Sorry, the KEYS file is 
missing!]")
-    body = body.replace("[PROJECT]", project_short_display_name)
-    body = body.replace("[RELEASE_CHECKLIST]", checklist_content)
-    body = body.replace("[REVIEW_URL]", review_url)
-    body = body.replace("[REVISION]", revision_number)
-    body = body.replace("[TAG]", revision_tag)
-    body = body.replace("[VERSION]", options.version_name)
-    body = body.replace("[VOTE_ENDS_UTC]", options.vote_end)
-    body = body.replace("[YOUR_ASF_ID]", options.asfuid)
-    body = body.replace("[YOUR_FULL_NAME]", options.fullname)
+    body = body.replace("{{CHECKLIST_URL}}", checklist_url)
+    body = body.replace("{{COMMITTEE}}", committee.display_name)
+    body = body.replace("{{DURATION}}", str(options.vote_duration))
+    body = body.replace("{{KEYS_FILE}}", keys_file or "(Sorry, the KEYS file 
is missing!)")
+    body = body.replace("{{PROJECT}}", project_short_display_name)
+    body = body.replace("{{RELEASE_CHECKLIST}}", checklist_content)
+    body = body.replace("{{REVIEW_URL}}", review_url)
+    body = body.replace("{{REVISION}}", revision_number)
+    body = body.replace("{{TAG}}", revision_tag)
+    body = body.replace("{{VERSION}}", options.version_name)
+    body = body.replace("{{VOTE_ENDS_UTC}}", options.vote_end)
+    body = body.replace("{{YOUR_ASF_ID}}", options.asfuid)
+    body = body.replace("{{YOUR_FULL_NAME}}", options.fullname)
 
     return body
 
diff --git a/atr/get/projects.py b/atr/get/projects.py
index 30be82e..2003a82 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -597,13 +597,13 @@ def _textarea_with_variables(
     for name, description in template_variables:
         variable_rows.append(
             htm.tr[
-                htm.td(".font-monospace.text-nowrap.py-1")[f"[{name}]"],
+                htm.td(".font-monospace.text-nowrap.py-1")[f"{{{{{name}}}}}"],
                 htm.td(".py-1")[description],
                 htm.td(".text-end.py-1")[
                     htpy.button(
                         ".btn.btn-sm.btn-outline-secondary.copy-var-btn",
                         type="button",
-                        data_variable=f"[{name}]",
+                        data_variable=f"{{{{{name}}}}}",
                     )["Copy"]
                 ],
             ]
diff --git a/atr/models/sql.py b/atr/models/sql.py
index ea94807..5991d2d 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -541,36 +541,36 @@ class Project(sqlmodel.SQLModel, table=True):
     @property
     def policy_announce_release_default(self) -> str:
         return """\
-The Apache [COMMITTEE] project team is pleased to announce the
-release of [PROJECT] [VERSION].
+The Apache {{COMMITTEE}} project team is pleased to announce the
+release of {{PROJECT}} {{VERSION}}.
 
 This is a stable release available for production use.
 
 Downloads are available from the following URL:
 
-[DOWNLOAD_URL]
+{{DOWNLOAD_URL}}
 
-On behalf of the Apache [COMMITTEE] project team,
+On behalf of the Apache {{COMMITTEE}} project team,
 
-[YOUR_FULL_NAME] ([YOUR_ASF_ID])
+{{YOUR_FULL_NAME}} ({{YOUR_ASF_ID}})
 """
 
     @property
     def policy_start_vote_default(self) -> str:
-        return """Hello [COMMITTEE],
+        return """Hello {{COMMITTEE}},
 
 I'd like to call a vote on releasing the following artifacts as
-Apache [PROJECT] [VERSION]. This vote is being conducted using an
+Apache {{PROJECT}} {{VERSION}}. This vote is being conducted using an
 Alpha version of the Apache Trusted Releases (ATR) platform.
 Please report any bugs or issues to the ASF Tooling team.
 
 The release candidate page, including downloads, can be found at:
 
-  [REVIEW_URL]
+  {{REVIEW_URL}}
 
 The release artifacts are signed with one or more OpenPGP keys from:
 
-  [KEYS_FILE]
+  {{KEYS_FILE}}
 
 Please review the release candidate and vote accordingly.
 
@@ -580,11 +580,11 @@ Please review the release candidate and vote accordingly.
 
 You can vote on ATR at the URL above, or manually by replying to this email.
 
-The vote ends after [DURATION] hours at [VOTE_ENDS_UTC].
+The vote ends after {{DURATION}} hours at {{VOTE_ENDS_UTC}}.
 
-[RELEASE_CHECKLIST]
+{{RELEASE_CHECKLIST}}
 Thanks,
-[YOUR_FULL_NAME] ([YOUR_ASF_ID])
+{{YOUR_FULL_NAME}} ({{YOUR_ASF_ID}})
 """
 
     @property
diff --git a/atr/render.py b/atr/render.py
index f7b9049..4329e15 100644
--- a/atr/render.py
+++ b/atr/render.py
@@ -132,13 +132,13 @@ def _variables_tab(
     for name, description in template_variables:
         variable_rows.append(
             htm.tr[
-                htm.td(".font-monospace.text-nowrap")[f"[{name}]"],
+                htm.td(".font-monospace.text-nowrap")[f"{{{{{name}}}}}"],
                 htm.td[description],
                 htm.td(".text-end")[
                     htpy.button(
                         ".btn.btn-sm.btn-outline-secondary.copy-var-btn",
                         type="button",
-                        data_variable=f"[{name}]",
+                        data_variable=f"{{{{{name}}}}}",
                     )["Copy"]
                 ],
             ]
diff --git a/migrations/versions/0033_2025.12.31_f2d97d96.py 
b/migrations/versions/0033_2025.12.31_f2d97d96.py
new file mode 100644
index 0000000..387a751
--- /dev/null
+++ b/migrations/versions/0033_2025.12.31_f2d97d96.py
@@ -0,0 +1,113 @@
+"""Migrate template variable syntax from brackets to double braces
+
+Revision ID: 0033_2025.12.31_f2d97d96
+Revises: 0032_2025.12.30_bb1b64a3
+Create Date: 2025-12-31 17:22:14.280843+00:00
+"""
+
+import re
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# Revision identifiers, used by Alembic
+revision: str = "0033_2025.12.31_f2d97d96"
+down_revision: str | None = "0032_2025.12.30_bb1b64a3"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+# Template fields in releasepolicy that may contain variable placeholders
+TEMPLATE_FIELDS = [
+    "release_checklist",
+    "start_vote_template",
+    "announce_release_template",
+    "vote_comment_template",
+]
+
+# Known variable names that should be converted
+# This explicit list avoids accidentally converting Subject tags
+KNOWN_VARIABLES = [
+    "CHECKLIST_URL",
+    "COMMITTEE",
+    "DOWNLOAD_URL",
+    "DURATION",
+    "KEYS_FILE",
+    "PROJECT",
+    "RELEASE_CHECKLIST",
+    "REVIEW_URL",
+    "REVISION",
+    "TAG",
+    "VERSION",
+    "VOTE_ENDS_UTC",
+    "YOUR_ASF_ID",
+    "YOUR_FULL_NAME",
+]
+
+# Pattern to match only known [VARIABLE] names
+OLD_VARIABLE_PATTERN = re.compile(r"\[(" + "|".join(KNOWN_VARIABLES) + r")\]")
+
+# Pattern to match only known {{VARIABLE}} names
+NEW_VARIABLE_PATTERN = re.compile(r"\{\{(" + "|".join(KNOWN_VARIABLES) + 
r")\}\}")
+
+
+def _convert_old_to_new(text: str) -> str:
+    """Convert [VARIABLE] syntax to {{VARIABLE}} syntax."""
+    return OLD_VARIABLE_PATTERN.sub(r"{{\1}}", text)
+
+
+def _convert_new_to_old(text: str) -> str:
+    """Convert {{VARIABLE}} syntax to [VARIABLE] syntax."""
+    return NEW_VARIABLE_PATTERN.sub(r"[\1]", text)
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+
+    # Fetch all release policies with their template fields
+    result = conn.execute(sa.text(f"SELECT id, {', '.join(TEMPLATE_FIELDS)} 
FROM releasepolicy"))
+    rows = result.fetchall()
+
+    for row in rows:
+        row_id = row[0]
+        updates: dict[str, str] = {}
+
+        for i, field in enumerate(TEMPLATE_FIELDS):
+            old_value = row[i + 1]
+            if old_value:
+                new_value = _convert_old_to_new(old_value)
+                if new_value != old_value:
+                    updates[field] = new_value
+
+        if updates:
+            set_clause = ", ".join(f"{field} = :{field}" for field in updates)
+            conn.execute(
+                sa.text(f"UPDATE releasepolicy SET {set_clause} WHERE id = 
:id"),
+                {"id": row_id, **updates},
+            )
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+
+    # Fetch all release policies with their template fields
+    result = conn.execute(sa.text(f"SELECT id, {', '.join(TEMPLATE_FIELDS)} 
FROM releasepolicy"))
+    rows = result.fetchall()
+
+    for row in rows:
+        row_id = row[0]
+        updates: dict[str, str] = {}
+
+        for i, field in enumerate(TEMPLATE_FIELDS):
+            old_value = row[i + 1]
+            if old_value:
+                new_value = _convert_new_to_old(old_value)
+                if new_value != old_value:
+                    updates[field] = new_value
+
+        if updates:
+            set_clause = ", ".join(f"{field} = :{field}" for field in updates)
+            conn.execute(
+                sa.text(f"UPDATE releasepolicy SET {set_clause} WHERE id = 
:id"),
+                {"id": row_id, **updates},
+            )


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to