build(release): comment on PRs when added to release tracking issue (#4302)

Previously, adding a PR to a release tracking issue's backports
checklist left no notification on the PR itself, making it harder to
see which release tracks the backport.

Update `add-backports` to update the tracking issue per PR and post a
comment on each backported PR linking the created or updated release
tracking issue, and grant `pull-requests: write` permission to the
workflow.
diff --git a/.github/workflows/release_add_backports.yaml b/.github/workflows/release_add_backports.yaml
index b20fd95..db28a03 100644
--- a/.github/workflows/release_add_backports.yaml
+++ b/.github/workflows/release_add_backports.yaml
@@ -24,6 +24,7 @@
 
 permissions:
   issues: write
+  pull-requests: write
 
 jobs:
   add_backports:
diff --git a/dev/release/add_backports.py b/dev/release/add_backports.py
index 8d357e8..1a10e46 100644
--- a/dev/release/add_backports.py
+++ b/dev/release/add_backports.py
@@ -26,6 +26,7 @@
         args = self.args
 
         issue_num = args.issue
+        created_issue = False
         if not issue_num:
             print(
                 "No issue specified. Trying to auto-discover active release"
@@ -64,6 +65,7 @@
                     issue_num = self.gh.create_release_tracking_issue(
                         patch_version, template_content
                     )
+                    created_issue = True
                     print(
                         f"::notice::Created patch release tracking issue #{issue_num} for"
                         f" v{patch_version}"
@@ -74,59 +76,75 @@
                 )
                 return 1
 
-        resolved_prs = []
-        for pr_ref in args.prs:
-            try:
-                pr_num = self.gh.resolve_pr_number(pr_ref)
-                resolved_prs.append(pr_num)
-            except Exception as e:
-                print(f"::error::Error resolving PR ref '{pr_ref}': {e}")
-                return 1
-
-        print(
-            f"Adding backports {resolved_prs} (resolved from {args.prs}) to tracking issue #{issue_num}..."
-        )
         try:
             body = self.gh.get_issue_body(issue_num)
-            items_to_add = [{"ref": f"#{pr}"} for pr in resolved_prs]
-            body = add_backports_to_body(body, items_to_add)
-            for pr in resolved_prs:
-                body = add_sync_changelog_task_to_body(body, pr)
-            state = parse_checklist_state(body)
-            rc_tags = state.get("rc_tags", {})
-            has_pending_rc = any(
-                not task.checked and task.status != "done" for task in rc_tags.values()
-            )
-            next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0
-
             issue_title = self.gh.get_issue_title(issue_num)
-            version_match = RELEASE_TITLE_RE.search(issue_title)
-            is_patch = False
-            if version_match:
-                version = version_match.group(1)
-                is_patch = not version.endswith(".0")
-
-            if not has_pending_rc and (rc_tags or not is_patch):
-                print(
-                    f"No pending RC task found. Adding 'Tag"
-                    f" RC{next_rc_num}' to checklist..."
-                )
-                body = add_rc_task_to_body(body, next_rc_num)
-        except ValueError as e:
-            print(f"::error::{e}")
-            return 1
         except Exception as e:
             print(f"::error::Failed to update tracking issue: {e}")
             return 1
 
-        try:
-            self.gh.update_issue_body(issue_num, body)
+        version_match = RELEASE_TITLE_RE.search(issue_title)
+        is_patch = False
+        if version_match:
+            version = version_match.group(1)
+            is_patch = not version.endswith(".0")
+
+        action = "Created" if created_issue else "Updated"
+        comment_body = (
+            f"{action} release tracking issue #{issue_num} ({issue_title}) with"
+            " this PR added to the backports checklist."
+        )
+
+        for pr_ref in args.prs:
+            try:
+                pr = self.gh.resolve_pr_number(pr_ref)
+            except Exception as e:
+                print(f"::error::Error resolving PR ref '{pr_ref}': {e}")
+                return 1
+
             print(
-                f"::notice::Successfully updated tracking issue #{issue_num} checklist."
+                f"Adding backport #{pr} (resolved from {pr_ref}) to tracking"
+                f" issue #{issue_num}..."
             )
-        except Exception as e:
-            print(f"::error::Failed to update tracking issue body: {e}")
-            return 1
+            try:
+                body = add_backports_to_body(body, [{"ref": f"#{pr}"}])
+                body = add_sync_changelog_task_to_body(body, pr)
+                state = parse_checklist_state(body)
+                rc_tags = state.get("rc_tags", {})
+                has_pending_rc = any(
+                    not task.checked and task.status != "done"
+                    for task in rc_tags.values()
+                )
+                next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0
+
+                if not has_pending_rc and (rc_tags or not is_patch):
+                    print(
+                        f"No pending RC task found. Adding 'Tag"
+                        f" RC{next_rc_num}' to checklist..."
+                    )
+                    body = add_rc_task_to_body(body, next_rc_num)
+            except ValueError as e:
+                print(f"::error::{e}")
+                return 1
+            except Exception as e:
+                print(f"::error::Failed to update tracking issue: {e}")
+                return 1
+
+            try:
+                self.gh.update_issue_body(issue_num, body)
+                print(
+                    f"::notice::Successfully updated tracking issue #{issue_num}"
+                    " checklist."
+                )
+            except Exception as e:
+                print(f"::error::Failed to update tracking issue body: {e}")
+                return 1
+
+            try:
+                self.gh.post_issue_comment(pr, comment_body)
+            except Exception as e:
+                print(f"::error::Failed to post comment to PR #{pr}: {e}")
+                return 1
 
         return 0
 
diff --git a/tests/tools/private/release/add_backports_test.py b/tests/tools/private/release/add_backports_test.py
index 24636c6..3b5ece7 100644
--- a/tests/tools/private/release/add_backports_test.py
+++ b/tests/tools/private/release/add_backports_test.py
@@ -30,6 +30,12 @@
     assert "- [ ] Tag RC0" in updated_body
     assert "- [ ] Sync Changelog #124" in updated_body
     assert "- [ ] Sync Changelog #125" in updated_body
+    expected_comment = (
+        "Updated release tracking issue #123 (Release 2.1.0) with this PR added"
+        " to the backports checklist."
+    )
+    assert mock_gh.issue_comments[124] == [expected_comment]
+    assert mock_gh.issue_comments[125] == [expected_comment]
 
 
 def test_add_backports_auto_discover_success(mock_gh):
@@ -51,6 +57,10 @@
     assert result == 0
     updated_body = mock_gh.get_issue_body(issue_num)
     assert "- [ ] #124" in updated_body
+    assert mock_gh.issue_comments[124] == [
+        f"Updated release tracking issue #{issue_num} (Release 2.1.0) with this"
+        " PR added to the backports checklist."
+    ]
 
 
 def test_add_backports_auto_discover_no_issues_creates_patch_release(
@@ -75,6 +85,10 @@
     assert "Prepare Release" not in body
     assert "Create Release branch" not in body
     assert "- [ ] Tag Final" in body
+    assert mock_gh.issue_comments[124] == [
+        f"Created release tracking issue #{issue['number']} (Release 1.2.1)"
+        " with this PR added to the backports checklist."
+    ]
 
 
 def test_add_backports_auto_discover_skips_complete_release_creates_patch_release(