1
+ """A script for measuring time to first response for GitHub issues.
2
+
3
+ This script uses the GitHub API to search for issues in a repository and measure
4
+ the time to first response for each issue. It then calculates the average time
5
+ to first response and writes the issues with their time to first response to a
6
+ markdown file.
7
+
8
+ Functions:
9
+ search_issues: Search for issues in a GitHub repository.
10
+ auth_to_github: Authenticate to the GitHub API.
11
+ measure_time_to_first_response: Measure the time to first response for a GitHub issue.
12
+ get_average_time_to_first_response: Calculate the average time to first response for
13
+ a list of issues.
14
+ write_to_markdown: Write the issues with metrics to a markdown file.
15
+
16
+ """
17
+
1
18
import os
2
- from datetime import datetime
19
+ from datetime import datetime , timedelta
3
20
from os .path import dirname , join
4
21
from urllib .parse import urlparse
5
22
@@ -13,24 +30,25 @@ def search_issues(repository_url, issue_search_query, github_connection):
13
30
14
31
Args:
15
32
repository_url (str): The URL of the repository to search in.
33
+ ie https://github.com/user/repo
16
34
issue_search_query (str): The search query to use for finding issues.
17
35
github_connection (github3.GitHub): A connection to the GitHub API.
18
36
19
37
Returns:
20
38
List[github3.issues.Issue]: A list of issues that match the search query.
21
39
"""
40
+ print ("Searching for issues..." )
22
41
# Parse the repository owner and name from the URL
23
42
parsed_url = urlparse (repository_url )
24
43
path = parsed_url .path .strip ("/" )
25
-
44
+ print ( f"parsing URL: { repository_url } " )
26
45
# Split the path into owner and repo
27
46
owner , repo = path .split ("/" )
28
-
29
- # Get the repository object
30
- repo = github_connection .repository (owner , repo ) # type: ignore
47
+ print (f"owner: { owner } , repo: { repo } " )
31
48
32
49
# Search for issues that match the query
33
- issues = repo .search_issues (issue_search_query ) # type: ignore
50
+ full_query = f"repo:{ owner } /{ repo } { issue_search_query } "
51
+ issues = github_connection .search_issues (full_query ) # type: ignore
34
52
35
53
# Print the issue titles
36
54
for issue in issues :
@@ -59,8 +77,8 @@ def measure_time_to_first_response(issues):
59
77
issues (list of github3.Issue): A list of GitHub issues.
60
78
61
79
Returns:
62
- list of github3.Issue : A list of GitHub issues with the time to first response
63
- added as an attribute .
80
+ list of tuple : A list of tuples containing a GitHub issue
81
+ title, url, and its time to first response .
64
82
65
83
Raises:
66
84
TypeError: If the input is not a list of GitHub issues.
@@ -69,22 +87,31 @@ def measure_time_to_first_response(issues):
69
87
issues_with_metrics = []
70
88
for issue in issues :
71
89
# Get the first comment
72
- first_comment = issue .comments ()[0 ] # type: ignore
73
-
74
- # Get the created_at time for the first comment
75
- first_comment_time = datetime .fromisoformat (first_comment .created_at ) # type: ignore
76
-
77
- # Get the created_at time for the issue
78
- issue_time = datetime .fromisoformat (issue .created_at ) # type: ignore
79
-
80
- # Calculate the time between the issue and the first comment
81
- time_to_first_response = first_comment_time - issue_time
82
-
83
- # Add the time to the issue
84
- issue .time_to_first_response = time_to_first_response
90
+ if issue .comments <= 0 :
91
+ first_comment_time = None
92
+ time_to_first_response = None
93
+ else :
94
+ comments = issue .issue .comments (
95
+ number = 1 , sort = "created" , direction = "asc"
96
+ ) # type: ignore
97
+ for comment in comments :
98
+ # Get the created_at time for the first comment
99
+ first_comment_time = comment .created_at # type: ignore
100
+
101
+ # Get the created_at time for the issue
102
+ issue_time = datetime .fromisoformat (issue .created_at ) # type: ignore
103
+
104
+ # Calculate the time between the issue and the first comment
105
+ time_to_first_response = first_comment_time - issue_time # type: ignore
85
106
86
107
# Add the issue to the list of issues with metrics
87
- issues_with_metrics .append (issue )
108
+ issues_with_metrics .append (
109
+ [
110
+ issue .title ,
111
+ issue .html_url ,
112
+ time_to_first_response ,
113
+ ]
114
+ )
88
115
89
116
return issues_with_metrics
90
117
@@ -97,21 +124,26 @@ def get_average_time_to_first_response(issues):
97
124
first response added as an attribute.
98
125
99
126
Returns:
100
- datetime.timedelta: The average time to first response for the issues.
127
+ datetime.timedelta: The average time to first response for the issues in seconds .
101
128
102
129
Raises:
103
130
TypeError: If the input is not a list of GitHub issues.
104
131
105
132
"""
106
133
total_time_to_first_response = 0
107
134
for issue in issues :
108
- total_time_to_first_response += issue . time_to_first_response .total_seconds ()
135
+ total_time_to_first_response += issue [ 2 ] .total_seconds ()
109
136
110
- average_time_to_first_response = total_time_to_first_response / len (
137
+ average_seconds_to_first_response = total_time_to_first_response / len (
111
138
issues
112
139
) # type: ignore
113
140
114
- return average_time_to_first_response
141
+ # Print the average time to first response converting seconds to a readable time format
142
+ print (
143
+ f"Average time to first response: { timedelta (seconds = average_seconds_to_first_response )} "
144
+ )
145
+
146
+ return timedelta (seconds = average_seconds_to_first_response )
115
147
116
148
117
149
def write_to_markdown (issues_with_metrics , average_time_to_first_response , file = None ):
@@ -136,10 +168,10 @@ def write_to_markdown(issues_with_metrics, average_time_to_first_response, file=
136
168
f"Average time to first response: { average_time_to_first_response } \n "
137
169
)
138
170
file .write (f"Number of issues: { len (issues_with_metrics )} \n \n " )
139
- file .write ("| Issue | TTFR |\n " )
140
- file .write ("| --- | ---: |\n " )
141
- for issue , ttfr in issues_with_metrics :
142
- file .write (f"| { issue } | { ttfr } |\n " )
171
+ file .write ("| Title | URL | TTFR |\n " )
172
+ file .write ("| --- | --- | --- : |\n " )
173
+ for title , url , ttfr in issues_with_metrics :
174
+ file .write (f"| { title } | { url } | { ttfr } |\n " )
143
175
print ("Wrote issue metrics to issue_metrics.md" )
144
176
145
177
@@ -170,25 +202,19 @@ def main():
170
202
if not issue_search_query :
171
203
raise ValueError ("ISSUE_SEARCH_QUERY environment variable not set" )
172
204
173
- issue_search_query = os .getenv ("REPOSITORY_URL" )
174
- if not issue_search_query :
205
+ repo_url = os .getenv ("REPOSITORY_URL" )
206
+ if not repo_url :
175
207
raise ValueError ("REPOSITORY_URL environment variable not set" )
176
208
177
209
# Search for issues
178
- issues = search_issues (issue_search_query , issue_search_query , github_connection )
179
-
180
- # Print the number of issues found
181
- print (f"Found { len (issues )} issues" )
210
+ issues = search_issues (repo_url , issue_search_query , github_connection )
182
211
183
212
# Find the time to first response
184
213
issues_with_ttfr = measure_time_to_first_response (issues )
185
214
average_time_to_first_response = get_average_time_to_first_response (
186
215
issues_with_ttfr
187
216
)
188
217
189
- # Print the average time to first response
190
- print (f"Average time to first response: { average_time_to_first_response } " )
191
-
192
218
# Write the results to a markdown file
193
219
write_to_markdown (issues_with_ttfr , average_time_to_first_response )
194
220
0 commit comments