-
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Expand file tree
/
Copy pathget_comment.py
More file actions
374 lines (318 loc) · 11.8 KB
/
Copy pathget_comment.py
File metadata and controls
374 lines (318 loc) · 11.8 KB
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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# 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.
import os
import re
from github import Auth, Github, GithubException
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.
"""
try:
with open(versions_file, "r") as f:
return dict(line.strip().split("=") for line in f)
except FileNotFoundError:
# If the versions file wasn't created due to an early failure in the
# linting workflow, we still want the bot to comment on the PR with the
# merging upstream/main work-around message
return {}
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 = (
f"-----------------------------------------------\n### {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_str, pr_number, sha, run_id, details, versions):
sub_text = (
"\n\n<sub> _Generated for commit:"
f" [{sha[:7]}](https://github.com/{repo_str}/pull/{pr_number}/commits/{sha}). "
"Link to the linter CI: [here]"
f"(https://github.com/{repo_str}/actions/runs/{run_id})_ </sub>"
)
try:
with open(log_file, "r") as f:
log = f.read()
except FileNotFoundError:
# If the log file wasn't created due to an early failure in the linting
# workflow, we still want the bot to comment on the PR with the merging
# upstream/main work-around message
log = ""
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"
"#development-workflow)) 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 = ""
# ruff check
message += get_step_message(
log,
start="### Running the ruff linter ###",
end="Problems detected by ruff check",
title="`ruff check`",
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,
)
# ruff format
message += get_step_message(
log,
start="### Running the ruff formatter ###",
end="Problems detected by ruff format",
title="`ruff format`",
message=(
"`ruff` detected issues. Please run `ruff format` locally and push "
"the changes. Here you can see the detected issues. Note that the "
f"installed `ruff` version is `ruff={versions['ruff']}`."
),
details=details,
)
# pyrefly
message += get_step_message(
log,
start="### Running pyrefly ###",
end="Problems detected by pyrefly",
title="`pyrefly`",
message=(
"`pyrefly` detected issues. Please fix them locally and push the changes. "
"Here you can see the detected issues. Note that the installed `pyrefly` "
f"version is `pyrefly={versions['pyrefly']}`."
),
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,
)
# sphinx-lint
message += get_step_message(
log,
start="### Running sphinx-lint ###",
end="Problems detected by sphinx-lint",
title="`sphinx-lint`",
message=(
"`sphinx-lint` detected issues in the documentation. Please fix them "
"locally and push the changes. Here you can see the detected issues. Note "
"that the installed `sphinx-lint` version is "
f"`sphinx-lint={versions['sphinx-lint']}`."
),
details=details,
)
# codespell
message += get_step_message(
log,
start="### Running codespell ###",
end="Problems detected by codespell",
title="`codespell`",
message=(
"`codespell` detected likely spelling mistakes. Please fix them locally "
"and push the changes. If a word is a false positive, add it to "
"`build_tools/codespell_ignore_words.txt`. Here you can see the detected "
"issues. Note that the installed `codespell` version is "
f"`codespell={versions['codespell']}`."
),
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, the linting succeeded
return None
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/development_setup.html#set-up-pre-commit)"
+ ".\n\n"
+ "You can see the details of the linting issues under the `lint` job [here]"
+ f"(https://github.com/{repo_str}/actions/runs/{run_id})\n\n"
+ message
+ sub_text
)
return message
def find_lint_bot_comments(issue):
"""Get the comment from the linting bot."""
failed_comment = "❌ Linting issues"
for comment in issue.get_comments():
if comment.user.login == "github-actions[bot]":
if failed_comment in comment.body:
return comment
return None
def create_or_update_comment(comment, message, issue):
"""Create a new comment or update the existing linting comment."""
if comment is not None:
print("Updating existing comment")
comment.edit(message)
else:
print("Creating new comment")
issue.create_comment(message)
def update_linter_fails_label(linting_failed, issue):
"""Add or remove the label indicating that the linting has failed."""
label = "CI:Linter failure"
if linting_failed:
issue.add_to_labels(label)
else:
try:
issue.remove_from_labels(label)
except GithubException as exception:
# The exception is ignored if raised because the issue did not have the
# label already
if not exception.message == "Label does not exist":
raise
if __name__ == "__main__":
repo_str = 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)
for var, val in [
("GITHUB_REPOSITORY", repo_str),
("GITHUB_TOKEN", token),
("PR_NUMBER", pr_number),
("LOG_FILE", log_file),
("RUN_ID", run_id),
]:
if not val:
raise ValueError(f"The following environment variable is not set: {var}")
if not re.match(r"\d+$", pr_number):
raise ValueError(f"PR_NUMBER should be a number, got {pr_number!r} instead")
pr_number = int(pr_number)
gh = Github(auth=Auth.Token(token))
repo = gh.get_repo(repo_str)
issue = repo.get_issue(number=pr_number)
message = get_message(
log_file,
repo_str=repo_str,
pr_number=pr_number,
sha=sha,
run_id=run_id,
details=True,
versions=versions,
)
update_linter_fails_label(
linting_failed=message is not None,
issue=issue,
)
comment = find_lint_bot_comments(issue)
if message is None: # linting succeeded
if comment is not None:
print("Deleting existing comment.")
comment.delete()
else:
try:
create_or_update_comment(comment, message, issue)
print(message)
except GithubException:
# The above fails if the message is too long. In that case, we
# try again without the details.
message = get_message(
log_file,
repo_str=repo_str,
pr_number=pr_number,
sha=sha,
run_id=run_id,
details=False,
versions=versions,
)
create_or_update_comment(comment, message, issue)
print(message)