Skip to content

Commit 3e6737a

Browse files
Factor out reporting (#2332)
Factor out reporting Reviewed-by: Laura Barcziová
2 parents 20d49d3 + a1e347a commit 3e6737a

15 files changed

+690
-601
lines changed

packit_service/worker/reporting.py

-587
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
from packit_service.worker.reporting.enums import BaseCommitStatus, DuplicateCheckMode
5+
from packit_service.worker.reporting.reporters.base import StatusReporter
6+
from packit_service.worker.reporting.reporters.github import (
7+
StatusReporterGithubChecks,
8+
StatusReporterGithubStatuses,
9+
)
10+
from packit_service.worker.reporting.reporters.gitlab import StatusReporterGitlab
11+
from packit_service.worker.reporting.utils import (
12+
report_in_issue_repository,
13+
update_message_with_configured_failure_comment_message,
14+
)
15+
16+
__all__ = [
17+
BaseCommitStatus.__name__,
18+
StatusReporter.__name__,
19+
DuplicateCheckMode.__name__,
20+
report_in_issue_repository.__name__,
21+
update_message_with_configured_failure_comment_message.__name__,
22+
StatusReporterGithubChecks.__name__,
23+
StatusReporterGithubStatuses.__name__,
24+
StatusReporterGitlab.__name__,
25+
]
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
from enum import Enum, auto
5+
from typing import Dict, Union
6+
7+
from ogr.abstract import CommitStatus
8+
from ogr.services.github.check_run import (
9+
GithubCheckRunResult,
10+
GithubCheckRunStatus,
11+
)
12+
13+
14+
class DuplicateCheckMode(Enum):
15+
"""Enum of possible behaviour for handling duplicates when commenting."""
16+
17+
# Do not check for duplicates
18+
do_not_check = auto()
19+
# Check only last comment from us for duplicate
20+
check_last_comment = auto()
21+
# Check the whole comment list for duplicate
22+
check_all_comments = auto()
23+
24+
25+
class BaseCommitStatus(Enum):
26+
failure = "failure"
27+
neutral = "neutral"
28+
success = "success"
29+
pending = "pending"
30+
running = "running"
31+
error = "error"
32+
33+
34+
MAP_TO_COMMIT_STATUS: Dict[BaseCommitStatus, CommitStatus] = {
35+
BaseCommitStatus.pending: CommitStatus.pending,
36+
BaseCommitStatus.running: CommitStatus.running,
37+
BaseCommitStatus.failure: CommitStatus.failure,
38+
BaseCommitStatus.neutral: CommitStatus.error,
39+
BaseCommitStatus.success: CommitStatus.success,
40+
BaseCommitStatus.error: CommitStatus.error,
41+
}
42+
43+
MAP_TO_CHECK_RUN: Dict[
44+
BaseCommitStatus, Union[GithubCheckRunResult, GithubCheckRunStatus]
45+
] = {
46+
BaseCommitStatus.pending: GithubCheckRunStatus.queued,
47+
BaseCommitStatus.running: GithubCheckRunStatus.in_progress,
48+
BaseCommitStatus.failure: GithubCheckRunResult.failure,
49+
BaseCommitStatus.neutral: GithubCheckRunResult.neutral,
50+
BaseCommitStatus.success: GithubCheckRunResult.success,
51+
BaseCommitStatus.error: GithubCheckRunResult.failure,
52+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
from random import choice
5+
6+
7+
class News:
8+
__FOOTERS = [
9+
"Do you maintain a Fedora package and don't have access to the upstream repository? "
10+
"Packit can help. "
11+
"Take a look [here](https://packit.dev/posts/pull-from-upstream/) to know more.",
12+
"Do you maintain a Fedora package and you think it's boring? Packit can help. "
13+
"Take a look [here](https://packit.dev/posts/downstream-automation/) to know more.",
14+
"Want to use a build from a different project when testing? "
15+
"Take a look [here](https://packit.dev/posts/testing-farm-triggering/) to know more.",
16+
"Curious how Packit handles the Release field during propose-downstream? "
17+
"Take a look [here](https://packit.dev/posts/release-field-handling/) to know more.",
18+
"Did you know Packit is on Mastodon? Or, more specifically, on Fosstodon? "
19+
"Follow [@[email protected]](https://fosstodon.org/@packit) "
20+
"and be one of the first to know about all the news!",
21+
"Interested in the Packit team plans and priorities? "
22+
"Check [our epic board](https://github.com/orgs/packit/projects/7/views/29).",
23+
"Do you use `propose_downstream`? We would be happy if you could help"
24+
"us with verifying it in staging. "
25+
"See [the details](https://packit.dev/posts/verify-sync-release-volunteers)",
26+
]
27+
28+
@classmethod
29+
def get_sentence(cls) -> str:
30+
"""
31+
A random sentence to show our users as a footer when adding a status.
32+
(Will be visible at the very bottom of the markdown field.
33+
"""
34+
return choice(cls.__FOOTERS)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
import logging
5+
from datetime import datetime, timezone
6+
from typing import Optional, Union, Dict, Callable
7+
8+
from packit_service.worker.reporting.enums import (
9+
BaseCommitStatus,
10+
MAP_TO_COMMIT_STATUS,
11+
MAP_TO_CHECK_RUN,
12+
DuplicateCheckMode,
13+
)
14+
15+
from ogr.abstract import GitProject
16+
from ogr.services.github import GithubProject
17+
from ogr.services.gitlab import GitlabProject
18+
from ogr.services.pagure import PagureProject
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class StatusReporter:
24+
def __init__(
25+
self,
26+
project: GitProject,
27+
commit_sha: str,
28+
packit_user: str,
29+
project_event_id: Optional[int] = None,
30+
pr_id: Optional[int] = None,
31+
):
32+
logger.debug(
33+
f"Status reporter will report for {project}, commit={commit_sha}, pr={pr_id}"
34+
)
35+
self.project: GitProject = project
36+
self._project_with_commit: Optional[GitProject] = None
37+
self._packit_user = packit_user
38+
39+
self.commit_sha: str = commit_sha
40+
self.project_event_id: int = project_event_id
41+
self.pr_id: Optional[int] = pr_id
42+
43+
@classmethod
44+
def get_instance(
45+
cls,
46+
project: GitProject,
47+
commit_sha: str,
48+
packit_user: str,
49+
project_event_id: Optional[int] = None,
50+
pr_id: Optional[int] = None,
51+
) -> "StatusReporter":
52+
"""
53+
Get the StatusReporter instance.
54+
"""
55+
from .github import StatusReporterGithubChecks
56+
from .gitlab import StatusReporterGitlab
57+
from .pagure import StatusReporterPagure
58+
59+
reporter = StatusReporter
60+
if isinstance(project, GithubProject):
61+
reporter = StatusReporterGithubChecks
62+
elif isinstance(project, GitlabProject):
63+
reporter = StatusReporterGitlab
64+
elif isinstance(project, PagureProject):
65+
reporter = StatusReporterPagure
66+
return reporter(project, commit_sha, packit_user, project_event_id, pr_id)
67+
68+
@property
69+
def project_with_commit(self) -> GitProject:
70+
"""
71+
Returns GitProject from which we can set commit status.
72+
"""
73+
if self._project_with_commit is None:
74+
self._project_with_commit = (
75+
self.project.get_pr(self.pr_id).source_project
76+
if isinstance(self.project, GitlabProject) and self.pr_id is not None
77+
else self.project
78+
)
79+
80+
return self._project_with_commit
81+
82+
@staticmethod
83+
def get_commit_status(state: BaseCommitStatus):
84+
return MAP_TO_COMMIT_STATUS[state]
85+
86+
@staticmethod
87+
def get_check_run(state: BaseCommitStatus):
88+
return MAP_TO_CHECK_RUN[state]
89+
90+
def set_status(
91+
self,
92+
state: BaseCommitStatus,
93+
description: str,
94+
check_name: str,
95+
url: str = "",
96+
links_to_external_services: Optional[Dict[str, str]] = None,
97+
markdown_content: str = None,
98+
):
99+
raise NotImplementedError()
100+
101+
def report(
102+
self,
103+
state: BaseCommitStatus,
104+
description: str,
105+
url: str = "",
106+
links_to_external_services: Optional[Dict[str, str]] = None,
107+
check_names: Union[str, list, None] = None,
108+
markdown_content: str = None,
109+
update_feedback_time: Callable = None,
110+
) -> None:
111+
"""
112+
Set commit check status.
113+
114+
Args:
115+
state: State accepted by github.
116+
description: The long text.
117+
url: Url to point to (logs usually).
118+
119+
Defaults to empty string
120+
links_to_external_services: Direct links to external services.
121+
e.g. `{"Testing Farm": "url-to-testing-farm"}`
122+
123+
Defaults to None
124+
check_names: Those in bold.
125+
126+
Defaults to None
127+
markdown_content: In GitHub checks, we can provide a markdown content.
128+
129+
Defaults to None
130+
131+
update_feedback_time: a callable which tells the caller when a check
132+
status has been updated.
133+
134+
Returns:
135+
None
136+
"""
137+
if not check_names:
138+
logger.warning("No checks to set status for.")
139+
return
140+
141+
elif isinstance(check_names, str):
142+
check_names = [check_names]
143+
144+
for check in check_names:
145+
self.set_status(
146+
state=state,
147+
description=description,
148+
check_name=check,
149+
url=url,
150+
links_to_external_services=links_to_external_services,
151+
markdown_content=markdown_content,
152+
)
153+
154+
if update_feedback_time:
155+
update_feedback_time(datetime.now(timezone.utc))
156+
157+
@staticmethod
158+
def is_final_state(state: BaseCommitStatus) -> bool:
159+
return state in {
160+
BaseCommitStatus.success,
161+
BaseCommitStatus.error,
162+
BaseCommitStatus.failure,
163+
}
164+
165+
def _add_commit_comment_with_status(
166+
self, state: BaseCommitStatus, description: str, check_name: str, url: str = ""
167+
):
168+
"""Add a comment with status to the commit.
169+
170+
A fallback solution when setting commit status fails.
171+
"""
172+
body = (
173+
"\n".join(
174+
[
175+
f"- name: {check_name}",
176+
f"- state: {state.name}",
177+
f"- url: {url or 'not provided'}",
178+
]
179+
)
180+
+ f"\n\n{description}"
181+
)
182+
183+
if self.is_final_state(state):
184+
self.comment(body, DuplicateCheckMode.check_all_comments, to_commit=True)
185+
else:
186+
logger.debug(f"Ain't comment as {state!r} is not a final state")
187+
188+
def report_status_by_comment(
189+
self,
190+
state: BaseCommitStatus,
191+
url: str,
192+
check_names: Union[str, list, None],
193+
description: str,
194+
):
195+
"""
196+
Reporting build status with MR comment if no permission to the fork project
197+
"""
198+
199+
if isinstance(check_names, str):
200+
check_names = [check_names]
201+
202+
comment_table_rows = [
203+
"| Job | Result |",
204+
"| ------------- | ------------ |",
205+
] + [f"| [{check}]({url}) | {state.name.upper()} |" for check in check_names]
206+
207+
table = "\n".join(comment_table_rows)
208+
self.comment(table + f"\n### Description\n\n{description}")
209+
210+
def get_statuses(self):
211+
self.project_with_commit.get_commit_statuses(commit=self.commit_sha)
212+
213+
def _has_identical_comment(
214+
self, body: str, mode: DuplicateCheckMode, check_commit: bool = False
215+
) -> bool:
216+
"""Checks if the body is the same as the last or any (based on mode) comment.
217+
218+
Check either commit comments or PR comments (if specified).
219+
"""
220+
if mode == DuplicateCheckMode.do_not_check:
221+
return False
222+
223+
comments = (
224+
reversed(self.project.get_commit_comments(self.commit_sha))
225+
if check_commit or not self.pr_id
226+
else self.project.get_pr(pr_id=self.pr_id).get_comments(reverse=True)
227+
)
228+
for comment in comments:
229+
if comment.author.startswith(self._packit_user):
230+
if mode == DuplicateCheckMode.check_last_comment:
231+
return body == comment.body
232+
elif (
233+
mode == DuplicateCheckMode.check_all_comments
234+
and body == comment.body
235+
):
236+
return True
237+
return False
238+
239+
def comment(
240+
self,
241+
body: str,
242+
duplicate_check: DuplicateCheckMode = DuplicateCheckMode.do_not_check,
243+
to_commit: bool = False,
244+
):
245+
"""Add a comment.
246+
247+
It's added either to a commit or to a PR (if specified).
248+
249+
Args:
250+
body: The comment text.
251+
duplicate_check: Determines if the comment will be added if
252+
the same comment is already present in the PR
253+
(if the instance is tied to a PR) or in a commit.
254+
to_commit: Add the comment to the commit even if PR is specified.
255+
"""
256+
if self._has_identical_comment(body, duplicate_check, to_commit):
257+
logger.debug("Identical comment already exists")
258+
return
259+
260+
if to_commit or not self.pr_id:
261+
self.project.commit_comment(commit=self.commit_sha, body=body)
262+
else:
263+
self.project.get_pr(pr_id=self.pr_id).comment(body=body)

0 commit comments

Comments
 (0)