-
-
Notifications
You must be signed in to change notification settings - Fork 25.5k
/
get_comment.py
356 lines (308 loc) · 11.4 KB
/
get_comment.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# This script is used to generate a comment for a PR when linting issues are
# detected. It is used by the `Comment on failed linting` GitHub Action.
# This script fails if there are not comments to be posted.
import os
import requests
def get_versions(versions_file):
"""Get the versions of the packages used in the linter job.
Parameters
----------
versions_file : str
The path to the file that contains the versions of the packages.
Returns
-------
versions : dict
A dictionary with the versions of the packages.
"""
with open("versions.txt", "r") as f:
return dict(line.strip().split("=") for line in f)
def get_step_message(log, start, end, title, message, details):
"""Get the message for a specific test.
Parameters
----------
log : str
The log of the linting job.
start : str
The string that marks the start of the test.
end : str
The string that marks the end of the test.
title : str
The title for this section.
message : str
The message to be added at the beginning of the section.
details : bool
Whether to add the details of each step.
Returns
-------
message : str
The message to be added to the comment.
"""
if end not in log:
return ""
res = (
"-----------------------------------------------\n"
+ f"### {title}\n\n"
+ message
+ "\n\n"
)
if details:
res += (
"<details>\n\n```\n"
+ log[log.find(start) + len(start) + 1 : log.find(end) - 1]
+ "\n```\n\n</details>\n\n"
)
return res
def get_message(log_file, repo, pr_number, sha, run_id, details, versions):
with open(log_file, "r") as f:
log = f.read()
sub_text = (
"\n\n<sub> _Generated for commit:"
f" [{sha[:7]}](https://github.com/{repo}/pull/{pr_number}/commits/{sha}). "
"Link to the linter CI: [here]"
f"(https://github.com/{repo}/actions/runs/{run_id})_ </sub>"
)
if "### Linting completed ###" not in log:
return (
"## ❌ Linting issues\n\n"
"There was an issue running the linter job. Please update with "
"`upstream/main` ([link]("
"https://scikit-learn.org/dev/developers/contributing.html"
"#how-to-contribute)) and push the changes. If you already have done "
"that, please send an empty commit with `git commit --allow-empty` "
"and push the changes to trigger the CI.\n\n" + sub_text
)
message = ""
# black
message += get_step_message(
log,
start="### Running black ###",
end="Problems detected by black",
title="`black`",
message=(
"`black` detected issues. Please run `black .` locally and push "
"the changes. Here you can see the detected issues. Note that "
"running black might also fix some of the issues which might be "
"detected by `ruff`. Note that the installed `black` version is "
f"`black={versions['black']}`."
),
details=details,
)
# ruff
message += get_step_message(
log,
start="### Running ruff ###",
end="Problems detected by ruff",
title="`ruff`",
message=(
"`ruff` detected issues. Please run "
"`ruff check --fix --output-format=full .` locally, fix the remaining "
"issues, and push the changes. Here you can see the detected issues. Note "
f"that the installed `ruff` version is `ruff={versions['ruff']}`."
),
details=details,
)
# mypy
message += get_step_message(
log,
start="### Running mypy ###",
end="Problems detected by mypy",
title="`mypy`",
message=(
"`mypy` detected issues. Please fix them locally and push the changes. "
"Here you can see the detected issues. Note that the installed `mypy` "
f"version is `mypy={versions['mypy']}`."
),
details=details,
)
# cython-lint
message += get_step_message(
log,
start="### Running cython-lint ###",
end="Problems detected by cython-lint",
title="`cython-lint`",
message=(
"`cython-lint` detected issues. Please fix them locally and push "
"the changes. Here you can see the detected issues. Note that the "
"installed `cython-lint` version is "
f"`cython-lint={versions['cython-lint']}`."
),
details=details,
)
# deprecation order
message += get_step_message(
log,
start="### Checking for bad deprecation order ###",
end="Problems detected by deprecation order check",
title="Deprecation Order",
message=(
"Deprecation order check detected issues. Please fix them locally and "
"push the changes. Here you can see the detected issues."
),
details=details,
)
# doctest directives
message += get_step_message(
log,
start="### Checking for default doctest directives ###",
end="Problems detected by doctest directive check",
title="Doctest Directives",
message=(
"doctest directive check detected issues. Please fix them locally and "
"push the changes. Here you can see the detected issues."
),
details=details,
)
# joblib imports
message += get_step_message(
log,
start="### Checking for joblib imports ###",
end="Problems detected by joblib import check",
title="Joblib Imports",
message=(
"`joblib` import check detected issues. Please fix them locally and "
"push the changes. Here you can see the detected issues."
),
details=details,
)
if not message:
# no issues detected, so this script "fails"
return (
"## ✔️ Linting Passed\n"
"All linting checks passed. Your pull request is in excellent shape! ☀️"
+ sub_text
)
if not details:
# This happens if posting the log fails, which happens if the log is too
# long. Typically, this happens if the PR branch hasn't been updated
# since we've introduced import sorting.
branch_not_updated = (
"_Merging with `upstream/main` might fix / improve the issues if you "
"haven't done that since 21.06.2023._\n\n"
)
else:
branch_not_updated = ""
message = (
"## ❌ Linting issues\n\n"
+ branch_not_updated
+ "This PR is introducing linting issues. Here's a summary of the issues. "
+ "Note that you can avoid having linting issues by enabling `pre-commit` "
+ "hooks. Instructions to enable them can be found [here]("
+ "https://scikit-learn.org/dev/developers/contributing.html#how-to-contribute)"
+ ".\n\n"
+ "You can see the details of the linting issues under the `lint` job [here]"
+ f"(https://github.com/{repo}/actions/runs/{run_id})\n\n"
+ message
+ sub_text
)
return message
def get_headers(token):
"""Get the headers for the GitHub API."""
return {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
}
def find_lint_bot_comments(repo, token, pr_number):
"""Get the comment from the linting bot."""
# repo is in the form of "org/repo"
# API doc: https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#list-issue-comments # noqa
response = requests.get(
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
headers=get_headers(token),
)
response.raise_for_status()
all_comments = response.json()
failed_comment = "❌ Linting issues"
success_comment = "✔️ Linting Passed"
# Find all comments that match the linting bot, and return the first one.
# There should always be only one such comment, or none, if the PR is
# just created.
comments = [
comment
for comment in all_comments
if comment["user"]["login"] == "github-actions[bot]"
and (failed_comment in comment["body"] or success_comment in comment["body"])
]
if len(all_comments) > 25 and not comments:
# By default the API returns the first 30 comments. If we can't find the
# comment created by the bot in those, then we raise and we skip creating
# a comment in the first place.
raise RuntimeError("Comment not found in the first 30 comments.")
return comments[0] if comments else None
def create_or_update_comment(comment, message, repo, pr_number, token):
"""Create a new comment or update existing one."""
# repo is in the form of "org/repo"
if comment is not None:
print("updating existing comment")
# API doc: https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#update-an-issue-comment # noqa
response = requests.patch(
f"https://api.github.com/repos/{repo}/issues/comments/{comment['id']}",
headers=get_headers(token),
json={"body": message},
)
else:
print("creating new comment")
# API doc: https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment # noqa
response = requests.post(
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
headers=get_headers(token),
json={"body": message},
)
response.raise_for_status()
if __name__ == "__main__":
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITHUB_TOKEN"]
pr_number = os.environ["PR_NUMBER"]
sha = os.environ["BRANCH_SHA"]
log_file = os.environ["LOG_FILE"]
run_id = os.environ["RUN_ID"]
versions_file = os.environ["VERSIONS_FILE"]
versions = get_versions(versions_file)
if not repo or not token or not pr_number or not log_file or not run_id:
raise ValueError(
"One of the following environment variables is not set: "
"GITHUB_REPOSITORY, GITHUB_TOKEN, PR_NUMBER, LOG_FILE, RUN_ID"
)
try:
comment = find_lint_bot_comments(repo, token, pr_number)
except RuntimeError:
print("Comment not found in the first 30 comments. Skipping!")
exit(0)
try:
message = get_message(
log_file,
repo=repo,
pr_number=pr_number,
sha=sha,
run_id=run_id,
details=True,
versions=versions,
)
create_or_update_comment(
comment=comment,
message=message,
repo=repo,
pr_number=pr_number,
token=token,
)
print(message)
except requests.HTTPError:
# The above fails if the message is too long. In that case, we
# try again without the details.
message = get_message(
log_file,
repo=repo,
pr_number=pr_number,
sha=sha,
run_id=run_id,
details=False,
versions=versions,
)
create_or_update_comment(
comment=comment,
message=message,
repo=repo,
pr_number=pr_number,
token=token,
)
print(message)