Skip to content
This repository has been archived by the owner on Oct 13, 2023. It is now read-only.

Commit

Permalink
comment on pr
Browse files Browse the repository at this point in the history
Comment on a PR after build is completed and if its not a `dry_run`. This feature is disabled by default and can be enabled by using the `--comment-on-pr` flag.

Workflow:
- After a build is completed, we get the PR from the merge commit and check if the same comment has already been posted.
- If it hasn't it will post the comment.
- If the code fails, logger will post an error, but will not hinder other tasks, since enclosed in try-catch block

Unit tests added.
  • Loading branch information
ashwindasr committed Apr 25, 2023
1 parent f4e5a90 commit 9cbf532
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 3 deletions.
6 changes: 4 additions & 2 deletions doozerlib/cli/__main__.py
Expand Up @@ -786,8 +786,9 @@ def print_build_metrics(runtime):
help="Specify an exact arch to push (golang name e.g. 'amd64').")
@click.option('--dry-run', default=False, is_flag=True, help='Do not build anything, but only print build operations.')
@click.option('--build-retries', type=int, default=1, help='Number of build attempts for an osbs build')
@click.option('--comment-on-pr', default=False, is_flag=True, help='Comment on PR after a build, if flag is enabled')
@pass_runtime
def images_build_image(runtime, repo_type, repo, push_to_defaults, push_to, scratch, threads, filter_by_os, dry_run, build_retries):
def images_build_image(runtime, repo_type, repo, push_to_defaults, push_to, scratch, threads, filter_by_os, dry_run, build_retries, comment_on_pr):
"""
Attempts to build container images for all of the distgit repositories
in a group. If an image has already been built, it will be treated as
Expand Down Expand Up @@ -882,7 +883,8 @@ def images_build_image(runtime, repo_type, repo, push_to_defaults, push_to, scra
lambda dgr, terminate_event: dgr.build_container(
active_profile, push_to_defaults, additional_registries=push_to, retries=build_retries,
terminate_event=terminate_event, scratch=scratch, realtime=(threads == 1),
dry_run=dry_run, registry_config_dir=runtime.registry_config_dir, filter_by_os=filter_by_os),
dry_run=dry_run, registry_config_dir=runtime.registry_config_dir,
filter_by_os=filter_by_os, comment_on_pr=comment_on_pr),
items, n_threads=threads)
results = results.get()

Expand Down
79 changes: 79 additions & 0 deletions doozerlib/comment_on_pr.py
@@ -0,0 +1,79 @@
from ghapi.all import GhApi
from doozerlib.pushd import Dir
from dockerfile_parse import DockerfileParser


class CommentOnPr:
def __init__(self, distgit_dir: str, token: str):
self.distgit_dir = distgit_dir
self.token = token
self.owner = None
self.repo = None
self.commit = None
self.gh_client = None # GhApi client
self.pr_url = None
self.pr_no = None

def list_comments(self):
"""
List the comments in a PR
"""
# https://docs.github.com/rest/reference/issues#list-issue-comments
return self.gh_client.issues.list_comments(issue_number=self.pr_no, per_page=100)

def check_if_comment_exist(self, comment):
"""
Check if the same comment already exists in the PR
"""
issue_comments = self.list_comments()
for issue_comment in issue_comments:
if issue_comment["body"] == comment:
return True
return False

def post_comment(self, comment):
"""
Post the comment in the PR if the comment doesn't exist already
"""
# https://docs.github.com/rest/reference/issues#create-an-issue-comment
if not self.check_if_comment_exist(comment):
self.gh_client.issues.create_comment(issue_number=self.pr_no, body=comment)
return True
return False

def set_pr_from_commit(self):
"""
Get the PR from the merge commit
"""
# https://docs.github.com/rest/commits/commits#list-pull-requests-associated-with-a-commit
prs = self.gh_client.repos.list_pull_requests_associated_with_commit(self.commit)
if len(prs) == 1:
# self._logger.info(f"PR from merge commit {sha}: {pull_url}")
self.pr_url = prs[0]["html_url"]
self.pr_no = prs[0]["number"]
return
raise Exception(f"Multiple PRs found for merge commit {self.commit}")

def set_repo_details(self):
"""
Get the owner, commit and repo from the dfp label
"""
with Dir(self.distgit_dir):
dfp = DockerfileParser(str(Dir.getpath().joinpath('Dockerfile')))

# eg: "https://github.com/openshift/origin/commit/660e0c785a2c9b1fd5fad33cbcffd77a6d84ccb5"
source_commit_url = dfp.labels["io.openshift.build.commit.url"]
url_split = source_commit_url.split("/")
commit = url_split[-1] # eg: 660e0c785a2c9b1fd5fad33cbcffd77a6d84ccb5
repo = url_split[-3] # eg: origin
owner = url_split[-4] # eg: openshift

self.owner = owner
self.commit = commit
self.repo = repo

def set_github_client(self):
"""
Set the gh client after the get_source_details function is run
"""
self.gh_client = GhApi(owner=self.owner, repo=self.repo, token=self.token)
24 changes: 23 additions & 1 deletion doozerlib/distgit.py
Expand Up @@ -38,6 +38,8 @@
from doozerlib.rpm_utils import parse_nvr
from doozerlib.source_modifications import SourceModifierFactory
from doozerlib.util import convert_remote_git_to_https, yellow_print
from doozerlib.comment_on_pr import CommentOnPr
from string import Template

# doozer used to be part of OIT
OIT_COMMENT_PREFIX = '#oit##'
Expand Down Expand Up @@ -937,7 +939,7 @@ def wait_for_rebase(self, image_name, terminate_event):

def build_container(
self, profile, push_to_defaults, additional_registries, terminate_event,
scratch=False, retries=3, realtime=False, dry_run=False, registry_config_dir=None, filter_by_os=None):
scratch=False, retries=3, realtime=False, dry_run=False, registry_config_dir=None, filter_by_os=None, comment_on_pr=False):
"""
This method is designed to be thread-safe. Multiple builds should take place in brew
at the same time. After a build, images are pushed serially to all mirrors.
Expand Down Expand Up @@ -1046,6 +1048,26 @@ def wait(n):
record["nvrs"] = build_info["nvr"]
if not dry_run:
self.update_build_db(True, task_id=task_id, scratch=scratch)
if comment_on_pr:
try:
comment_on_pr_obj = CommentOnPr(distgit_dir=self.distgit_dir,
token=os.getenv(constants.GITHUB_TOKEN))
comment_on_pr_obj.set_repo_details()
comment_on_pr_obj.set_github_client()
comment_on_pr_obj.set_pr_from_commit()
# Message to be posted to the comment
message = Template("**[ART PR BUILD NOTIFIER]** _(beta)_\n\n"
"This PR has been included in build "
"[$nvr](https://brewweb.engineering.redhat.com/brew/buildinfo"
"?buildID=$build_id)"
"for distgit *$distgit_name*. \n All builds following this will "
"include this PR.")
comment_on_pr_obj.post_comment(message.substitute(nvr=build_info["nvr"],
build_id=build_info["id"],
distgit_name=self.metadata.name))
except Exception as e:
self.logger.error(f"Error commenting on PR for build task id {task_id} for distgit"
f"{self.metadata.name}: {e}")
if not scratch:
push_version = build_info["version"]
push_release = build_info["release"]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -19,3 +19,4 @@ setuptools-scm
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
aiohttp
jira==3.4.1
ghapi
107 changes: 107 additions & 0 deletions tests/test_comment_on_pr.py
@@ -0,0 +1,107 @@
import unittest
from unittest.mock import MagicMock, patch
from doozerlib.comment_on_pr import CommentOnPr


