blob: a428f102cfe1d5baf6cec146bcc2135e2e7a3b3a [file] [log] [blame]
Anas Nashif20483162022-02-25 18:37:47 -05001#!/usr/bin/env python3
2
3# Copyright (c) 2022 Intel Corp.
4# SPDX-License-Identifier: Apache-2.0
5
6import argparse
7import sys
8import os
9import time
10import datetime
11from github import Github, GithubException
Anas Nashif60271522022-07-18 19:37:31 -040012from github.GithubException import UnknownObjectException
Anas Nashif20483162022-02-25 18:37:47 -050013from collections import defaultdict
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000014from west.manifest import Manifest
15from west.manifest import ManifestProject
Anas Nashif20483162022-02-25 18:37:47 -050016
17TOP_DIR = os.path.join(os.path.dirname(__file__))
18sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
19from get_maintainer import Maintainers
20
21def log(s):
22 if args.verbose > 0:
23 print(s, file=sys.stdout)
24
25def parse_args():
26 global args
27 parser = argparse.ArgumentParser(
28 description=__doc__,
Jamie McCraeec704442023-01-04 16:08:36 +000029 formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
Anas Nashif20483162022-02-25 18:37:47 -050030
31 parser.add_argument("-M", "--maintainer-file", required=False, default="MAINTAINERS.yml",
32 help="Maintainer file to be used.")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000033
34 group = parser.add_mutually_exclusive_group()
35 group.add_argument("-P", "--pull_request", required=False, default=None, type=int,
36 help="Operate on one pull-request only.")
Fabio Baltierib6cbcba2023-08-22 17:04:31 +000037 group.add_argument("-I", "--issue", required=False, default=None, type=int,
38 help="Operate on one issue only.")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +000039 group.add_argument("-s", "--since", required=False,
40 help="Process pull-requests since date.")
41 group.add_argument("-m", "--modules", action="store_true",
42 help="Process pull-requests from modules.")
Anas Nashif20483162022-02-25 18:37:47 -050043
44 parser.add_argument("-y", "--dry-run", action="store_true", default=False,
45 help="Dry run only.")
46
47 parser.add_argument("-o", "--org", default="zephyrproject-rtos",
48 help="Github organisation")
49
50 parser.add_argument("-r", "--repo", default="zephyr",
51 help="Github repository")
52
53 parser.add_argument("-v", "--verbose", action="count", default=0,
54 help="Verbose Output")
55
56 args = parser.parse_args()
57
58def process_pr(gh, maintainer_file, number):
59
60 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
61 pr = gh_repo.get_pull(number)
62
63 log(f"working on https://github.com/{args.org}/{args.repo}/pull/{pr.number} : {pr.title}")
64
65 labels = set()
Anas Nashif20483162022-02-25 18:37:47 -050066 area_counter = defaultdict(int)
Anas Nashifd9a300e2023-10-12 10:24:06 +000067 found_maintainers = defaultdict(int)
Anas Nashif20483162022-02-25 18:37:47 -050068
69 num_files = 0
70 all_areas = set()
71 fn = list(pr.get_files())
Anas Nashifd9a300e2023-10-12 10:24:06 +000072
Anas Nashifdebe7fe2023-11-06 13:22:55 +000073 for changed_file in fn:
74 if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
Anas Nashifdebe7fe2023-11-06 13:22:55 +000075 break
76
Martin Jägerc1a3bfb2024-06-04 14:42:32 +020077 if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1):
78 labels = {'size: XS'}
Anas Nashifd9a300e2023-10-12 10:24:06 +000079
Anas Nashif20483162022-02-25 18:37:47 -050080 if len(fn) > 500:
81 log(f"Too many files changed ({len(fn)}), skipping....")
82 return
Anas Nashifd9a300e2023-10-12 10:24:06 +000083
84 for changed_file in fn:
Anas Nashif20483162022-02-25 18:37:47 -050085 num_files += 1
Anas Nashifd9a300e2023-10-12 10:24:06 +000086 log(f"file: {changed_file.filename}")
87 areas = maintainer_file.path2areas(changed_file.filename)
Anas Nashif20483162022-02-25 18:37:47 -050088
Anas Nashifd9a300e2023-10-12 10:24:06 +000089 if not areas:
90 continue
Anas Nashif20483162022-02-25 18:37:47 -050091
Anas Nashifd9a300e2023-10-12 10:24:06 +000092 all_areas.update(areas)
93 is_instance = False
94 sorted_areas = sorted(areas, key=lambda x: 'Platform' in x.name, reverse=True)
95 for area in sorted_areas:
96 c = 1 if not is_instance else 0
97
98 area_counter[area] += c
99 labels.update(area.labels)
100 # FIXME: Here we count the same file multiple times if it exists in
101 # multiple areas with same maintainer
102 for area_maintainer in area.maintainers:
103 found_maintainers[area_maintainer] += c
104
105 if 'Platform' in area.name:
106 is_instance = True
107
108 area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
109 log(f"Area matches: {area_counter}")
Anas Nashif20483162022-02-25 18:37:47 -0500110 log(f"labels: {labels}")
Anas Nashif20483162022-02-25 18:37:47 -0500111
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900112 # Create a list of collaborators ordered by the area match
113 collab = list()
Anas Nashifd9a300e2023-10-12 10:24:06 +0000114 for area in area_counter:
115 collab += maintainer_file.areas[area.name].maintainers
116 collab += maintainer_file.areas[area.name].collaborators
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900117 collab = list(dict.fromkeys(collab))
118 log(f"collab: {collab}")
119
Anas Nashifd9a300e2023-10-12 10:24:06 +0000120 _all_maintainers = dict(sorted(found_maintainers.items(), key=lambda item: item[1], reverse=True))
Anas Nashif20483162022-02-25 18:37:47 -0500121
122 log(f"Submitted by: {pr.user.login}")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000123 log(f"candidate maintainers: {_all_maintainers}")
Anas Nashif20483162022-02-25 18:37:47 -0500124
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400125 assignees = []
Anas Nashif913426e2024-06-25 21:56:57 -0400126 tmp_assignees = []
Fabio Baltierid06450b2022-10-03 14:51:40 +0000127
Anas Nashifd9a300e2023-10-12 10:24:06 +0000128 # we start with areas with most files changed and pick the maintainer from the first one.
129 # if the first area is an implementation, i.e. driver or platform, we
Anas Nashif913426e2024-06-25 21:56:57 -0400130 # continue searching for any other areas involved
Anas Nashifd9a300e2023-10-12 10:24:06 +0000131 for area, count in area_counter.items():
132 if count == 0:
133 continue
134 if len(area.maintainers) > 0:
Anas Nashif913426e2024-06-25 21:56:57 -0400135 tmp_assignees = area.maintainers
136 if pr.user.login in area.maintainers:
137 # submitter = assignee, try to pick next area and
138 # assign someone else other than the submitter
Fin Maaße6d6aac2025-03-13 12:14:28 +0100139 # when there also other maintainers for the area
140 # assign them
141 if len(area.maintainers) > 1:
142 assignees = area.maintainers.copy()
143 assignees.remove(pr.user.login)
144 else:
145 continue
Anas Nashif913426e2024-06-25 21:56:57 -0400146 else:
147 assignees = area.maintainers
Anas Nashif20483162022-02-25 18:37:47 -0500148
Anas Nashifd9a300e2023-10-12 10:24:06 +0000149 if 'Platform' not in area.name:
150 break
Anas Nashif20483162022-02-25 18:37:47 -0500151
Anas Nashif913426e2024-06-25 21:56:57 -0400152 if tmp_assignees and not assignees:
153 assignees = tmp_assignees
154
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400155 if assignees:
156 prop = (found_maintainers[assignees[0]] / num_files) * 100
157 log(f"Picked assignees: {assignees} ({prop:.2f}% ownership)")
Anas Nashifd9a300e2023-10-12 10:24:06 +0000158 log("+++++++++++++++++++++++++")
Anas Nashif20483162022-02-25 18:37:47 -0500159
160 # Set labels
Fabio Baltieri16d723e2023-01-26 17:03:13 +0000161 if labels:
162 if len(labels) < 10:
163 for l in labels:
164 log(f"adding label {l}...")
165 if not args.dry_run:
166 pr.add_to_labels(l)
167 else:
168 log(f"Too many labels to be applied")
Anas Nashif20483162022-02-25 18:37:47 -0500169
170 if collab:
171 reviewers = []
172 existing_reviewers = set()
173
174 revs = pr.get_reviews()
175 for review in revs:
176 existing_reviewers.add(review.user)
177
178 rl = pr.get_review_requests()
179 page = 0
180 for r in rl:
181 existing_reviewers |= set(r.get_page(page))
182 page += 1
183
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500184 # check for reviewers that remove themselves from list of reviewer and
185 # do not attempt to add them again based on MAINTAINERS file.
186 self_removal = []
187 for event in pr.get_issue_events():
188 if event.event == 'review_request_removed' and event.actor == event.requested_reviewer:
189 self_removal.append(event.actor)
190
191 for collaborator in collab:
Anas Nashif60271522022-07-18 19:37:31 -0400192 try:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500193 gh_user = gh.get_user(collaborator)
Fabio Baltierifec3cee2024-04-02 08:49:48 +0000194 if pr.user == gh_user or gh_user in existing_reviewers:
195 continue
196 if not gh_repo.has_in_collaborators(gh_user):
197 log(f"Skip '{collaborator}': not in collaborators")
198 continue
199 if gh_user in self_removal:
200 log(f"Skip '{collaborator}': self removed")
201 continue
202 reviewers.append(collaborator)
Anas Nashif60271522022-07-18 19:37:31 -0400203 except UnknownObjectException as e:
Anas Nashif0d7d39d2024-01-10 14:10:13 -0500204 log(f"Can't get user '{collaborator}', account does not exist anymore? ({e})")
Anas Nashif20483162022-02-25 18:37:47 -0500205
Stephanos Ioannidisfaf42082022-10-20 21:51:03 +0900206 if len(existing_reviewers) < 15:
207 reviewer_vacancy = 15 - len(existing_reviewers)
208 reviewers = reviewers[:reviewer_vacancy]
209
210 if reviewers:
211 try:
212 log(f"adding reviewers {reviewers}...")
213 if not args.dry_run:
214 pr.create_review_request(reviewers=reviewers)
215 except GithubException:
216 log("cant add reviewer")
217 else:
218 log("not adding reviewers because the existing reviewer count is greater than or "
219 "equal to 15")
Anas Nashif20483162022-02-25 18:37:47 -0500220
221 ms = []
222 # assignees
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400223 if assignees and not pr.assignee:
Anas Nashif20483162022-02-25 18:37:47 -0500224 try:
Anas Nashif1a46b7e2024-06-18 07:27:46 -0400225 for assignee in assignees:
226 u = gh.get_user(assignee)
227 ms.append(u)
Anas Nashif20483162022-02-25 18:37:47 -0500228 except GithubException:
229 log(f"Error: Unknown user")
230
231 for mm in ms:
232 log(f"Adding assignee {mm}...")
233 if not args.dry_run:
234 pr.add_to_assignees(mm)
Anas Nashifd63c2c42022-06-16 11:25:52 -0400235 else:
236 log("not setting assignee")
Anas Nashif20483162022-02-25 18:37:47 -0500237
238 time.sleep(1)
239
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000240
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000241def process_issue(gh, maintainer_file, number):
242 gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
243 issue = gh_repo.get_issue(number)
244
245 log(f"Working on {issue.url}: {issue.title}")
246
247 if issue.assignees:
248 print(f"Already assigned {issue.assignees}, bailing out")
249 return
250
251 label_to_maintainer = defaultdict(set)
252 for _, area in maintainer_file.areas.items():
253 if not area.labels:
254 continue
255
256 labels = set()
257 for label in area.labels:
258 labels.add(label.lower())
259 labels = tuple(sorted(labels))
260
261 for maintainer in area.maintainers:
262 label_to_maintainer[labels].add(maintainer)
263
264 # Add extra entries for areas with multiple labels so they match with just
265 # one label if it's specific enough.
266 for areas, maintainers in dict(label_to_maintainer).items():
267 for area in areas:
268 if tuple([area]) not in label_to_maintainer:
269 label_to_maintainer[tuple([area])] = maintainers
270
271 issue_labels = set()
272 for label in issue.labels:
273 label_name = label.name.lower()
274 if tuple([label_name]) not in label_to_maintainer:
275 print(f"Ignoring label: {label}")
276 continue
277 issue_labels.add(label_name)
278 issue_labels = tuple(sorted(issue_labels))
279
280 print(f"Using labels: {issue_labels}")
281
282 if issue_labels not in label_to_maintainer:
283 print(f"no match for the label set, not assigning")
284 return
285
286 for maintainer in label_to_maintainer[issue_labels]:
287 log(f"Adding {maintainer} to {issue.html_url}")
288 if not args.dry_run:
289 issue.add_to_assignees(maintainer)
290
291
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000292def process_modules(gh, maintainers_file):
293 manifest = Manifest.from_file()
294
295 repos = {}
296 for project in manifest.get_projects([]):
297 if not manifest.is_active(project):
298 continue
299
300 if isinstance(project, ManifestProject):
301 continue
302
303 area = f"West project: {project.name}"
304 if area not in maintainers_file.areas:
305 log(f"No area for: {area}")
306 continue
307
308 maintainers = maintainers_file.areas[area].maintainers
309 if not maintainers:
310 log(f"No maintainers for: {area}")
311 continue
312
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000313 collaborators = maintainers_file.areas[area].collaborators
314
315 log(f"Found {area}, maintainers={maintainers}, collaborators={collaborators}")
316
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000317 repo_name = f"{args.org}/{project.name}"
318 repos[repo_name] = maintainers_file.areas[area]
319
320 query = f"is:open is:pr no:assignee"
321 for repo in repos:
322 query += f" repo:{repo}"
323
324 issues = gh.search_issues(query=query)
325 for issue in issues:
326 pull = issue.as_pull_request()
327
328 if pull.draft:
329 continue
330
331 if pull.assignees:
332 log(f"ERROR: {pull.html_url} should have no assignees, found {pull.assignees}")
333 continue
334
335 repo_name = f"{args.org}/{issue.repository.name}"
336 area = repos[repo_name]
337
338 for maintainer in area.maintainers:
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000339 log(f"Assigning {maintainer} to {pull.html_url}")
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000340 if not args.dry_run:
341 pull.add_to_assignees(maintainer)
Fabio Baltiericf6bb282023-09-14 13:09:33 +0000342 pull.create_review_request(maintainer)
343
344 for collaborator in area.collaborators:
345 log(f"Adding {collaborator} to {pull.html_url}")
346 if not args.dry_run:
347 pull.create_review_request(collaborator)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000348
349
Anas Nashif20483162022-02-25 18:37:47 -0500350def main():
351 parse_args()
352
353 token = os.environ.get('GITHUB_TOKEN', None)
354 if not token:
355 sys.exit('Github token not set in environment, please set the '
356 'GITHUB_TOKEN environment variable and retry.')
357
358 gh = Github(token)
359 maintainer_file = Maintainers(args.maintainer_file)
360
361 if args.pull_request:
362 process_pr(gh, maintainer_file, args.pull_request)
Fabio Baltieri5e786602023-08-25 13:49:27 +0000363 elif args.issue:
Fabio Baltierib6cbcba2023-08-22 17:04:31 +0000364 process_issue(gh, maintainer_file, args.issue)
Fabio Baltieri9a1f4ab2023-08-15 14:31:32 +0000365 elif args.modules:
366 process_modules(gh, maintainer_file)
Anas Nashif20483162022-02-25 18:37:47 -0500367 else:
368 if args.since:
369 since = args.since
370 else:
371 today = datetime.date.today()
372 since = today - datetime.timedelta(days=1)
373
374 common_prs = f'repo:{args.org}/{args.repo} is:open is:pr base:main -is:draft no:assignee created:>{since}'
375 pulls = gh.search_issues(query=f'{common_prs}')
376
377 for issue in pulls:
378 process_pr(gh, maintainer_file, issue.number)
379
380
381if __name__ == "__main__":
382 main()