This is an automated email from the ASF dual-hosted git repository.
chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new ff64569ecac KAFKA-20362 Auto-fill PR reviewers in PR description
(#21928)
ff64569ecac is described below
commit ff64569ecace9e3175eb472786853f72be0db3c8
Author: Ming-Yen Chung <[email protected]>
AuthorDate: Sat Apr 4 13:12:20 2026 +0800
KAFKA-20362 Auto-fill PR reviewers in PR description (#21928)
When a pull request **submit review** event is triggered (comment,
request changes, or approval), this change automatically adds the
reviewer to the Reviewers trailer in the PR body.
Note that **comment** will **NOT** trigger the auto-fill process.
This eliminates the need to manually run committer-tools/reviewers.py
before merging. The reviewer's name and email are resolved from their
most recent commit in the repo, falling back to their GitHub profile.
Changes:
- pr-reviewed.yml: save reviewer login to artifact on review events
- pr-linter.yml: pass reviewer login to pr-format.py
- pr-format.py: resolve reviewer identity via GitHub API and append
to the Reviewers trailer using git interpret-trailers
Testing:
```
$ python -c "
from importlib.machinery import SourceFileLoader
m = SourceFileLoader('pr_format',
'.github/scripts/pr-format.py').load_module()
print(m.resolve_reviewer('mingyen066'))
print(m.resolve_reviewer('antirez'))
print(m.resolve_reviewer('torvalds'))
"
# Tier 1 hit: found name and email from repo commit history
('Ming-Yen Chung', '[email protected]')
# Tier 1 miss, Tier 2 hit: found email from GitHub profile
('Salvatore Sanfilippo', '[email protected]')
# Both miss email: placeholder used for committer to fix manually
('Linus Torvalds', 'torvalds-email-not-found')
$ python -c " from importlib.machinery import SourceFileLoader m =
SourceFileLoader('pr_format',
'.github/scripts/pr-format.py').load_module() existing = ['Chia-Ping
Tsai <[email protected]>, Ming-Yen Chung <[email protected]>']
print(m.already_exists('[email protected]', existing))
print(m.already_exists('[email protected]', existing))
print(m.already_exists('[email protected]', existing)) " True False
True
$ python -c " from importlib.machinery import SourceFileLoader m =
SourceFileLoader('pr_format',
'.github/scripts/pr-format.py').load_module() body = 'This is a PR
description.\n' result = m.update_reviewers_trailer(body, 'Reviewers:
Alice <[email protected]>') print(result) " This is a PR description.
Reviewers: Alice <[email protected]>
```
Reviewers: TaiJuWu <[email protected]>, Chia-Ping Tsai
<[email protected]>
---
.github/scripts/pr-format.py | 85 ++++++++++++++++++++++++++++++++++++++-
.github/workflows/pr-linter.yml | 7 +++-
.github/workflows/pr-reviewed.yml | 7 +++-
3 files changed, 95 insertions(+), 4 deletions(-)
diff --git a/.github/scripts/pr-format.py b/.github/scripts/pr-format.py
index d2da5e3e5bf..3bc911c0634 100644
--- a/.github/scripts/pr-format.py
+++ b/.github/scripts/pr-format.py
@@ -18,12 +18,13 @@ from io import BytesIO
import json
import logging
import os
+import re
import subprocess
import shlex
import sys
import tempfile
import textwrap
-from typing import Dict, Optional, TextIO
+from typing import Dict, List, Optional, TextIO
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
@@ -103,6 +104,73 @@ def split_paragraphs(text: str):
yield paragraph, markdown
+def resolve_reviewer(login: str) -> tuple:
+ """Map a GitHub login to (name, email).
+
+ Tries the repo commit history first, then falls back to the GitHub user
profile.
+ If the display name is unavailable, the login is used as the name.
+ If the email is unavailable, '{login}@email-not-found' is used as a
placeholder.
+ """
+ name = None
+ email = None
+
+ # Tier 1: find from repo commit history
+ try:
+ cmd = f"gh api repos/apache/kafka/commits?author={login}&per_page=1"
+ p = subprocess.run(shlex.split(cmd), capture_output=True, text=True)
+ if p.returncode == 0:
+ commits = json.loads(p.stdout)
+ if commits:
+ author = commits[0].get("commit", {}).get("author", {})
+ name = author.get("name")
+ email = author.get("email")
+ except Exception as e:
+ logger.debug(f"Failed to resolve {login} from commit history: {e}")
+
+ # Tier 2: GitHub user profile
+ if not name or not email:
+ try:
+ cmd = f"gh api users/{login}"
+ p = subprocess.run(shlex.split(cmd), capture_output=True,
text=True)
+ if p.returncode == 0:
+ user = json.loads(p.stdout)
+ if not name:
+ name = user.get("name")
+ if not email:
+ email = user.get("email")
+ except Exception as e:
+ logger.debug(f"Failed to resolve {login} from GitHub profile: {e}")
+
+ if not name:
+ name = login
+
+ if not email:
+ email = f"{login}@email-not-found"
+
+ return (name, email)
+
+
+def already_exists(email: str, existing_reviewers: List[str]) -> bool:
+ """Check if a reviewer with the given email is already in the existing
reviewers list."""
+ existing_emails = re.findall(r'<(.+?)>', ", ".join(existing_reviewers))
+ return email.lower() in [e.lower() for e in existing_emails]
+
+
+def update_reviewers_trailer(body: str, trailer: str) -> str:
+ """Update the Reviewers trailer in the body using git
interpret-trailers."""
+ with tempfile.NamedTemporaryFile() as fp:
+ fp.write(body.strip().encode())
+ fp.write(b"\n")
+ fp.flush()
+ cmd = f"git interpret-trailers --if-exists replace --trailer
{shlex.quote(trailer)} {fp.name}"
+ p = subprocess.run(shlex.split(cmd), capture_output=True)
+ fp.close()
+
+ if p.returncode == 0:
+ return p.stdout.decode()
+ return body
+
+
if __name__ == "__main__":
"""
This script performs some basic linting of our PR titles and body. The PR
number is read from the PR_NUMBER
@@ -123,7 +191,7 @@ if __name__ == "__main__":
"""
pr_number = get_env("PR_NUMBER")
- cmd = f"gh pr view {pr_number} --json 'title,body,reviews'"
+ cmd = f"gh pr view {pr_number} --json 'title,body,reviews,author'"
p = subprocess.run(shlex.split(cmd), capture_output=True)
if p.returncode != 0:
logger.error(f"GitHub CLI failed with exit code
{p.returncode}.\nSTDOUT: {p.stdout.decode()}\nSTDERR:{p.stderr.decode()}")
@@ -134,6 +202,19 @@ if __name__ == "__main__":
body = gh_json["body"]
reviews = gh_json["reviews"]
+ # Auto-fill reviewer from the current review event.
+ # Approvals are also review events, so approvers are automatically added.
+ reviewer_login = get_env("REVIEWER_LOGIN")
+ pr_author = (gh_json.get("author") or {}).get("login")
+ if reviewer_login and reviewer_login != pr_author:
+ existing_reviewers = parse_trailers(title, body).get("Reviewers", [])
+ name, email = resolve_reviewer(reviewer_login)
+ if not already_exists(email, existing_reviewers):
+ existing_value = ", ".join(existing_reviewers)
+ resolved = f"{name} <{email}>"
+ new_value = f"{existing_value}, {resolved}" if existing_value else
resolved
+ body = update_reviewers_trailer(body, f"Reviewers: {new_value}")
+
checks = [] # (bool (0=ok, 1=error), message)
def check(positive_assertion, ok_msg, err_msg):
diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml
index d38a9659a01..6b085cefad9 100644
--- a/.github/workflows/pr-linter.yml
+++ b/.github/workflows/pr-linter.yml
@@ -61,7 +61,12 @@ jobs:
echo "Restored PR_NUMBER.txt:"
cat PR_NUMBER.txt
PR_NUMBER=$(cat PR_NUMBER.txt)
- PR_NUMBER=$PR_NUMBER python .github/scripts/pr-format.py 2>>
"$GITHUB_STEP_SUMMARY" 1>> pr-format-output.txt
+ REVIEWER_LOGIN=""
+ if [ -f REVIEWER_LOGIN.txt ]; then
+ REVIEWER_LOGIN=$(cat REVIEWER_LOGIN.txt)
+ echo "Reviewer login: $REVIEWER_LOGIN"
+ fi
+ PR_NUMBER=$PR_NUMBER REVIEWER_LOGIN=$REVIEWER_LOGIN python
.github/scripts/pr-format.py 2>> "$GITHUB_STEP_SUMMARY" 1>> pr-format-output.txt
exitcode="$?"
message=$(cat pr-format-output.txt)
echo "message=$message" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/pr-reviewed.yml
b/.github/workflows/pr-reviewed.yml
index 64636a6c292..edacae2ff29 100644
--- a/.github/workflows/pr-reviewed.yml
+++ b/.github/workflows/pr-reviewed.yml
@@ -36,7 +36,12 @@ jobs:
GITHUB_CONTEXT: ${{ toJson(github) }}
- name: Save PR Number
run: echo ${{ github.event.pull_request.number }} > PR_NUMBER.txt
+ - name: Save Reviewer Login
+ if: github.event_name == 'pull_request_review'
+ run: echo ${{ github.event.review.user.login }} > REVIEWER_LOGIN.txt
- uses: actions/upload-artifact@v4
with:
name: PR_NUMBER.txt
- path: PR_NUMBER.txt
+ path: |
+ PR_NUMBER.txt
+ REVIEWER_LOGIN.txt