workflows(release): support /backport comment on release tracking issue and pr (#4050)

Release tracking issues previously expected `/add-backports` while PRs
used `/backport`, leading to inconsistent command conventions across
release workflows and documentation.

Update the comment workflow handler to support `/backport` and
`/backports` consistently across both release tracking issues and
PRs. Documentation and issue templates are updated to reflect the
unified syntax, and missing arguments now emit GitHub Actions workflow
error annotations.
diff --git a/.github/ISSUE_TEMPLATE/release_tracking_template.md b/.github/ISSUE_TEMPLATE/release_tracking_template.md
index ab634e7..efa78a9 100644
--- a/.github/ISSUE_TEMPLATE/release_tracking_template.md
+++ b/.github/ISSUE_TEMPLATE/release_tracking_template.md
@@ -11,24 +11,29 @@
 - [ ] Tag Final
 
 ## Backports
-
-To request a backport, add it to the checklist below and process it. See [RELEASING.md: How to add backports](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#how-to-add-backports) for details.
-
+ 
+To request a backport, comment `/backport` on the PR, comment `/backport <PR>`
+on this issue, or add it to the checklist below. See
+[RELEASING.md: How to add backports](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#how-to-add-backports)
+for details.
+ 
 ---
-
-To manually control the release flow, see the [RELEASING.md: Manual Editing](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#manual-editing-of-tracking-issue) section.
-
+ 
+To manually control the release flow, see the
+[RELEASING.md: Manual Editing](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#manual-editing-of-tracking-issue)
+section.
+ 
 <details>
 <summary><b>Available Commands</b></summary>
-
+ 
 Comment commands:
 - `/prepare`: Determines version, creates tracking issue and preparation PR.
 - `/prepare-complete [PR]`: Marks preparation task as complete.
 - `/create-release-branch`: Cuts and pushes the release branch.
 - `/create-rc`: Tags and publishes a new release candidate (RC).
 - `/process-backports`: Cherry-picks pending backports.
-- `/add-backports <PRs>`: Adds PRs to the backports and processes backports.
+- `/backport <PRs>`: Adds PRs to the backports and processes backports.
 - `/promote`: Promotes the latest RC to final release.
-
+ 
 See [RELEASING.md](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md) for details on how to use them.
 </details>
diff --git a/.github/workflows/on_comment.py b/.github/workflows/on_comment.py
index 4d4b4eb..1aa0f1b 100755
--- a/.github/workflows/on_comment.py
+++ b/.github/workflows/on_comment.py
@@ -15,11 +15,17 @@
     return val.lower() == "true"
 
 
-def _match_command(command: str, comment_body: str) -> re.Match[str] | None:
+def _match_command(
+    command: str | tuple[str, ...], comment_body: str
+) -> re.Match[str] | None:
     """Matches a slash command at the start of any line, capturing optional trailing args."""
-    cmd = command.lstrip("/")
+    if isinstance(command, str):
+        commands = (command,)
+    else:
+        commands = command
+    pattern = "|".join(re.escape(cmd.lstrip("/")) for cmd in commands)
     return re.search(
-        rf"^\s*/{re.escape(cmd)}(?:\s+(\S.*?))?\s*$",
+        rf"^\s*/(?:{pattern})(?:\s+(\S.*?))?\s*$",
         comment_body,
         re.MULTILINE,
     )
@@ -61,7 +67,7 @@
 
 def _react_negative(repo: str, comment_id: str) -> None:
     """Logs error and adds a negative reaction to the comment."""
-    print("Error: No PRs specified for add-backports.", file=sys.stderr)
+    print("::error::No PRs specified for backport.")
     if comment_id and repo:
         _add_comment_reaction(repo=repo, comment_id=comment_id, content="-1")
 
@@ -95,7 +101,7 @@
         _write_github_output("command", "process-backports")
         return
 
-    if m := _match_command("add-backports", comment_body):
+    if m := _match_command(("backport", "backports"), comment_body):
         raw_args = m.group(1) if m.group(1) else ""
         items = [item for item in re.split(r"[\s,]+", raw_args) if item]
         if csv := ",".join(items):
@@ -128,7 +134,7 @@
 
 def _process_pr_comment(comment_body: str, pr_number: str) -> None:
     """Processes comments on a pull request."""
-    if _match_command("backport", comment_body):
+    if _match_command(("backport", "backports"), comment_body):
         _write_github_output("command", "pr-backport")
         _write_github_output("pr_number", pr_number)
         return
diff --git a/RELEASING.md b/RELEASING.md
index 6749e2f..f740cb3 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -108,7 +108,7 @@
 
 ### Method B: Comment on the Tracking Issue
 
-Comment `/add-backports <PR_REF> [<PR_REF> ...]` (space or comma separated) on
+Comment `/backport <PR_REF> [<PR_REF> ...]` (space or comma separated) on
 the tracking issue. The `<PR_REF>` can be a PR number (optionally prefixed with
 `#`) or a PR URL (strictly for the configured repository). This will
 automatically add the PRs to the checklist and trigger processing.
diff --git a/tests/workflows/on_comment_test.py b/tests/workflows/on_comment_test.py
index 9ec9413..d7df7da 100644
--- a/tests/workflows/on_comment_test.py
+++ b/tests/workflows/on_comment_test.py
@@ -152,10 +152,10 @@
     assert gha_env.read_env() == {"issue_number": "100"}
 
 
-def test_release_issue_add_backports(monkeypatch, gha_env):
+def test_release_issue_backport(monkeypatch, gha_env):
     _run_comment(
         monkeypatch,
-        "/add-backports 1, 2, 3",
+        "/backport 1, 2, 3",
         has_release_label="true",
     )
     assert gha_env.read_outputs() == {
@@ -166,10 +166,10 @@
     assert gha_env.read_env() == {"issue_number": "100"}
 
 
-def test_release_issue_add_backports_hashes(monkeypatch, gha_env):
+def test_release_issue_backport_hashes(monkeypatch, gha_env):
     _run_comment(
         monkeypatch,
-        "/add-backports #123 #567",
+        "/backport #123 #567",
         has_release_label="true",
     )
     assert gha_env.read_outputs() == {
@@ -180,12 +180,10 @@
     assert gha_env.read_env() == {"issue_number": "100"}
 
 
-def test_release_issue_add_backports_empty(
-    monkeypatch, gha_env, mock_add_reaction, capsys
-):
+def test_release_issue_backport_empty(monkeypatch, gha_env, mock_add_reaction, capsys):
     _run_comment(
         monkeypatch,
-        "/add-backports",
+        "/backport",
         has_release_label="true",
         repo="bazel-contrib/rules_python",
         comment_id="789",
@@ -196,7 +194,7 @@
     }
     assert gha_env.read_env() == {"issue_number": "100"}
     captured = capsys.readouterr()
-    assert "Error: No PRs specified for add-backports." in captured.err
+    assert "::error::No PRs specified for backport." in captured.out
     mock_add_reaction.assert_called_once_with(
         repo="bazel-contrib/rules_python",
         comment_id="789",
@@ -311,6 +309,20 @@
     assert gha_env.read_env() == {}
 
 
+def test_pr_backports_plural_alias(monkeypatch, gha_env):
+    _run_comment(
+        monkeypatch,
+        "/backports",
+        is_pr="true",
+        event_number="300",
+    )
+    assert gha_env.read_outputs() == {
+        "command": "pr-backport",
+        "pr_number": "300",
+    }
+    assert gha_env.read_env() == {}
+
+
 def test_pr_prepare_complete(monkeypatch, gha_env):
     _run_comment(
         monkeypatch,