build(release): fetch PR body in get_pr_info and grant PR read permissions (#4177)

The release preparation completion workflow parses the merged PR body to
locate the associated release tracking issue. Previously, the workflow
failed to resolve the tracking issue because `get_pr_info` omitted the
PR `body` field and the GitHub Actions workflow lacked permission to
read pull requests.

Request the `body` field when querying PR metadata and grant
`pull-requests: read` permission to the workflow so it can extract the
issue number from the merged preparation PR.

Work towards #4175
diff --git a/.github/workflows/release_complete_prepare.yaml b/.github/workflows/release_complete_prepare.yaml
index 8315057..6d07024 100644
--- a/.github/workflows/release_complete_prepare.yaml
+++ b/.github/workflows/release_complete_prepare.yaml
@@ -27,6 +27,7 @@
 permissions:
   contents: write
   issues: write
+  pull-requests: read
 
 jobs:
   complete_prepare:
diff --git a/dev/release/gh.py b/dev/release/gh.py
index b3bff9c..ec43662 100644
--- a/dev/release/gh.py
+++ b/dev/release/gh.py
@@ -731,9 +731,9 @@
             pr_num: The PR number.
 
         Returns:
-            Dictionary containing PR fields (state, isDraft, mergeCommit, etc.).
+            Dictionary containing PR fields (state, isDraft, mergeCommit, body, etc.).
         """
-        output = self._gh_pr_view(pr_num, "state", "isDraft", "mergeCommit")
+        output = self._gh_pr_view(pr_num, "state", "isDraft", "mergeCommit", "body")
         return json.loads(output) if output else {}
 
     @override
diff --git a/tests/tools/private/release/gh_test.py b/tests/tools/private/release/gh_test.py
index e1aea0e..814bedb 100644
--- a/tests/tools/private/release/gh_test.py
+++ b/tests/tools/private/release/gh_test.py
@@ -122,6 +122,29 @@
     )
 
 
+def test_get_pr_info(gh, auto_patch_cmd_helpers):
+    auto_patch_cmd_helpers.run_gh.return_value = (
+        '{"state": "MERGED", "isDraft": false, '
+        '"mergeCommit": {"oid": "abc1234"}, "body": "Work towards #4175"}'
+    )
+    info = gh.get_pr_info(123)
+    assert info == {
+        "state": "MERGED",
+        "isDraft": False,
+        "mergeCommit": {"oid": "abc1234"},
+        "body": "Work towards #4175",
+    }
+    auto_patch_cmd_helpers.run_gh.assert_called_with(
+        "pr",
+        "view",
+        "123",
+        "--json=state,isDraft,mergeCommit,body",
+        "--repo=my-owner/my-repo",
+        check=True,
+        capture_output=True,
+    )
+
+
 def test_get_pr_files_not_found(gh, auto_patch_cmd_helpers):
     auto_patch_cmd_helpers.run_gh.side_effect = subprocess.CalledProcessError(1, ["gh"])
     with pytest.raises(GetPrError, match="Failed to get PR #123 on my-owner/my-repo"):