Skip to content

Commit

Permalink
Factor out reporting (#2332)
Browse files Browse the repository at this point in the history
Factor out reporting

Reviewed-by: Laura Barcziová
  • Loading branch information
2 parents 20d49d3 + a1e347a commit 3e6737a
Show file tree
Hide file tree
Showing 15 changed files with 690 additions and 601 deletions.
587 changes: 0 additions & 587 deletions packit_service/worker/reporting.py

This file was deleted.

25 changes: 25 additions & 0 deletions packit_service/worker/reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

from packit_service.worker.reporting.enums import BaseCommitStatus, DuplicateCheckMode
from packit_service.worker.reporting.reporters.base import StatusReporter
from packit_service.worker.reporting.reporters.github import (
StatusReporterGithubChecks,
StatusReporterGithubStatuses,
)
from packit_service.worker.reporting.reporters.gitlab import StatusReporterGitlab
from packit_service.worker.reporting.utils import (
report_in_issue_repository,
update_message_with_configured_failure_comment_message,
)

__all__ = [
BaseCommitStatus.__name__,
StatusReporter.__name__,
DuplicateCheckMode.__name__,
report_in_issue_repository.__name__,
update_message_with_configured_failure_comment_message.__name__,
StatusReporterGithubChecks.__name__,
StatusReporterGithubStatuses.__name__,
StatusReporterGitlab.__name__,
]
52 changes: 52 additions & 0 deletions packit_service/worker/reporting/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

from enum import Enum, auto
from typing import Dict, Union

from ogr.abstract import CommitStatus
from ogr.services.github.check_run import (
GithubCheckRunResult,
GithubCheckRunStatus,
)


class DuplicateCheckMode(Enum):
"""Enum of possible behaviour for handling duplicates when commenting."""

# Do not check for duplicates
do_not_check = auto()
# Check only last comment from us for duplicate
check_last_comment = auto()
# Check the whole comment list for duplicate
check_all_comments = auto()


class BaseCommitStatus(Enum):
failure = "failure"
neutral = "neutral"
success = "success"
pending = "pending"
running = "running"
error = "error"


MAP_TO_COMMIT_STATUS: Dict[BaseCommitStatus, CommitStatus] = {
BaseCommitStatus.pending: CommitStatus.pending,
BaseCommitStatus.running: CommitStatus.running,
BaseCommitStatus.failure: CommitStatus.failure,
BaseCommitStatus.neutral: CommitStatus.error,
BaseCommitStatus.success: CommitStatus.success,
BaseCommitStatus.error: CommitStatus.error,
}

MAP_TO_CHECK_RUN: Dict[
BaseCommitStatus, Union[GithubCheckRunResult, GithubCheckRunStatus]
] = {
BaseCommitStatus.pending: GithubCheckRunStatus.queued,
BaseCommitStatus.running: GithubCheckRunStatus.in_progress,
BaseCommitStatus.failure: GithubCheckRunResult.failure,
BaseCommitStatus.neutral: GithubCheckRunResult.neutral,
BaseCommitStatus.success: GithubCheckRunResult.success,
BaseCommitStatus.error: GithubCheckRunResult.failure,
}
34 changes: 34 additions & 0 deletions packit_service/worker/reporting/news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

from random import choice


class News:
__FOOTERS = [
"Do you maintain a Fedora package and don't have access to the upstream repository? "
"Packit can help. "
"Take a look [here](https://packit.dev/posts/pull-from-upstream/) to know more.",
"Do you maintain a Fedora package and you think it's boring? Packit can help. "
"Take a look [here](https://packit.dev/posts/downstream-automation/) to know more.",
"Want to use a build from a different project when testing? "
"Take a look [here](https://packit.dev/posts/testing-farm-triggering/) to know more.",
"Curious how Packit handles the Release field during propose-downstream? "
"Take a look [here](https://packit.dev/posts/release-field-handling/) to know more.",
"Did you know Packit is on Mastodon? Or, more specifically, on Fosstodon? "
"Follow [@packit@fosstodon.org](https://fosstodon.org/@packit) "
"and be one of the first to know about all the news!",
"Interested in the Packit team plans and priorities? "
"Check [our epic board](https://github.com/orgs/packit/projects/7/views/29).",
"Do you use `propose_downstream`? We would be happy if you could help"
"us with verifying it in staging. "
"See [the details](https://packit.dev/posts/verify-sync-release-volunteers)",
]

@classmethod
def get_sentence(cls) -> str:
"""
A random sentence to show our users as a footer when adding a status.
(Will be visible at the very bottom of the markdown field.
"""
return choice(cls.__FOOTERS)
263 changes: 263 additions & 0 deletions packit_service/worker/reporting/reporters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

import logging
from datetime import datetime, timezone
from typing import Optional, Union, Dict, Callable

from packit_service.worker.reporting.enums import (
BaseCommitStatus,
MAP_TO_COMMIT_STATUS,
MAP_TO_CHECK_RUN,
DuplicateCheckMode,
)

from ogr.abstract import GitProject
from ogr.services.github import GithubProject
from ogr.services.gitlab import GitlabProject
from ogr.services.pagure import PagureProject

logger = logging.getLogger(__name__)


class StatusReporter:
def __init__(
self,
project: GitProject,
commit_sha: str,
packit_user: str,
project_event_id: Optional[int] = None,
pr_id: Optional[int] = None,
):
logger.debug(
f"Status reporter will report for {project}, commit={commit_sha}, pr={pr_id}"
)
self.project: GitProject = project
self._project_with_commit: Optional[GitProject] = None
self._packit_user = packit_user

self.commit_sha: str = commit_sha
self.project_event_id: int = project_event_id
self.pr_id: Optional[int] = pr_id

@classmethod
def get_instance(
cls,
project: GitProject,
commit_sha: str,
packit_user: str,
project_event_id: Optional[int] = None,
pr_id: Optional[int] = None,
) -> "StatusReporter":
"""
Get the StatusReporter instance.
"""
from .github import StatusReporterGithubChecks
from .gitlab import StatusReporterGitlab
from .pagure import StatusReporterPagure

reporter = StatusReporter
if isinstance(project, GithubProject):
reporter = StatusReporterGithubChecks
elif isinstance(project, GitlabProject):
reporter = StatusReporterGitlab
elif isinstance(project, PagureProject):
reporter = StatusReporterPagure
return reporter(project, commit_sha, packit_user, project_event_id, pr_id)

@property
def project_with_commit(self) -> GitProject:
"""
Returns GitProject from which we can set commit status.
"""
if self._project_with_commit is None:
self._project_with_commit = (
self.project.get_pr(self.pr_id).source_project
if isinstance(self.project, GitlabProject) and self.pr_id is not None
else self.project
)

return self._project_with_commit

@staticmethod
def get_commit_status(state: BaseCommitStatus):
return MAP_TO_COMMIT_STATUS[state]

@staticmethod
def get_check_run(state: BaseCommitStatus):
return MAP_TO_CHECK_RUN[state]

def set_status(
self,
state: BaseCommitStatus,
description: str,
check_name: str,
url: str = "",
links_to_external_services: Optional[Dict[str, str]] = None,
markdown_content: str = None,
):
raise NotImplementedError()

def report(
self,
state: BaseCommitStatus,
description: str,
url: str = "",
links_to_external_services: Optional[Dict[str, str]] = None,
check_names: Union[str, list, None] = None,
markdown_content: str = None,
update_feedback_time: Callable = None,
) -> None:
"""
Set commit check status.
Args:
state: State accepted by github.
description: The long text.
url: Url to point to (logs usually).
Defaults to empty string
links_to_external_services: Direct links to external services.
e.g. `{"Testing Farm": "url-to-testing-farm"}`
Defaults to None
check_names: Those in bold.
Defaults to None
markdown_content: In GitHub checks, we can provide a markdown content.
Defaults to None
update_feedback_time: a callable which tells the caller when a check
status has been updated.
Returns:
None
"""
if not check_names:
logger.warning("No checks to set status for.")
return

elif isinstance(check_names, str):
check_names = [check_names]

for check in check_names:
self.set_status(
state=state,
description=description,
check_name=check,
url=url,
links_to_external_services=links_to_external_services,
markdown_content=markdown_content,
)

if update_feedback_time:
update_feedback_time(datetime.now(timezone.utc))

@staticmethod
def is_final_state(state: BaseCommitStatus) -> bool:
return state in {
BaseCommitStatus.success,
BaseCommitStatus.error,
BaseCommitStatus.failure,
}

def _add_commit_comment_with_status(
self, state: BaseCommitStatus, description: str, check_name: str, url: str = ""
):
"""Add a comment with status to the commit.
A fallback solution when setting commit status fails.
"""
body = (
"\n".join(
[
f"- name: {check_name}",
f"- state: {state.name}",
f"- url: {url or 'not provided'}",
]
)
+ f"\n\n{description}"
)

if self.is_final_state(state):
self.comment(body, DuplicateCheckMode.check_all_comments, to_commit=True)
else:
logger.debug(f"Ain't comment as {state!r} is not a final state")

def report_status_by_comment(
self,
state: BaseCommitStatus,
url: str,
check_names: Union[str, list, None],
description: str,
):
"""
Reporting build status with MR comment if no permission to the fork project
"""

if isinstance(check_names, str):
check_names = [check_names]

comment_table_rows = [
"| Job | Result |",
"| ------------- | ------------ |",
] + [f"| [{check}]({url}) | {state.name.upper()} |" for check in check_names]

table = "\n".join(comment_table_rows)
self.comment(table + f"\n### Description\n\n{description}")

def get_statuses(self):
self.project_with_commit.get_commit_statuses(commit=self.commit_sha)

def _has_identical_comment(
self, body: str, mode: DuplicateCheckMode, check_commit: bool = False
) -> bool:
"""Checks if the body is the same as the last or any (based on mode) comment.
Check either commit comments or PR comments (if specified).
"""
if mode == DuplicateCheckMode.do_not_check:
return False

comments = (
reversed(self.project.get_commit_comments(self.commit_sha))
if check_commit or not self.pr_id
else self.project.get_pr(pr_id=self.pr_id).get_comments(reverse=True)
)
for comment in comments:
if comment.author.startswith(self._packit_user):
if mode == DuplicateCheckMode.check_last_comment:
return body == comment.body
elif (
mode == DuplicateCheckMode.check_all_comments
and body == comment.body
):
return True
return False

def comment(
self,
body: str,
duplicate_check: DuplicateCheckMode = DuplicateCheckMode.do_not_check,
to_commit: bool = False,
):
"""Add a comment.
It's added either to a commit or to a PR (if specified).
Args:
body: The comment text.
duplicate_check: Determines if the comment will be added if
the same comment is already present in the PR
(if the instance is tied to a PR) or in a commit.
to_commit: Add the comment to the commit even if PR is specified.
"""
if self._has_identical_comment(body, duplicate_check, to_commit):
logger.debug("Identical comment already exists")
return

if to_commit or not self.pr_id:
self.project.commit_comment(commit=self.commit_sha, body=body)
else:
self.project.get_pr(pr_id=self.pr_id).comment(body=body)
Loading

0 comments on commit 3e6737a

Please sign in to comment.