7
7
import json
8
8
import os
9
9
import re
10
- from collections .abc import Collection
10
+ from collections .abc import Callable , Collection
11
11
from contextlib import suppress
12
12
from functools import cached_property
13
13
from pathlib import Path
36
36
trim_blocks = True ,
37
37
undefined = StrictUndefined ,
38
38
)
39
+ NEW_CONTRIBUTOR_LABEL = "new_contributor"
39
40
40
41
IssueOrPrCtx = Union ["IssueLabelerCtx" , "PRLabelerCtx" ]
41
42
IssueOrPr = Union ["github.Issue.Issue" , "github.PullRequest.PullRequest" ]
@@ -48,12 +49,12 @@ def log(ctx: IssueOrPrCtx, *args: object) -> None:
48
49
49
50
50
51
def get_repo (
51
- * , authed : bool = True , owner : str , repo : str
52
+ args : GlobalArgs , authed : bool = True
52
53
) -> tuple [github .Github , github .Repository .Repository ]:
53
54
gclient = github .Github (
54
55
auth = github .Auth .Token (os .environ ["GITHUB_TOKEN" ]) if authed else None ,
55
56
)
56
- repo_obj = gclient .get_repo (f" { owner } / { repo } " )
57
+ repo_obj = gclient .get_repo (args . full_repo )
57
58
return gclient , repo_obj
58
59
59
60
@@ -70,6 +71,11 @@ def get_event_info() -> dict[str, Any]:
70
71
class GlobalArgs :
71
72
owner : str
72
73
repo : str
74
+ use_author_association : bool
75
+
76
+ @property
77
+ def full_repo (self ) -> str :
78
+ return f"{ self .owner } /{ self .repo } "
73
79
74
80
75
81
@dataclasses .dataclass ()
@@ -79,6 +85,7 @@ class LabelerCtx:
79
85
dry_run : bool
80
86
event_info : dict [str , Any ]
81
87
issue : github .Issue .Issue
88
+ global_args : GlobalArgs
82
89
83
90
TYPE : ClassVar [str ]
84
91
@@ -211,24 +218,57 @@ def add_label_if_new(ctx: IssueOrPrCtx, labels: Collection[str] | str) -> None:
211
218
ctx .member .add_to_labels (* labels )
212
219
213
220
214
- def new_contributor_welcome (ctx : IssueOrPrCtx ) -> None :
221
+ def is_new_contributor_assoc (ctx : IssueOrPrCtx ) -> bool :
215
222
"""
216
- Welcome a new contributor to the repo with a message and a label
223
+ Determine whether a user has previously contributed.
224
+ Requires authentication as a regular user and does not work with an app
225
+ token.
217
226
"""
218
- # This contributor has already been welcomed!
219
- if "new_contributor" in ctx .previously_labeled :
220
- return
221
227
author_association = ctx .event_member .get (
222
228
"author_association" , ctx .member .raw_data ["author_association" ]
223
229
)
224
230
log (ctx , "author_association is" , author_association )
225
- if author_association not in {
226
- "FIRST_TIMER" ,
227
- "FIRST_TIME_CONTRIBUTOR" ,
228
- }:
231
+ return author_association in {"FIRST_TIMER" , "FIRST_TIME_CONTRIBUTOR" }
232
+
233
+
234
+ def is_new_contributor_manual (ctx : IssueOrPrCtx ) -> bool :
235
+ """
236
+ Determine whether a user has previously opened an issue or PR in this repo
237
+ without needing special API access.
238
+ """
239
+ query_data = {
240
+ "repo" : "ansible/ansible-documentation" ,
241
+ "author" : ctx .issue .user .login ,
242
+ # Avoid potential race condition where a new contributor opens multiple
243
+ # PRs or issues at once.
244
+ # Better to welcome twice than not at all.
245
+ "is" : "closed" ,
246
+ }
247
+ issues = ctx .client .search_issues ("" , ** query_data )
248
+ for issue in issues :
249
+ if issue .number != ctx .issue .number :
250
+ return False
251
+ return True
252
+
253
+
254
+ def new_contributor_welcome (ctx : IssueOrPrCtx ) -> None :
255
+ """
256
+ Welcome a new contributor to the repo with a message and a label
257
+ """
258
+ is_new_contributor : Callable [[IssueOrPrCtx ], bool ] = (
259
+ is_new_contributor_assoc
260
+ if ctx .global_args .use_author_association
261
+ else is_new_contributor_manual
262
+ )
263
+ if (
264
+ # Contributor has already been welcomed
265
+ NEW_CONTRIBUTOR_LABEL in ctx .previously_labeled
266
+ #
267
+ or not is_new_contributor (ctx )
268
+ ):
229
269
return
230
270
log (ctx , "Welcoming new contributor" )
231
- add_label_if_new (ctx , "new_contributor" )
271
+ add_label_if_new (ctx , NEW_CONTRIBUTOR_LABEL )
232
272
create_comment (ctx , get_data_file ("docs_team_info.md" ))
233
273
234
274
@@ -277,11 +317,17 @@ def warn_porting_guide_change(ctx: PRLabelerCtx) -> None:
277
317
278
318
279
319
@APP .callback ()
280
- def cb (* , click_ctx : typer .Context , owner : str = OWNER , repo : str = REPO ):
320
+ def cb (
321
+ * ,
322
+ click_ctx : typer .Context ,
323
+ owner : str = OWNER ,
324
+ repo : str = REPO ,
325
+ use_author_association : bool = False ,
326
+ ):
281
327
"""
282
328
Basic triager for ansible/ansible-documentation
283
329
"""
284
- click_ctx .obj = GlobalArgs (owner , repo )
330
+ click_ctx .obj = GlobalArgs (owner , repo , use_author_association )
285
331
286
332
287
333
@APP .command (name = "pr" )
@@ -300,9 +346,7 @@ def process_pr(
300
346
dry_run = True
301
347
authed = True
302
348
303
- gclient , repo = get_repo (
304
- authed = authed , owner = global_args .owner , repo = global_args .repo
305
- )
349
+ gclient , repo = get_repo (global_args , authed )
306
350
pr = repo .get_pull (pr_number )
307
351
ctx = PRLabelerCtx (
308
352
client = gclient ,
@@ -311,6 +355,7 @@ def process_pr(
311
355
dry_run = dry_run ,
312
356
event_info = get_event_info (),
313
357
issue = pr .as_issue (),
358
+ global_args = global_args ,
314
359
)
315
360
if not force_process_closed and pr .state != "open" :
316
361
log (ctx , "Refusing to process closed ticket" )
@@ -337,16 +382,15 @@ def process_issue(
337
382
if authed_dry_run :
338
383
dry_run = True
339
384
authed = True
340
- gclient , repo = get_repo (
341
- authed = authed , owner = global_args .owner , repo = global_args .repo
342
- )
385
+ gclient , repo = get_repo (global_args , authed )
343
386
issue = repo .get_issue (issue_number )
344
387
ctx = IssueLabelerCtx (
345
388
client = gclient ,
346
389
repo = repo ,
347
390
issue = issue ,
348
391
dry_run = dry_run ,
349
392
event_info = get_event_info (),
393
+ global_args = global_args ,
350
394
)
351
395
if not force_process_closed and issue .state != "open" :
352
396
log (ctx , "Refusing to process closed ticket" )
0 commit comments