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

Yicong-Huang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 2652315f6c ci: add /request-review and /unrequest-review comment 
commands (#4986)
2652315f6c is described below

commit 2652315f6c8d51688ca26dafaa760f811cff01a2
Author: Matthew B. <[email protected]>
AuthorDate: Fri May 8 17:39:36 2026 -0700

    ci: add /request-review and /unrequest-review comment commands (#4986)
    
    ### What changes were proposed in this PR?
    - Renamed .github/workflows/take-commands.yml →
    .github/workflows/comment-commands.yml, since the workflow now handles
    more than just /take and /untake.
    - Added a new request-review job that handles two new slash commands on
    PRs:
    - /request-review @alice @bob requests reviews from the listed
    users/teams.
    - /unrequest-review @alice cancels a pending review request.
    - Authorization: PR author (fast path) or any committer with
    write/maintain/admin permission. Anyone else is rejected and logged.
    - Supports both individual users (@alice) and teams (@org/team-name);
    routes them to the correct API bucket.
    - Strips self-mentions before calling the API so the atomic call doesn't
    fail over a single bad name.
    - Workflow renamed Issue take commands → Comment commands and granted
    pull-requests: write.
    - Avoids the /review namespace so it stays free for future use (e.g.,
    self-review).
    
    ### Any related issues, documentation, or discussions?
    Closes: #4975
    
    ### How was this PR tested?
    Tested on my local fork.
    
    
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-Authored with Claude Opus 4.7 in Compliance with ASF
---
 .github/workflows/comment-commands.yml | 167 +++++++++++++++++++++++++++++++++
 .github/workflows/take-commands.yml    |  85 -----------------
 2 files changed, 167 insertions(+), 85 deletions(-)

diff --git a/.github/workflows/comment-commands.yml 
b/.github/workflows/comment-commands.yml
new file mode 100644
index 0000000000..3300db1353
--- /dev/null
+++ b/.github/workflows/comment-commands.yml
@@ -0,0 +1,167 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# /take, /untake, /request-review, and /unrequest-review comment commands.
+#
+# Triage state is no longer materialized as a label — it is the search
+# filter `is:issue is:open no:assignee`. Anyone can self-claim an issue
+# by commenting `/take` (and self-release with `/untake`); PR-driven
+# assignee sync is handled by `pr-assignment.yml`.
+#
+# On pull requests, the author can request or cancel reviewer requests
+# via `/request-review @user [@user ...]` and `/unrequest-review @user
+# [@user ...]`. We avoid the `/review` namespace so it stays free for
+# future use (e.g. self-review).
+name: Comment commands
+on:
+  issue_comment:
+    types: [created]
+
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  take:
+    # The startsWith filter at the job level keeps unrelated comments
+    # from allocating a runner; the regex inside the script enforces an
+    # exact `/take` or `/untake` so suffixes like `/take this` do not
+    # silently match.
+    if: >-
+      github.event_name == 'issue_comment'
+      && github.event.action == 'created'
+      && github.event.issue.pull_request == null
+      && github.event.comment.user.type != 'Bot'
+      && (startsWith(github.event.comment.body, '/take')
+          || startsWith(github.event.comment.body, '/untake'))
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v8
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const body = (context.payload.comment.body || '').trim();
+            const issue_number = context.payload.issue.number;
+            const login = context.payload.comment.user.login;
+            const { owner, repo } = context.repo;
+            core.info(
+              `take/untake candidate: ${login} on issue #${issue_number}; ` +
+                `body=${JSON.stringify(body)}`,
+            );
+
+            if (/^\/take\s*$/.test(body)) {
+              try {
+                await github.rest.issues.addAssignees({
+                  owner, repo, issue_number, assignees: [login],
+                });
+                core.info(`Assigned ${login} to issue #${issue_number}`);
+              } catch (e) {
+                core.warning(
+                  `addAssignees on #${issue_number} failed: ${e.message}`,
+                );
+              }
+            } else if (/^\/untake\s*$/.test(body)) {
+              try {
+                await github.rest.issues.removeAssignees({
+                  owner, repo, issue_number, assignees: [login],
+                });
+                core.info(`Unassigned ${login} from issue #${issue_number}`);
+              } catch (e) {
+                core.warning(
+                  `removeAssignees on #${issue_number} failed: ${e.message}`,
+                );
+              }
+            } else {
+              core.info(
+                `Comment does not match exact '/take' or '/untake'; skipping.`,
+              );
+            }
+
+  request-review:
+    # Job-level startsWith gate avoids spinning up a runner for every
+    # PR comment; the regex inside the script enforces the exact shape.
+    if: >-
+      github.event_name == 'issue_comment'
+      && github.event.action == 'created'
+      && github.event.issue.pull_request != null
+      && github.event.comment.user.type != 'Bot'
+      && (startsWith(github.event.comment.body, '/request-review')
+          || startsWith(github.event.comment.body, '/unrequest-review'))
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/github-script@v8
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const body = (context.payload.comment.body || '').trim();
+            const pull_number = context.payload.issue.number;
+            const commenter = context.payload.comment.user.login;
+            const author = context.payload.issue.user.login;
+            const { owner, repo } = context.repo;
+
+            const match = body.match(
+              /^\/(request-review|unrequest-review)\b(.*)$/s,
+            );
+            if (!match) {
+              core.info(`Comment does not match exact command; skipping.`);
+              return;
+            }
+            const action = match[1];
+
+            if (commenter !== author) {
+              core.info(
+                `${commenter} is not the author of #${pull_number}; skipping.`,
+              );
+              return;
+            }
+
+            // Parse @user and @org/team mentions; route teams to the
+            // team_reviewers bucket. Strip self so the API doesn't
+            // reject the whole atomic call over one bad name. Copilot
+            // is a bot reviewer that the REST API expects as the exact
+            // slug "Copilot", so normalize any casing of @copilot.
+            const reviewers = [];
+            const team_reviewers = [];
+            for (const [, h] of match[2].matchAll(
+              /@([\w-]+(?:\/[\w.-]+)?)/g,
+            )) {
+              if (h.includes('/')) team_reviewers.push(h.split('/')[1]);
+              else if (h.toLowerCase() === 'copilot') 
reviewers.push('Copilot');
+              else if (h.toLowerCase() !== author.toLowerCase())
+                reviewers.push(h);
+            }
+            if (!reviewers.length && !team_reviewers.length) {
+              core.warning(`No valid @mentions in '${action}'; skipping.`);
+              return;
+            }
+
+            const params = { owner, repo, pull_number, reviewers, 
team_reviewers };
+            try {
+              if (action === 'request-review') {
+                await github.rest.pulls.requestReviewers(params);
+              } else {
+                await github.rest.pulls.removeRequestedReviewers(params);
+              }
+              core.info(
+                `${action} on #${pull_number} by ${commenter}: ` +
+                  `users=[${reviewers.join(', ')}] ` +
+                  `teams=[${team_reviewers.join(', ')}]`,
+              );
+            } catch (e) {
+              core.warning(
+                `${action} on #${pull_number} failed: ${e.message}`,
+              );
+            }
diff --git a/.github/workflows/take-commands.yml 
b/.github/workflows/take-commands.yml
deleted file mode 100644
index bf567f6240..0000000000
--- a/.github/workflows/take-commands.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# /take and /untake comment commands.
-#
-# Triage state is no longer materialized as a label — it is the search
-# filter `is:issue is:open no:assignee`. Anyone can self-claim an issue
-# by commenting `/take` (and self-release with `/untake`); PR-driven
-# assignee sync is handled by `pr-assignment.yml`.
-name: Issue take commands
-on:
-  issue_comment:
-    types: [created]
-
-permissions:
-  issues: write
-
-jobs:
-  take:
-    # The startsWith filter at the job level keeps unrelated comments
-    # from allocating a runner; the regex inside the script enforces an
-    # exact `/take` or `/untake` so suffixes like `/take this` do not
-    # silently match.
-    if: >-
-      github.event_name == 'issue_comment'
-      && github.event.action == 'created'
-      && github.event.issue.pull_request == null
-      && github.event.comment.user.type != 'Bot'
-      && (startsWith(github.event.comment.body, '/take')
-          || startsWith(github.event.comment.body, '/untake'))
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/github-script@v8
-        with:
-          github-token: ${{ secrets.GITHUB_TOKEN }}
-          script: |
-            const body = (context.payload.comment.body || '').trim();
-            const issue_number = context.payload.issue.number;
-            const login = context.payload.comment.user.login;
-            const { owner, repo } = context.repo;
-            core.info(
-              `take/untake candidate: ${login} on issue #${issue_number}; ` +
-                `body=${JSON.stringify(body)}`,
-            );
-
-            if (/^\/take\s*$/.test(body)) {
-              try {
-                await github.rest.issues.addAssignees({
-                  owner, repo, issue_number, assignees: [login],
-                });
-                core.info(`Assigned ${login} to issue #${issue_number}`);
-              } catch (e) {
-                core.warning(
-                  `addAssignees on #${issue_number} failed: ${e.message}`,
-                );
-              }
-            } else if (/^\/untake\s*$/.test(body)) {
-              try {
-                await github.rest.issues.removeAssignees({
-                  owner, repo, issue_number, assignees: [login],
-                });
-                core.info(`Unassigned ${login} from issue #${issue_number}`);
-              } catch (e) {
-                core.warning(
-                  `removeAssignees on #${issue_number} failed: ${e.message}`,
-                );
-              }
-            } else {
-              core.info(
-                `Comment does not match exact '/take' or '/untake'; skipping.`,
-              );
-            }

Reply via email to