Skip to content

Commit b677b48

Browse files
committed
feat: Add reviewer on pull request based on commit history
added functionality in pr-triager.yaml PipelineRun to add reviewer in a pull request based on previous commit history Signed-off-by: Zaki Shaikh <zashaikh@redhat.com>
1 parent 1da22b4 commit b677b48

File tree

3 files changed

+87
-6
lines changed

3 files changed

+87
-6
lines changed

.tekton/pr-labeler.yaml renamed to .tekton/pr-triager.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
apiVersion: tekton.dev/v1beta1
33
kind: PipelineRun
44
metadata:
5-
name: pr-labeler
5+
name: pr-triager
66
annotations:
77
pipelinesascode.tekton.dev/max-keep-runs: "2"
88
pipelinesascode.tekton.dev/cancel-in-progress: "true"
@@ -34,7 +34,7 @@ spec:
3434
- name: gemini_model
3535
- name: excluded_labels
3636
tasks:
37-
- name: pr-labeler
37+
- name: pr-triager
3838
taskSpec:
3939
params:
4040
- name: repo_url
@@ -87,7 +87,7 @@ spec:
8787
- name: EXCLUDED_LABELS
8888
value: "$(params.excluded_labels)"
8989
command:
90-
- ./hack/pr-labeler.py
90+
- ./hack/pr-triager.py
9191
params:
9292
- name: repo_url
9393
value: "$(params.repo_url)"

hack/pr-labeler.py renamed to hack/pr-triager.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# ///
99
import json
1010
import os
11+
from collections import Counter
1112

1213
# pylint: disable=no-name-in-module
1314
import google.generativeai as genai
@@ -219,6 +220,82 @@ def add_labels_to_pr(labels):
219220
print(f"Response: {e.response.text}")
220221

221222

223+
def add_reviewers_to_pr(reviewers):
224+
"""Add reviewers to the current pull request"""
225+
if not reviewers:
226+
print("No reviewers to add")
227+
return
228+
229+
url = f"https://api.github.com/repos/{os.environ['REPO_OWNER']}/{os.environ['REPO_NAME']}/pulls/{os.environ['PR_NUMBER']}/requested_reviewers"
230+
headers = {
231+
"Authorization": f"token {os.environ['GITHUB_TOKEN']}",
232+
"Accept": "application/vnd.github.v3+json",
233+
}
234+
data = {"reviewers": reviewers}
235+
236+
try:
237+
response = requests.post(url, headers=headers, json=data, timeout=300)
238+
response.raise_for_status()
239+
print(f"Successfully added reviewers: {reviewers}")
240+
except requests.exceptions.RequestException as e:
241+
print(f"Error adding reviewers: {e}")
242+
if hasattr(e, "response") and e.response:
243+
print(f"Response: {e.response.text}")
244+
245+
246+
def handle_reviewer_suggestions(pr_info, files_changed):
247+
"""Suggests and adds reviewers based on file change history if none are assigned"""
248+
if pr_info.get("requested_reviewers"):
249+
assigned_reviewers = [r["login"] for r in pr_info["requested_reviewers"]]
250+
print(f"PR already has reviewers assigned: {assigned_reviewers}")
251+
return
252+
253+
print("No reviewers assigned, attempting to suggest one...")
254+
pr_author = pr_info.get("user", {}).get("login")
255+
headers = {
256+
"Authorization": f"token {os.environ['GITHUB_TOKEN']}",
257+
"Accept": "application/vnd.github.v3+json",
258+
}
259+
260+
# Filter out vendor files
261+
non_vendor_files = [
262+
f for f in files_changed if not f.split("\t")[1].startswith("vendor/")
263+
]
264+
265+
# Limit to the first 50 files
266+
files_to_process = non_vendor_files
267+
if len(non_vendor_files) > 50:
268+
print(
269+
"PR has more than 50 non-vendor files, analyzing the first 50 for reviewer suggestion."
270+
)
271+
files_to_process = non_vendor_files[:50]
272+
273+
contributor_counts = Counter()
274+
for file_info in files_to_process:
275+
filename = file_info.split("\t")[1]
276+
# Limit to 20 commits per file
277+
commits_url = f"https://api.github.com/repos/{os.environ['REPO_OWNER']}/{os.environ['REPO_NAME']}/commits?path={filename}&per_page=20"
278+
try:
279+
# Use a direct request instead of pagination for only the first page
280+
response = requests.get(commits_url, headers=headers, timeout=300)
281+
response.raise_for_status()
282+
commits_data = response.json()
283+
for commit in commits_data:
284+
if commit.get("author") and commit["author"].get("login"):
285+
author = commit["author"]["login"]
286+
if author != pr_author:
287+
contributor_counts[author] += 1
288+
except requests.exceptions.RequestException as e:
289+
print(f"Could not fetch commits for {filename}: {e}")
290+
continue
291+
if not contributor_counts:
292+
print("Could not identify any potential reviewers from file history.")
293+
else:
294+
top_reviewer = contributor_counts.most_common(1)[0][0]
295+
print(f"Suggested reviewer based on file history: {top_reviewer}")
296+
add_reviewers_to_pr([top_reviewer])
297+
298+
222299
def validate_environment():
223300
"""Validate all required environment variables are set"""
224301
required_vars = [
@@ -256,6 +333,10 @@ def main():
256333

257334
print(f"Analyzing PR #{os.environ['PR_NUMBER']}: {pr_title}")
258335

336+
# --- Suggest and add reviewers ---
337+
handle_reviewer_suggestions(pr_info, files_changed)
338+
339+
# --- ORIGINAL LABELING LOGIC (UNCHANGED) ---
259340
# Show which Gemini model is being used
260341
model_name = os.environ.get("GEMINI_MODEL", DEFAULT_MODEL)
261342
print(f"Using Gemini model: {model_name}")

pkg/pipelineascode/pipelineascode.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi
247247
OriginalPipelineRunName: pr.GetAnnotations()[keys.OriginalPRName],
248248
}
249249

250-
// Patch the pipelineRun with the appropriate annotations and labels.
251-
// Set the state so the watcher will continue with reconciling the pipelineRun
252-
// The watcher reconciles only pipelineRuns that has the state annotation.
250+
// Patch the PipelineRun with the annotations and labels required by the watcher.
251+
// The watcher reconciles only PipelineRuns that contain the state annotation,
252+
// so we set it here to ensure reconciliation continues.
253253
patchAnnotations := map[string]string{}
254254
patchLabels := map[string]string{}
255255
whatPatching := ""

0 commit comments

Comments
 (0)