class TestCommentOnPr(unittest.TestCase):
def setUp(self):
self.distgit_dir = "distgit_dir"
self.token = "token"
self.commit = "commit_sha"
self.comment = "comment"

def test_list_comments(self):
pr_no = 1
api_mock = MagicMock()
api_mock.issues.list_comments.return_value = [{"body": "test comment"}]
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.pr_no = pr_no
comment_on_pr.gh_client = api_mock
result = comment_on_pr.list_comments()
api_mock.issues.list_comments.assert_called_once_with(issue_number=pr_no, per_page=100)
self.assertEqual(result, [{"body": "test comment"}])

@patch.object(CommentOnPr, "list_comments")
def test_check_if_comment_exist(self, mock_list_comments):
api_mock = MagicMock()
api_mock.issues.list_comments.return_value = [{"body": self.comment}]
mock_list_comments.return_value = api_mock.issues.list_comments()
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
result = comment_on_pr.check_if_comment_exist(self.comment)
self.assertTrue(result)

@patch.object(CommentOnPr, "list_comments")
def test_check_if_comment_exist_when_comment_does_not_exist(self, mock_list_comments):
api_mock = MagicMock()
api_mock.issues.list_comments.return_value = [{"body": "test comment"}]
mock_list_comments.return_value = api_mock.issues.list_comments()
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
result = comment_on_pr.check_if_comment_exist(self.comment)
self.assertFalse(result)

