Neews python 3.7+ (or do a different date parser)
[omc-tools.git] / github-approve-label-workflow.py
1 #! /usr/bin/env python
2 # requires python 3.7+
3 #
4 # Do we have any open PR's that have label "Approval: done"
5 # that are over 24 hours without any other comments?
6 #
7 # get a token.... https://github.com/settings/tokens/new -- just repo is fine
8 # pop it in token.txt or you'll get a bad API limit
9 #
10 # note that we'd use pyGithub but we can't as it doesn't fully handle the timeline objects
11 # as of Feb 2020
12 #
13 # mark@openssl.org Feb 2020
14 #
15 import requests
16 import json
17 from datetime import datetime, timezone
18 from optparse import OptionParser
19
20 api_url = "https://api.github.com/repos/openssl/openssl"
21
22 def convertdate(date):
23     # python fromisoformat needs a TZ in hours/minutes
24     return datetime.fromisoformat(date.replace('Z', '+00:00'))
25
26 # Get all the open pull requests, filtering by approval: done label
27
28 def getpullrequests():
29     url = api_url + "/pulls?per_page=100&page=1"  # defaults to open
30     res = requests.get(url, headers=headers)
31     repos = res.json()
32     prs = []
33     while 'next' in res.links.keys():
34         res = requests.get(res.links['next']['url'], headers=headers)
35         repos.extend(res.json())
36
37     # Let's filter by label if we're just looking to move things, we can parse
38     # everything for statistics in another script
39
40     try:
41         for pr in repos:
42             if 'labels' in pr:
43                 for label in pr['labels']:
44                     if label['name'] == 'approval: done':
45                         prs.append(pr['number'])
46     except:
47         print("failed", repos['message'])
48     return prs
49
50 # Change the labels on an issue from approval: done to approval: ready to merge
51
52 def movelabeldonetoready(issue):
53     url = api_url + "/issues/" + str(issue) + "/labels/approval:%20done"
54     res = requests.delete(url, headers=headers)
55     if (res.status_code != 200):
56         print("Error removing label", res.status_code, res.content)
57         return
58     url = api_url + "/issues/" + str(issue) + "/labels"
59     newlabel = {"labels": ["approval: ready to merge"]}
60     res = requests.post(url, data=json.dumps(newlabel), headers=headers)
61     if (res.status_code != 200):
62         print("Error adding label", res.status_code, res.content)
63
64 # Check through an issue and see if it's a candidate for moving
65
66 def checkpr(pr):
67     url = api_url + "/issues/" + str(pr) + "/timeline?per_page=100&page=1"
68     res = requests.get(url, headers=headers)
69     repos = res.json()
70     while 'next' in res.links.keys():
71         res = requests.get(res.links['next']['url'], headers=headers)
72         repos.extend(res.json())
73
74     comments = []
75     approvallabel = {}
76     readytomerge = 0
77
78     for event in repos:
79         try:
80             if (event['event'] == "commented"):
81                 comments.append(convertdate(event["updated_at"]))
82                 if debug:
83                     print("debug: commented at ",
84                           convertdate(event["updated_at"]))
85             if (event['event'] == "committed"):
86                 comments.append(convertdate(event["author"]["date"]))
87                 if debug:
88                     print("debug: created at ",
89                           convertdate(event["author"]["date"]))
90             elif (event['event'] == "labeled"):
91                 if debug:
92                     print("debug: labelled with ", event['label']['name'],
93                           "at", convertdate(event["created_at"]))
94                 approvallabel[event['label']['name']] = convertdate(
95                     event["created_at"])
96             elif (event['event'] == "unlabeled"):
97                 if (debug):
98                     print("debug: unlabelled with ", event['label']['name'],
99                           "at", convertdate(event["created_at"]))
100                 if event['label'][
101                         'name'] in approvallabel:  # have to do this for if labels got renamed in the middle
102                     del approvallabel[event['label']['name']]
103             elif (event['event'] == "reviewed"
104                   and event['state'] == "approved"):
105                 if debug:
106                     print("debug: approved at",
107                           convertdate(event['submitted_at']))
108         except:
109             return (repos['message'])
110
111     if 'approval: ready to merge' in approvallabel:
112         return ("issue already has label approval: ready to merge")
113     if 'approval: done' not in approvallabel:
114         return ("issue did not get label approval: done")
115     approvedone = approvallabel['approval: done']
116
117     if max(comments) > approvedone:
118         return ("issue had comments after approval: done label was given")
119
120     now = datetime.now(timezone.utc)
121     hourssinceapproval = (now - approvedone).total_seconds() / 3600
122     if debug:
123         print("Now: ", now)
124         print("Last comment: ", max(comments))
125         print("Approved since: ", approvedone)
126         print("hours since approval", hourssinceapproval)
127
128     if (hourssinceapproval < 24):
129         return ("not yet 24 hours since labelled approval:done hours:" +
130                 str(int(hourssinceapproval)))
131
132     if (options.commit):
133         print("Moving issue ", pr, " to approval: ready to merge")
134         movelabeldonetoready(pr)
135     else:
136         print("use --commit to actually change the labels")
137     return (
138         "this issue was candidate to move to approval: ready to merge hours:" +
139         str(int(hourssinceapproval)))
140
141 # main
142
143 parser = OptionParser()
144 parser.add_option("-d","--debug",action="store_true",help="be noisy",dest="debug")
145 parser.add_option("-t","--token",help="file containing github authentication token for example 'token 18asdjada...'",dest="token")
146 parser.add_option("-c","--commit",action="store_true",help="actually change the labels",dest="commit")
147 (options, args) = parser.parse_args()
148 if (options.token):
149     fp = open(options.token, "r")
150     git_token = fp.readline().strip('\n')
151 else:
152     git_token = ""  # blank token is fine, but you can't change labels and you hit API rate limiting
153 debug = options.debug
154 # since timeline is a preview feature we have to enable access to it with an accept header
155 headers = {
156     "Accept": "application/vnd.github.mockingbird-preview",
157     "Authorization": git_token
158 }
159
160 if debug:
161     print("Getting list of PRs")
162 prs = getpullrequests()
163 print("There were", len(prs), "open PRs with approval:done ")
164 for pr in prs:
165     print(pr, checkpr(pr))