Skip to content

Commit 763815d

Browse files
authored
pr_labeler: refactor new_contributor_welcome code (#990)
* pr_labeler: add GlobalArgs.full_repo property * pr_labeler: refactor new_contributor_welcome code As of #69, the pr_labeler responds with a welcome message when an issue or PR is opened by a new contributor. It turns out this never actually worked properly. The previous method that relied on Github's `author_association` flag did not work with the app token that the pr_labeler uses. This refactors the code to figure out whether a user is a new contributor by searching the list of issues and PRs. Fixes: #204 * pr_labeler: address potential race condition
1 parent 2d1ed76 commit 763815d

File tree

1 file changed

+65
-21
lines changed

1 file changed

+65
-21
lines changed

hacking/pr_labeler/label.py

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import json
88
import os
99
import re
10-
from collections.abc import Collection
10+
from collections.abc import Callable, Collection
1111
from contextlib import suppress
1212
from functools import cached_property
1313
from pathlib import Path
@@ -36,6 +36,7 @@
3636
trim_blocks=True,
3737
undefined=StrictUndefined,
3838
)
39+
NEW_CONTRIBUTOR_LABEL = "new_contributor"
3940

4041
IssueOrPrCtx = Union["IssueLabelerCtx", "PRLabelerCtx"]
4142
IssueOrPr = Union["github.Issue.Issue", "github.PullRequest.PullRequest"]
@@ -48,12 +49,12 @@ def log(ctx: IssueOrPrCtx, *args: object) -> None:
4849

4950

5051
def get_repo(
51-
*, authed: bool = True, owner: str, repo: str
52+
args: GlobalArgs, authed: bool = True
5253
) -> tuple[github.Github, github.Repository.Repository]:
5354
gclient = github.Github(
5455
auth=github.Auth.Token(os.environ["GITHUB_TOKEN"]) if authed else None,
5556
)
56-
repo_obj = gclient.get_repo(f"{owner}/{repo}")
57+
repo_obj = gclient.get_repo(args.full_repo)
5758
return gclient, repo_obj
5859

5960

@@ -70,6 +71,11 @@ def get_event_info() -> dict[str, Any]:
7071
class GlobalArgs:
7172
owner: str
7273
repo: str
74+
use_author_association: bool
75+
76+
@property
77+
def full_repo(self) -> str:
78+
return f"{self.owner}/{self.repo}"
7379

7480

7581
@dataclasses.dataclass()
@@ -79,6 +85,7 @@ class LabelerCtx:
7985
dry_run: bool
8086
event_info: dict[str, Any]
8187
issue: github.Issue.Issue
88+
global_args: GlobalArgs
8289

8390
TYPE: ClassVar[str]
8491

@@ -211,24 +218,57 @@ def add_label_if_new(ctx: IssueOrPrCtx, labels: Collection[str] | str) -> None:
211218
ctx.member.add_to_labels(*labels)
212219

213220

214-
def new_contributor_welcome(ctx: IssueOrPrCtx) -> None:
221+
def is_new_contributor_assoc(ctx: IssueOrPrCtx) -> bool:
215222
"""
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.
217226
"""
218-
# This contributor has already been welcomed!
219-
if "new_contributor" in ctx.previously_labeled:
220-
return
221227
author_association = ctx.event_member.get(
222228
"author_association", ctx.member.raw_data["author_association"]
223229
)
224230
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+
):
229269
return
230270
log(ctx, "Welcoming new contributor")
231-
add_label_if_new(ctx, "new_contributor")
271+
add_label_if_new(ctx, NEW_CONTRIBUTOR_LABEL)
232272
create_comment(ctx, get_data_file("docs_team_info.md"))
233273

234274

@@ -277,11 +317,17 @@ def warn_porting_guide_change(ctx: PRLabelerCtx) -> None:
277317

278318

279319
@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+
):
281327
"""
282328
Basic triager for ansible/ansible-documentation
283329
"""
284-
click_ctx.obj = GlobalArgs(owner, repo)
330+
click_ctx.obj = GlobalArgs(owner, repo, use_author_association)
285331

286332

287333
@APP.command(name="pr")
@@ -300,9 +346,7 @@ def process_pr(
300346
dry_run = True
301347
authed = True
302348

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)
306350
pr = repo.get_pull(pr_number)
307351
ctx = PRLabelerCtx(
308352
client=gclient,
@@ -311,6 +355,7 @@ def process_pr(
311355
dry_run=dry_run,
312356
event_info=get_event_info(),
313357
issue=pr.as_issue(),
358+
global_args=global_args,
314359
)
315360
if not force_process_closed and pr.state != "open":
316361
log(ctx, "Refusing to process closed ticket")
@@ -337,16 +382,15 @@ def process_issue(
337382
if authed_dry_run:
338383
dry_run = True
339384
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)
343386
issue = repo.get_issue(issue_number)
344387
ctx = IssueLabelerCtx(
345388
client=gclient,
346389
repo=repo,
347390
issue=issue,
348391
dry_run=dry_run,
349392
event_info=get_event_info(),
393+
global_args=global_args,
350394
)
351395
if not force_process_closed and issue.state != "open":
352396
log(ctx, "Refusing to process closed ticket")

0 commit comments

Comments
 (0)