Skip to content

Commit befb79f

Browse files
authored
Merge pull request #344 from open-craft/navin/merged-closed-date
feat: automatically fill closed or merged date in project
2 parents 1efb842 + 8f36619 commit befb79f

8 files changed

+90
-22
lines changed

openedx_webhooks/gh_projects.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from glom import glom
88

99
from openedx_webhooks.tasks import logger
10-
from openedx_webhooks.types import GhPrMetaDict, GhProject, PrDict, PrId
10+
from openedx_webhooks.types import GhPrMetaDict, GhProject, PrDict, PrGhProject, PrId
1111
from openedx_webhooks.utils import graphql_query, memoize_timed, value_graphql_type
1212

1313
# The name of the query is used by FakeGitHub while testing.
@@ -39,12 +39,10 @@
3939
"""
4040

4141

42-
def pull_request_projects(pr: PrDict) -> Set[GhProject]:
43-
"""Return the projects this PR is in.
44-
45-
The projects are expressed as sets of tuples with owning org and number:
46-
{("openedx", 19)}
42+
def pull_request_projects_info(pr: PrDict) -> list[PrGhProject]:
43+
"""Return the projects info this PR is in.
4744
45+
example: [{"id": "some-id", "org": "login_id", "number": "1"}]
4846
"""
4947
variables = glom(pr, {
5048
"owner": "base.repo.owner.login",
@@ -58,11 +56,20 @@ def pull_request_projects(pr: PrDict) -> Set[GhProject]:
5856
(
5957
"repository.pullRequest.projectItems.nodes",
6058
[
61-
{"org": "project.owner.login", "number": "project.number"}
59+
{"id": "id", "org": "project.owner.login", "number": "project.number"}
6260
]
6361
)
6462
)
65-
# I can't figure out how to get glom to make a tuple directly...
63+
return projects
64+
65+
66+
def pull_request_projects(pr: PrDict, projects: list[PrGhProject] | None = None) -> Set[GhProject]:
67+
"""
68+
Helper method for expressing projects info as sets of tuples with owning org and number:
69+
{("openedx", 19)}
70+
"""
71+
if projects is None:
72+
projects = pull_request_projects_info(pr)
6673
return {(p["org"], p["number"]) for p in projects}
6774

6875

openedx_webhooks/tasks/pr_tracking.py

+49-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from dataclasses import dataclass, field
1212
from typing import Dict, List, Optional, Set, cast
1313

14+
from openedx_webhooks import settings
1415
from openedx_webhooks.auth import get_github_session, get_jira_session
1516
from openedx_webhooks.bot_comments import (
1617
BOT_COMMENT_INDICATORS,
@@ -39,6 +40,7 @@
3940
from openedx_webhooks.gh_projects import (
4041
add_pull_request_to_project,
4142
pull_request_projects,
43+
pull_request_projects_info,
4244
update_project_pr_custom_field,
4345
)
4446
from openedx_webhooks.info import (
@@ -63,12 +65,11 @@
6365
GITHUB_MERGED_PR_OBSOLETE_LABELS,
6466
GITHUB_STATUS_LABELS,
6567
)
66-
from openedx_webhooks import settings
6768
from openedx_webhooks.tasks import logger
6869
from openedx_webhooks.tasks.jira_work import (
6970
update_jira_issue,
7071
)
71-
from openedx_webhooks.types import GhProject, JiraId, PrDict, PrId
72+
from openedx_webhooks.types import GhProject, JiraId, PrDict, PrGhProject, PrId
7273
from openedx_webhooks.utils import (
7374
get_pr_state,
7475
log_check_response,
@@ -125,6 +126,9 @@ class PrCurrentInfo:
125126
# The GitHub projects the PR is in.
126127
github_projects: Set[GhProject] = field(default_factory=set)
127128

129+
# The GitHub projects the PR is in.
130+
github_projects_info: list[PrGhProject] = field(default_factory=list)
131+
128132
# The status of the cla check.
129133
cla_check: Optional[Dict[str, str]] = None
130134

@@ -190,7 +194,8 @@ def current_support_state(pr: PrDict) -> PrCurrentInfo:
190194
current.bot_data.update(extract_data_from_comment(body))
191195

192196
current.github_labels = set(lbl["name"] for lbl in pr["labels"])
193-
current.github_projects = set(pull_request_projects(pr))
197+
current.github_projects_info = pull_request_projects_info(pr)
198+
current.github_projects = pull_request_projects(pr, current.github_projects_info)
194199
current.cla_check = cla_status_on_pr(pr)
195200

196201
return current
@@ -402,6 +407,9 @@ def _fix_ospr(self) -> None:
402407
# Check the GitHub projects.
403408
self._fix_projects()
404409

410+
# Update fields in project for this PR
411+
self._fix_project_node_fields()
412+
405413
def _fix_projects(self) -> None:
406414
"""
407415
Update projects for pr.
@@ -410,19 +418,38 @@ def _fix_projects(self) -> None:
410418
project_item_id = self.actions.add_pull_request_to_project(
411419
pr_node_id=self.pr["node_id"], project=project
412420
)
413-
if not project_item_id:
414-
continue
421+
self.current.github_projects_info.append({
422+
"id": project_item_id,
423+
"org": project[0],
424+
"number": project[1],
425+
})
426+
427+
def _fix_project_node_fields(self) -> None:
428+
"""
429+
Update pr fields in OSPR project board.
430+
"""
431+
for project in self.current.github_projects_info:
432+
if (
433+
project["org"] == settings.GITHUB_OSPR_PROJECT[0]
434+
and project["number"] == settings.GITHUB_OSPR_PROJECT[1]
435+
):
436+
project_item_id = project["id"]
437+
break
438+
else:
439+
return
440+
state = get_pr_state(self.pr)
441+
if state == "open":
415442
self.actions.update_project_pr_custom_field(
416443
field_name="Date opened",
417444
field_value=self.pr["created_at"],
418445
item_id=project_item_id,
419-
project=project
446+
project=settings.GITHUB_OSPR_PROJECT
420447
)
421448
# get base repo owner info
422449
repo_spec = get_repo_spec(self.pr["base"]["repo"]["full_name"])
423450
owner = repo_spec.owner
424451
if not owner:
425-
continue
452+
return
426453
# get user info if owner is an individual
427454
if repo_spec.is_owner_individual:
428455
owner_info = get_github_user_info(owner)
@@ -432,7 +459,21 @@ def _fix_projects(self) -> None:
432459
field_name="Repo Owner / Owning Team",
433460
field_value=owner,
434461
item_id=project_item_id,
435-
project=project
462+
project=settings.GITHUB_OSPR_PROJECT
463+
)
464+
elif state == "merged":
465+
self.actions.update_project_pr_custom_field(
466+
field_name="Date merged/closed",
467+
field_value=self.pr["merged_at"],
468+
item_id=project_item_id,
469+
project=settings.GITHUB_OSPR_PROJECT
470+
)
471+
elif state == "closed":
472+
self.actions.update_project_pr_custom_field(
473+
field_name="Date merged/closed",
474+
field_value=self.pr["closed_at"],
475+
item_id=project_item_id,
476+
project=settings.GITHUB_OSPR_PROJECT
436477
)
437478

438479
def _make_jira_issue(self, jira_nick) -> None:

openedx_webhooks/types.py

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
# A GitHub project: org name, and number.
1818
GhProject = Tuple[str, int]
1919

20+
# A GitHub project info: org name, number and pr node id in project.
21+
PrGhProject = Dict
22+
2023
# A GitHub project metadata json object.
2124
GhPrMetaDict = Dict
2225

tests/fake_github.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class PullRequest:
113113
node_id: str = field(default_factory=fake_node_id)
114114
created_at: datetime.datetime = field(default_factory=patchable_now)
115115
closed_at: Optional[datetime.datetime] = None
116+
merged_at: Optional[datetime.datetime] = None
116117
comments: List[int] = field(default_factory=list)
117118
labels: Set[str] = field(default_factory=set)
118119
state: str = "open"
@@ -140,6 +141,7 @@ def as_json(self, brief=False) -> Dict:
140141
},
141142
"created_at": self.created_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
142143
"closed_at": self.closed_at.strftime("%Y-%m-%dT%H:%M:%SZ") if self.closed_at else None,
144+
"merged_at": self.merged_at.strftime("%Y-%m-%dT%H:%M:%SZ") if self.merged_at else None,
143145
"url": f"{self.repo.github.host}/repos/{self.repo.full_name}/pulls/{self.number}",
144146
"html_url": f"https://github.com/{self.repo.full_name}/pull/{self.number}",
145147
}
@@ -154,6 +156,7 @@ def close(self, merge=False):
154156
self.state = "closed"
155157
self.merged = merge
156158
self.closed_at = datetime.datetime.now()
159+
self.merged_at = datetime.datetime.now()
157160

158161
def reopen(self):
159162
"""
@@ -162,6 +165,7 @@ def reopen(self):
162165
self.state = "open"
163166
self.merged = False
164167
self.closed_at = None
168+
self.merged_at = None
165169

166170
def add_comment(self, user="someone", **kwargs) -> Comment:
167171
comment = self.repo.make_comment(user, **kwargs)
@@ -461,7 +465,7 @@ def _graphql_ProjectsForPr(self, owner: str, name: str, number: int) -> Dict:
461465
for node_id in project_node_ids:
462466
org, num = self.project_nodes[node_id]
463467
nodes.append(
464-
{"project": {"owner": {"login": org}, "number": num}}
468+
{"project": {"owner": {"login": org}, "number": num}, "id": node_id}
465469
)
466470
return {
467471
"data": {
@@ -518,6 +522,7 @@ def _graphql_OrgProjectMetadata(self, orgname: str, number: int) -> dict:
518522
"nodes": [
519523
{"name": "Name", "id": "name-id", "dataType": "text"},
520524
{"name": "Date opened", "id": "date-opened-id", "dataType": "date"},
525+
{"name": "Date merged/closed", "id": "date-closed-id", "dataType": "date"},
521526
{"name": "Repo Owner / Owning Team", "id": "repo-owner-id", "dataType": "text"},
522527
]
523528
}

tests/test_gh_projects.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from openedx_webhooks.gh_projects import (
44
add_pull_request_to_project,
55
pull_request_projects,
6+
pull_request_projects_info,
67
)
78
from openedx_webhooks.types import PrId
89

@@ -16,7 +17,9 @@ def test_adding_pr_to_project(fake_github):
1617
assert not pr.is_in_project(("myorg", 23))
1718

1819
add_pull_request_to_project(prid, pr.node_id, ("myorg", 23))
19-
projects = set(pull_request_projects(prj))
20+
project_info = pull_request_projects_info(prj)
21+
assert project_info == [{'id': 'PROJECT:myorg.23', 'org': 'myorg', 'number': 23}]
22+
projects = set(pull_request_projects(prj, project_info))
2023
assert projects == {("myorg", 23)}
2124
assert pr.is_in_project(("myorg", 23))
2225
assert not pr.is_in_project(("anotherorg", 27))

tests/test_pull_request_closed.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22

33
import pytest
44

5-
from openedx_webhooks.bot_comments import (
6-
BotComment,
7-
)
85
from openedx_webhooks.cla_check import (
96
CLA_CONTEXT,
107
CLA_STATUS_GOOD,
11-
CLA_STATUS_NO_CONTRIBUTIONS,
128
)
139
from openedx_webhooks.gh_projects import pull_request_projects
1410
from openedx_webhooks.tasks.github import pull_request_changed
11+
1512
from .helpers import check_issue_link_in_markdown, random_text
1613

1714
# These tests should run when we want to test flaky GitHub behavior.
@@ -139,3 +136,11 @@ def test_pr_closed_labels(fake_github, is_merged):
139136
pr.close(merge=is_merged)
140137
pull_request_changed(pr.as_json())
141138
assert pr.labels == {"open-source-contribution", "custom label 1"}
139+
140+
141+
def test_pr_closed_date_on_close(closed_pull_request):
142+
pr = closed_pull_request
143+
pull_request_changed(pr.as_json())
144+
assert pr.repo.github.project_items['date-closed-id'] == {
145+
pr.closed_at.isoformat(timespec='seconds') + 'Z',
146+
}

tests/test_pull_request_opened.py

+1
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ def test_pr_project_fields_data(fake_github, mocker, owner):
499499
pr = fake_github.make_pull_request(owner="openedx", repo="edx-platform", created_at=created_at)
500500
pull_request_changed(pr.as_json())
501501
assert pr.repo.github.project_items['date-opened-id'] == {created_at.isoformat() + 'Z'}
502+
assert pr.repo.github.project_items['date-closed-id'] == set()
502503
owner_type, owner_name = owner.split(":")
503504
if owner_type == "user":
504505
assert pr.repo.github.project_items['repo-owner-id'] == {f"{owner_name.title()} (@{owner_name})"}

tests/test_rescan.py

+3
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,19 @@ def test_rescan_repository_dry_run(rescannable_repo, fake_github, fake_jira, pul
111111
"update_labels_on_pull_request", # ["open-source-contribution"]
112112
"add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!"
113113
"add_pull_request_to_project",
114+
"update_project_pr_custom_field",
114115
],
115116
106: [
116117
"initial_state",
117118
"set_cla_status", # "The author is authorized to contribute"
118119
"update_labels_on_pull_request", # ["open-source-contribution"]
119120
"add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!"
120121
"add_pull_request_to_project",
122+
"update_project_pr_custom_field",
121123
],
122124
108: [
123125
"initial_state",
126+
"update_project_pr_custom_field",
124127
],
125128
110: [
126129
"initial_state",

0 commit comments

Comments
 (0)