[IRBuilder] Refactor FMF interface (#121657)
[llvm-project.git] / .github / workflows / commit-access-review.py
blob91d3a61cdcb175848b1a3ce1ccbf6e82c86f6f20
1 #!/usr/bin/env python3
2 # ===-- commit-access-review.py --------------------------------------------===#
4 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5 # See https://llvm.org/LICENSE.txt for license information.
6 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8 # ===------------------------------------------------------------------------===#
10 # ===------------------------------------------------------------------------===#
12 import datetime
13 import github
14 import re
15 import requests
16 import time
17 import sys
18 import re
21 class User:
22 THRESHOLD = 5
24 def __init__(self, name, triage_list):
25 self.name = name
26 self.authored = 0
27 self.merged = 0
28 self.reviewed = 0
29 self.triage_list = triage_list
31 def add_authored(self, val=1):
32 self.authored += val
33 if self.meets_threshold():
34 print(self.name, "meets the threshold with authored commits")
35 del self.triage_list[self.name]
37 def set_authored(self, val):
38 self.authored = 0
39 self.add_authored(val)
41 def add_merged(self, val=1):
42 self.merged += val
43 if self.meets_threshold():
44 print(self.name, "meets the threshold with merged commits")
45 del self.triage_list[self.name]
47 def add_reviewed(self, val=1):
48 self.reviewed += val
49 if self.meets_threshold():
50 print(self.name, "meets the threshold with reviewed commits")
51 del self.triage_list[self.name]
53 def get_total(self):
54 return self.authored + self.merged + self.reviewed
56 def meets_threshold(self):
57 return self.get_total() >= self.THRESHOLD
59 def __repr__(self):
60 return "{} : a: {} m: {} r: {}".format(
61 self.name, self.authored, self.merged, self.reviewed
65 def check_manual_requests(
66 gh: github.Github, start_date: datetime.datetime
67 ) -> list[str]:
68 """
69 Return a list of users who have been asked since ``start_date`` if they
70 want to keep their commit access.
71 """
72 query = """
73 query ($query: String!) {
74 search(query: $query, type: ISSUE, first: 100) {
75 nodes {
76 ... on Issue {
77 body
78 comments (first: 100) {
79 nodes {
80 author {
81 login
89 """
90 formatted_start_date = start_date.strftime("%Y-%m-%dT%H:%M:%S")
91 variables = {
92 "query": f"type:issue created:>{formatted_start_date} org:llvm repo:llvm-project label:infra:commit-access"
95 res_header, res_data = gh._Github__requester.graphql_query(
96 query=query, variables=variables
98 data = res_data["data"]
99 users = []
100 for issue in data["search"]["nodes"]:
101 users.extend([user[1:] for user in re.findall("@[^ ,\n]+", issue["body"])])
103 return users
106 def get_num_commits(gh: github.Github, user: str, start_date: datetime.datetime) -> int:
108 Get number of commits that ``user`` has been made since ``start_date`.
110 variables = {
111 "owner": "llvm",
112 "user": user,
113 "start_date": start_date.strftime("%Y-%m-%dT%H:%M:%S"),
116 user_query = """
117 query ($user: String!) {
118 user(login: $user) {
124 res_header, res_data = gh._Github__requester.graphql_query(
125 query=user_query, variables=variables
127 data = res_data["data"]
128 variables["user_id"] = data["user"]["id"]
130 query = """
131 query ($owner: String!, $user_id: ID!, $start_date: GitTimestamp!){
132 organization(login: $owner) {
133 teams(query: "llvm-committers" first:1) {
134 nodes {
135 repositories {
136 nodes {
137 ref(qualifiedName: "main") {
138 target {
139 ... on Commit {
140 history(since: $start_date, author: {id: $user_id }) {
141 totalCount
153 count = 0
154 res_header, res_data = gh._Github__requester.graphql_query(
155 query=query, variables=variables
157 data = res_data["data"]
158 for repo in data["organization"]["teams"]["nodes"][0]["repositories"]["nodes"]:
159 count += int(repo["ref"]["target"]["history"]["totalCount"])
160 if count >= User.THRESHOLD:
161 break
162 return count
165 def is_new_committer_query_repo(
166 gh: github.Github, user: str, start_date: datetime.datetime
167 ) -> bool:
169 Determine if ``user`` is a new committer. A new committer can keep their
170 commit access even if they don't meet the criteria.
172 variables = {
173 "user": user,
176 user_query = """
177 query ($user: String!) {
178 user(login: $user) {
184 res_header, res_data = gh._Github__requester.graphql_query(
185 query=user_query, variables=variables
187 data = res_data["data"]
188 variables["owner"] = "llvm"
189 variables["user_id"] = data["user"]["id"]
190 variables["start_date"] = start_date.strftime("%Y-%m-%dT%H:%M:%S")
192 query = """
193 query ($owner: String!, $user_id: ID!){
194 organization(login: $owner) {
195 repository(name: "llvm-project") {
196 ref(qualifiedName: "main") {
197 target {
198 ... on Commit {
199 history(author: {id: $user_id }, first: 5) {
200 nodes {
201 committedDate
212 res_header, res_data = gh._Github__requester.graphql_query(
213 query=query, variables=variables
215 data = res_data["data"]
216 repo = data["organization"]["repository"]
217 commits = repo["ref"]["target"]["history"]["nodes"]
218 if len(commits) == 0:
219 return True
220 committed_date = commits[-1]["committedDate"]
221 if datetime.datetime.strptime(committed_date, "%Y-%m-%dT%H:%M:%SZ") < start_date:
222 return False
223 return True
226 def is_new_committer(
227 gh: github.Github, user: str, start_date: datetime.datetime
228 ) -> bool:
230 Wrapper around is_new_commiter_query_repo to handle exceptions.
232 try:
233 return is_new_committer_query_repo(gh, user, start_date)
234 except:
235 pass
236 return True
239 def get_review_count(
240 gh: github.Github, user: str, start_date: datetime.datetime
241 ) -> int:
243 Return the number of reviews that ``user`` has done since ``start_date``.
245 query = """
246 query ($query: String!) {
247 search(query: $query, type: ISSUE, first: 5) {
248 issueCount
252 formatted_start_date = start_date.strftime("%Y-%m-%dT%H:%M:%S")
253 variables = {
254 "owner": "llvm",
255 "repo": "llvm-project",
256 "user": user,
257 "query": f"type:pr commenter:{user} -author:{user} merged:>{formatted_start_date} org:llvm",
260 res_header, res_data = gh._Github__requester.graphql_query(
261 query=query, variables=variables
263 data = res_data["data"]
264 return int(data["search"]["issueCount"])
267 def count_prs(gh: github.Github, triage_list: dict, start_date: datetime.datetime):
269 Fetch all the merged PRs for the project since ``start_date`` and update
270 ``triage_list`` with the number of PRs merged for each user.
273 query = """
274 query ($query: String!, $after: String) {
275 search(query: $query, type: ISSUE, first: 100, after: $after) {
276 issueCount,
277 nodes {
278 ... on PullRequest {
279 author {
280 login
282 mergedBy {
283 login
287 pageInfo {
288 hasNextPage
289 endCursor
294 date_begin = start_date
295 date_end = None
296 while date_begin < datetime.datetime.now():
297 date_end = date_begin + datetime.timedelta(days=7)
298 formatted_date_begin = date_begin.strftime("%Y-%m-%dT%H:%M:%S")
299 formatted_date_end = date_end.strftime("%Y-%m-%dT%H:%M:%S")
300 variables = {
301 "query": f"type:pr is:merged merged:{formatted_date_begin}..{formatted_date_end} org:llvm",
303 has_next_page = True
304 while has_next_page:
305 print(variables)
306 res_header, res_data = gh._Github__requester.graphql_query(
307 query=query, variables=variables
309 data = res_data["data"]
310 for pr in data["search"]["nodes"]:
311 # Users can be None if the user has been deleted.
312 if not pr["author"]:
313 continue
314 author = pr["author"]["login"]
315 if author in triage_list:
316 triage_list[author].add_authored()
318 if not pr["mergedBy"]:
319 continue
320 merger = pr["mergedBy"]["login"]
321 if author == merger:
322 continue
323 if merger not in triage_list:
324 continue
325 triage_list[merger].add_merged()
327 has_next_page = data["search"]["pageInfo"]["hasNextPage"]
328 if has_next_page:
329 variables["after"] = data["search"]["pageInfo"]["endCursor"]
330 date_begin = date_end
333 def main():
334 token = sys.argv[1]
335 gh = github.Github(login_or_token=token)
336 org = gh.get_organization("llvm")
337 repo = org.get_repo("llvm-project")
338 one_year_ago = datetime.datetime.now() - datetime.timedelta(days=365)
339 triage_list = {}
340 for collaborator in repo.get_collaborators(permission="push"):
341 triage_list[collaborator.login] = User(collaborator.login, triage_list)
343 print("Start:", len(triage_list), "triagers")
344 # Step 0 Check if users have requested commit access in the last year.
345 for user in check_manual_requests(gh, one_year_ago):
346 if user in triage_list:
347 print(user, "requested commit access in the last year.")
348 del triage_list[user]
349 print("After Request Check:", len(triage_list), "triagers")
351 # Step 1 count all PRs authored or merged
352 count_prs(gh, triage_list, one_year_ago)
354 print("After PRs:", len(triage_list), "triagers")
356 if len(triage_list) == 0:
357 sys.exit(0)
359 # Step 2 check for reviews
360 for user in list(triage_list.keys()):
361 review_count = get_review_count(gh, user, one_year_ago)
362 triage_list[user].add_reviewed(review_count)
364 print("After Reviews:", len(triage_list), "triagers")
366 if len(triage_list) == 0:
367 sys.exit(0)
369 # Step 3 check for number of commits
370 for user in list(triage_list.keys()):
371 num_commits = get_num_commits(gh, user, one_year_ago)
372 # Override the total number of commits to not double count commits and
373 # authored PRs.
374 triage_list[user].set_authored(num_commits)
376 print("After Commits:", len(triage_list), "triagers")
378 # Step 4 check for new committers
379 for user in list(triage_list.keys()):
380 print("Checking", user)
381 if is_new_committer(gh, user, one_year_ago):
382 print("Removing new committer: ", user)
383 del triage_list[user]
385 print("Complete:", len(triage_list), "triagers")
387 with open("triagers.log", "w") as triagers_log:
388 for user in triage_list:
389 print(triage_list[user].__repr__())
390 triagers_log.write(user + "\n")
393 if __name__ == "__main__":
394 main()