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

Reply via email to