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.`,
- );
- }