@patch.object(CommentOnPr, "check_if_comment_exist")
def test_post_comment(self, mock_check_if_comment_exist):
pr_no = 1
api_mock = MagicMock()
mock_check_if_comment_exist.return_value = False
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.pr_no = pr_no
comment_on_pr.gh_client = api_mock
result = comment_on_pr.post_comment(self.comment)
api_mock.issues.create_comment.assert_called_once_with(issue_number=pr_no, body=self.comment)
self.assertTrue(result)

@patch.object(CommentOnPr, "check_if_comment_exist")
def test_post_comment_when_comment_already_exists(self, mock_check_if_comment_exist):
pr_no = 1
api_mock = MagicMock()
mock_check_if_comment_exist.return_value = True
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.pr_no = pr_no
comment_on_pr.gh_client = api_mock
result = comment_on_pr.post_comment(self.comment)
api_mock.issues.create_comment.assert_not_called()
self.assertFalse(result)

def test_get_pr_from_commit(self):
api_mock = MagicMock()
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.gh_client = api_mock
api_mock.repos.list_pull_requests_associated_with_commit.return_value = [{"html_url": "test_url", "number": 1}]
comment_on_pr.set_pr_from_commit()
self.assertEqual(comment_on_pr.pr_url, 'test_url')
self.assertEqual(comment_on_pr.pr_no, 1)

def test_multiple_prs_for_merge_commit(self):
api_mock = MagicMock()
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.gh_client = api_mock
api_mock.repos.list_pull_requests_associated_with_commit.return_value = [{"html_url": "test_url", "number": 1},
{"html_url": "test_url_2",
"number": 2}]
with self.assertRaises(Exception):
comment_on_pr.set_pr_from_commit()

def test_set_github_client(self):
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
comment_on_pr.owner = "owner"
comment_on_pr.repo = "repo"
comment_on_pr.token = "token"
comment_on_pr.set_github_client()
self.assertIsNotNone(comment_on_pr.gh_client)

@patch('doozerlib.comment_on_pr.DockerfileParser')
def test_get_source_details(self, mock_parser):
comment_on_pr = CommentOnPr(self.distgit_dir, self.token)
# Mocking the labels dictionary of the DockerfileParser object
mock_parser.return_value.labels = {
"io.openshift.build.commit.url": "https://github.com/openshift/origin/commit/660e0c785a2c9b1fd5fad33cbcffd77a6d84ccb5"
}

# Calling the get_source_details method
comment_on_pr.set_repo_details()

# Asserting that the owner, commit, and repo attributes are set correctly
self.assertEqual(comment_on_pr.owner, 'openshift')
self.assertEqual(comment_on_pr.commit, '660e0c785a2c9b1fd5fad33cbcffd77a6d84ccb5')
self.assertEqual(comment_on_pr.repo, 'origin')

0 comments on commit 9cbf532

Please sign in to comment.