[scm_request_processor] Make bot more interactive

This commits adds separate processing for comments in
scm_request_processor and adds following commands the bot will react on.

help - prints this help message
retry - retries the failed request processing
valid - can be used by authorized user to mark the request as valid
This commit is contained in:
Michal Konecny 2025-04-02 16:07:50 +02:00
parent 8900a0aff9
commit f8a8c2b68a
4 changed files with 465 additions and 398 deletions

View file

@ -10,6 +10,16 @@ The SCM Request processor accepts few topics emitted by pagure. These topics all
* org.fedoraproject.*.pagure.issue.edit - Issue on pagure is edited * org.fedoraproject.*.pagure.issue.edit - Issue on pagure is edited
* org.fedoraporject.*.pagure.issue.comment.added - New comment is added to issue * org.fedoraporject.*.pagure.issue.comment.added - New comment is added to issue
## Supported commands
The SCM Request processor now supports few commands that could be invoked by sending comment
addressed to bot user (releng-bot) in issue opened on https://pagure.io/releng/fedora-scm-requests/
Supported commands:
* help - prints help for commands
* retry - retries the failed request
* valid - validate the request as authorized user
## Flowchart diagram ## Flowchart diagram
[Flowchart diagram](./scm_request_processor.png) illustrates the code flow of the SCM Request Processor, what is being validated and what reaction is done if the validation fails. [Flowchart diagram](./scm_request_processor.png) illustrates the code flow of the SCM Request Processor, what is being validated and what reaction is done if the validation fails.

View file

@ -122,11 +122,11 @@ class TestProcess:
msg = IssueCommentAddedV1(body=body) msg = IssueCommentAddedV1(body=body)
with patch( with patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_ticket" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_comment"
) as mock_process_ticket: ) as mock_process_comment:
toddler.process(config, msg) toddler.process(config, msg)
mock_process_ticket.assert_not_called() mock_process_comment.assert_not_called()
assert caplog.records[ assert caplog.records[
-1 -1
@ -231,6 +231,39 @@ class TestProcess:
mock_fasjson.assert_called_with(config) mock_fasjson.assert_called_with(config)
mock_bugzilla.assert_called_with(config) mock_bugzilla.assert_called_with(config)
@patch("toddlers.utils.pagure.set_pagure")
@patch("toddlers.utils.fedora_account.set_fasjson")
@patch("toddlers.utils.bugzilla_system.set_bz")
def test_process_comment(self, mock_bugzilla, mock_fasjson, mock_pagure, toddler):
"""
Assert that toddler will handle comments correctly.
"""
pagure_user = "pagure_user"
config = {
"pagure_user": pagure_user,
"branch_slas": {},
"monitoring_choices": [],
"pagure_namespace_to_component": {},
"pagure_namespace_to_product": {},
"temp_dir": "",
"dist_git_url": "https://src.fedoraproject.org",
"dist_git_token": "Private API Key",
}
issue = {"status": "Open", "user": {"name": "zlopez"}}
body = {
"project": {"fullname": scm_request_processor.PROJECT_NAMESPACE},
"issue": {"status": "Open", "user": {"name": "zlopez"}},
"agent": "zlopez",
}
msg = IssueCommentAddedV1(body=body)
with patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_comment"
) as mock_process_comment:
toddler.process(config, msg)
mock_process_comment.assert_called_with(issue)
class TestProcessTicket: class TestProcessTicket:
""" """
@ -416,7 +449,7 @@ class TestProcessTicket:
self.toddler.branch_slas = {"branch": "SLA"} self.toddler.branch_slas = {"branch": "SLA"}
with patch( with patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.create_new_repo" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_new_repo"
) as mock_new_repo: ) as mock_new_repo:
self.toddler.process_ticket(issue) self.toddler.process_ticket(issue)
@ -563,9 +596,194 @@ class TestVerifySLAs:
self.toddler.verify_slas("", sla) self.toddler.verify_slas("", sla)
class TestCreateNewRepo: class TestProcessComment:
""" """
Test class for `toddlers.plugins.scm_request_processor.SCMRequestProcessor.create_new_repo` Test class for `toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_comment`.
"""
def setup_method(self):
"""
Initialize toddler.
"""
self.toddler = scm_request_processor.SCMRequestProcessor()
self.toddler.pagure_io = Mock()
self.toddler.pagure_user = "pagure_user"
def test_process_comment_notification(self, caplog):
"""
Assert that notification will be ignored.
"""
issue = {
"comments": [{"notification": True}],
"full_url": "https://blacklibrary.wh40k",
}
caplog.set_level(logging.INFO)
self.toddler.process_comment(issue)
assert caplog.records[-1].message == "Comment is notification. Ignoring."
def test_process_comment_missing_bot_mention(self, caplog):
"""
Assert that comment will be ignored if it doesn't mention bot.
"""
issue = {
"comments": [{"notification": False, "comment": ""}],
"full_url": "https://blacklibrary.wh40k",
}
caplog.set_level(logging.INFO)
self.toddler.process_comment(issue)
assert caplog.records[
-1
].message == "Comment is not for '{}'. Ignoring.".format(
self.toddler.pagure_user
)
def test_process_comment_help_command(self):
"""
Assert that help command will be processed correctly.
"""
issue = {
"id": 100,
"comments": [
{
"notification": False,
"comment": "@{0} help".format(self.toddler.pagure_user),
}
],
"full_url": "https://blacklibrary.wh40k",
}
self.toddler.process_comment(issue)
self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
issue["id"],
namespace=scm_request_processor.PROJECT_NAMESPACE,
comment=(
"{0} recognizes following commands on {1}.\n"
"help - prints this help message\n"
"retry - retries the failed request processing\n"
"valid - can be used by authorized user to mark the request as valid\n"
).format(self.toddler.pagure_user, scm_request_processor.PROJECT_NAMESPACE),
)
def test_process_comment_retry_command(self):
"""
Assert that retry command will be processed correctly.
"""
issue = {
"id": 100,
"comments": [
{
"notification": False,
"comment": "@{0} retry".format(self.toddler.pagure_user),
}
],
"full_url": "https://blacklibrary.wh40k",
}
with patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_ticket"
) as mock_process_ticket:
self.toddler.process_comment(issue)
mock_process_ticket.assert_called_with(issue)
def test_process_comment_valid_command(self):
"""
Assert that valid command from authorized user
will resume repo creation.
"""
user = "zlopez"
issue = {
"id": 100,
"comments": [
{
"user": {"name": user},
"notification": False,
"comment": "@{0} valid".format(self.toddler.pagure_user),
}
],
"full_url": "https://blacklibrary.wh40k",
"content": '{"action": "new_repo"}',
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []}
}
with patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor._create_new_repo"
) as mock_create_new_repo:
self.toddler.process_comment(issue)
mock_create_new_repo.assert_called_with(issue, {"action": "new_repo"})
def test_process_comment_valid_command_wrong_action(self):
"""
Assert that valid command will fail for invalid action.
"""
user = "zlopez"
issue = {
"id": 100,
"comments": [
{
"user": {"name": user},
"notification": False,
"comment": "@{0} valid".format(self.toddler.pagure_user),
}
],
"full_url": "https://blacklibrary.wh40k",
"content": '{"action": "new_branch"}',
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []}
}
self.toddler.process_comment(issue)
self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
issue["id"],
namespace=scm_request_processor.PROJECT_NAMESPACE,
comment="Validation can be done for 'new_repo' requests only.",
)
def test_process_comment_valid_command_unauthorized_user(self):
"""
Assert that valid command will fail for unathorized user.
"""
user = "zlopez"
issue = {
"id": 100,
"comments": [
{
"user": {"name": user},
"notification": False,
"comment": "@{0} valid".format(self.toddler.pagure_user),
}
],
"full_url": "https://blacklibrary.wh40k",
"content": '{"action": "new_repo"}',
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": ["Lucius"], "commit": [], "ticket": []}
}
self.toddler.process_comment(issue)
self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
issue["id"],
namespace=scm_request_processor.PROJECT_NAMESPACE,
comment=(
"User {0} is not a maintainer of {1}." "Can't validate this request."
).format(user, scm_request_processor.PROJECT_NAMESPACE),
)
class TestProcessNewRepo:
"""
Test class for `toddlers.plugins.scm_request_processor.SCMRequestProcessor.process_new_repo`
method. method.
""" """
@ -576,15 +794,16 @@ class TestCreateNewRepo:
self.toddler = scm_request_processor.SCMRequestProcessor() self.toddler = scm_request_processor.SCMRequestProcessor()
self.toddler.pagure_io = Mock() self.toddler.pagure_io = Mock()
self.toddler.dist_git = Mock() self.toddler.dist_git = Mock()
self.toddler.ping_comment = "{maintainers}"
def test_create_new_repo_missing_required_key(self): def test_process_new_repo_missing_required_key(self):
""" """
Assert that ticket will be closed if required key is missing in request. Assert that ticket will be closed if required key is missing in request.
""" """
issue = { issue = {
"id": 100, "id": 100,
} }
self.toddler.create_new_repo(issue, {}) self.toddler.process_new_repo(issue, {})
self.toddler.pagure_io.close_issue.assert_called_with( self.toddler.pagure_io.close_issue.assert_called_with(
100, 100,
@ -593,7 +812,7 @@ class TestCreateNewRepo:
reason="Invalid", reason="Invalid",
) )
def test_create_new_repo_invalid_repo_name(self): def test_process_new_repo_invalid_repo_name(self):
""" """
Assert that ticket will be closed if provided repository name is invalid. Assert that ticket will be closed if provided repository name is invalid.
""" """
@ -609,7 +828,7 @@ class TestCreateNewRepo:
"monitor": "monitor", "monitor": "monitor",
} }
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
error = ( error = (
"The repository name is invalid. It must be at least two " "The repository name is invalid. It must be at least two "
@ -625,196 +844,7 @@ class TestCreateNewRepo:
reason="Invalid", reason="Invalid",
) )
def test_create_new_repo_exception_validated_by_invalid_user(self): def test_process_new_repo_missing_bug_id(self):
"""
Assert that processing will be interrupted if the ticket is validated by
wrong user.
"""
# Preparation
user = "zlopez"
invalid_user = "Tzeentch"
issue = {
"id": 100,
"user": {"name": user},
"comments": [
{
"comment": "valid",
"user": {"name": invalid_user},
"notification": False,
}
],
}
repo = "repo"
branch = "main"
namespace = "tests"
bug_id = ""
action = "new_repo"
sls = {branch: "2050-06-01"}
monitor = "monitor"
exception = False
json = {
"repo": repo,
"branch": branch,
"namespace": namespace,
"bug_id": bug_id,
"action": action,
"sls": sls,
"monitor": monitor,
"exception": exception,
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []}
}
self.toddler.validation_comment = "valid"
self.toddler.create_new_repo(issue, json)
# asserts
self.toddler.pagure_io.get_project_contributors.assert_called_with(
scm_request_processor.PROJECT_NAMESPACE.split("/")[0],
scm_request_processor.PROJECT_NAMESPACE.split("/")[1],
)
def test_create_new_repo_exception_no_validation_comment(self):
"""
Assert that processing will be interrupted if the ticket validation comment
is not found.
"""
# Preparation
user = "zlopez"
issue = {
"id": 100,
"user": {"name": user},
"comments": [
{"comment": "comment", "user": {"name": user}, "notification": False}
],
}
repo = "repo"
branch = "main"
namespace = "tests"
bug_id = ""
action = "new_repo"
sls = {branch: "2050-06-01"}
monitor = "monitor"
exception = False
json = {
"repo": repo,
"branch": branch,
"namespace": namespace,
"bug_id": bug_id,
"action": action,
"sls": sls,
"monitor": monitor,
"exception": exception,
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []}
}
self.toddler.validation_comment = "valid"
self.toddler.create_new_repo(issue, json)
# asserts
self.toddler.pagure_io.get_project_contributors.assert_called_with(
scm_request_processor.PROJECT_NAMESPACE.split("/")[0],
scm_request_processor.PROJECT_NAMESPACE.split("/")[1],
)
def test_create_new_repo_exception_notify_maintainers(self):
"""
Assert that comment will be added when the ticket needs manual
validation.
"""
# Preparation
user = "zlopez"
issue = {"id": 100, "user": {"name": user}, "comments": []}
repo = "repo"
branch = "main"
namespace = "tests"
bug_id = ""
action = "new_repo"
sls = {branch: "2050-06-01"}
monitor = "monitor"
exception = False
json = {
"repo": repo,
"branch": branch,
"namespace": namespace,
"bug_id": bug_id,
"action": action,
"sls": sls,
"monitor": monitor,
"exception": exception,
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user, "Tzeentch"], "commit": [], "ticket": []}
}
self.toddler.ping_comment = "Look at this comment {maintainers}"
self.toddler.create_new_repo(issue, json)
# asserts
self.toddler.pagure_io.get_project_contributors.assert_called_with(
scm_request_processor.PROJECT_NAMESPACE.split("/")[0],
scm_request_processor.PROJECT_NAMESPACE.split("/")[1],
)
message = "Look at this comment @Tzeentch @{}".format(user)
self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
100, namespace=scm_request_processor.PROJECT_NAMESPACE, comment=message
)
def test_create_new_repo_exception_not_valid_notification_comment_present(self):
"""
Assert that comment will not be added if the toddler already commented on the
ticket.
"""
# Preparation
user = "zlopez"
issue = {
"id": 100,
"user": {"name": user},
"comments": [
{"comment": "comment", "user": {"name": user}, "notification": False}
],
}
repo = "repo"
branch = "main"
namespace = "tests"
bug_id = ""
action = "new_repo"
sls = {branch: "2050-06-01"}
monitor = "monitor"
exception = False
json = {
"repo": repo,
"branch": branch,
"namespace": namespace,
"bug_id": bug_id,
"action": action,
"sls": sls,
"monitor": monitor,
"exception": exception,
}
self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": ["Tzeentch"], "commit": [], "ticket": []}
}
self.toddler.pagure_user = user
self.toddler.create_new_repo(issue, json)
# asserts
self.toddler.pagure_io.get_project_contributors.assert_called_with(
scm_request_processor.PROJECT_NAMESPACE.split("/")[0],
scm_request_processor.PROJECT_NAMESPACE.split("/")[1],
)
self.toddler.pagure_io.add_comment_to_issue.assert_not_called()
def test_create_new_repo_missing_bug_id(self):
""" """
Assert that ticket will be closed if Bugzilla bug id is not provided. Assert that ticket will be closed if Bugzilla bug id is not provided.
""" """
@ -830,7 +860,8 @@ class TestCreateNewRepo:
"monitor": "monitor", "monitor": "monitor",
} }
self.toddler.create_new_repo(issue, json) self.toddler.dist_git.get_project.return_value = None
self.toddler.process_new_repo(issue, json)
self.toddler.pagure_io.close_issue.assert_called_with( self.toddler.pagure_io.close_issue.assert_called_with(
100, 100,
@ -842,7 +873,7 @@ class TestCreateNewRepo:
@patch( @patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
) )
def test_create_new_repo_invalid_review_bug(self, mock_validate_review_bug): def test_process_new_repo_invalid_review_bug(self, mock_validate_review_bug):
""" """
Assert that ticket will be closed if Bugzilla bug is not valid. Assert that ticket will be closed if Bugzilla bug is not valid.
""" """
@ -858,9 +889,10 @@ class TestCreateNewRepo:
"monitor": "monitor", "monitor": "monitor",
} }
self.toddler.dist_git.get_project.return_value = None
mock_validate_review_bug.side_effect = ValidationError("error") mock_validate_review_bug.side_effect = ValidationError("error")
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
mock_validate_review_bug.assert_called_with( mock_validate_review_bug.assert_called_with(
"123", "repo", "rawhide", namespace="rpms", pagure_user="zlopez" "123", "repo", "rawhide", namespace="rpms", pagure_user="zlopez"
@ -876,12 +908,7 @@ class TestCreateNewRepo:
@patch( @patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.valid_epel_package" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.valid_epel_package"
) )
@patch( def test_process_new_repo_invalid_epel(self, mock_valid_epel_package):
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
)
def test_create_new_repo_invalid_epel(
self, mock_validate_review_bug, mock_valid_epel_package
):
""" """
Assert that ticket will be closed if repo is invalid EPEL repo. Assert that ticket will be closed if repo is invalid EPEL repo.
""" """
@ -908,14 +935,10 @@ class TestCreateNewRepo:
mock_valid_epel_package.return_value = False mock_valid_epel_package.return_value = False
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
mock_valid_epel_package.assert_called_with(repo, branch) mock_valid_epel_package.assert_called_with(repo, branch)
mock_validate_review_bug.assert_called_with(
bug_id, repo, branch, namespace=namespace, pagure_user="zlopez"
)
self.toddler.pagure_io.close_issue.assert_called_with( self.toddler.pagure_io.close_issue.assert_called_with(
100, 100,
namespace=scm_request_processor.PROJECT_NAMESPACE, namespace=scm_request_processor.PROJECT_NAMESPACE,
@ -923,10 +946,7 @@ class TestCreateNewRepo:
reason="Invalid", reason="Invalid",
) )
@patch( def test_process_new_repo_requester_not_in_dist_git(self):
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
)
def test_create_new_repo_requester_not_in_dist_git(self, mock_validate_review_bug):
""" """
Assert that ticket will be commented on when requester doesn't Assert that ticket will be commented on when requester doesn't
have a valid dist git account. have a valid dist git account.
@ -955,11 +975,7 @@ class TestCreateNewRepo:
self.toddler.dist_git.user_exists.return_value = False self.toddler.dist_git.user_exists.return_value = False
self.toddler.dist_git._pagure_url = "https://src.fedoraproject.org" self.toddler.dist_git._pagure_url = "https://src.fedoraproject.org"
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
mock_validate_review_bug.assert_called_with(
bug_id, repo, branch, namespace=namespace, pagure_user="zlopez"
)
self.toddler.dist_git.user_exists.assert_called_with("zlopez") self.toddler.dist_git.user_exists.assert_called_with("zlopez")
@ -973,10 +989,7 @@ class TestCreateNewRepo:
comment=message, comment=message,
) )
@patch( def test_process_new_repo_project_exists(self):
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
)
def test_create_new_repo_project_exists(self, mock_validate_review_bug):
""" """
Assert that ticket will be closed when repo already exists in dist git. Assert that ticket will be closed when repo already exists in dist git.
""" """
@ -1003,11 +1016,7 @@ class TestCreateNewRepo:
self.toddler.dist_git.get_project.return_value = "project" self.toddler.dist_git.get_project.return_value = "project"
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
mock_validate_review_bug.assert_called_with(
bug_id, repo, branch, namespace=namespace, pagure_user="zlopez"
)
self.toddler.dist_git.get_project.assert_called_with(namespace, repo) self.toddler.dist_git.get_project.assert_called_with(namespace, repo)
@ -1018,10 +1027,7 @@ class TestCreateNewRepo:
reason="Invalid", reason="Invalid",
) )
@patch( def test_process_new_repo_master_branch(self):
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
)
def test_create_new_repo_master_branch(self, mock_validate_review_bug):
""" """
Assert that ticket will be closed when branch is set to master branch. Assert that ticket will be closed when branch is set to master branch.
Master branch is no longer allowed. Master branch is no longer allowed.
@ -1048,11 +1054,7 @@ class TestCreateNewRepo:
} }
self.toddler.dist_git.get_project.return_value = None self.toddler.dist_git.get_project.return_value = None
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
mock_validate_review_bug.assert_called_with(
bug_id, repo, branch, namespace=namespace, pagure_user="zlopez"
)
self.toddler.dist_git.get_project.assert_called_with(namespace, repo) self.toddler.dist_git.get_project.assert_called_with(namespace, repo)
@ -1063,7 +1065,7 @@ class TestCreateNewRepo:
reason="Invalid", reason="Invalid",
) )
def test_create_new_repo_unsupported_namespace(self): def test_process_new_repo_unsupported_namespace(self):
""" """
Assert that ticket will be closed when requested namespace is not recognized. Assert that ticket will be closed when requested namespace is not recognized.
""" """
@ -1087,7 +1089,7 @@ class TestCreateNewRepo:
"monitor": monitor, "monitor": monitor,
"exception": exception, "exception": exception,
} }
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
error = ( error = (
"The requested namespace '{0}' is not recognized. " "The requested namespace '{0}' is not recognized. "
@ -1105,14 +1107,15 @@ class TestCreateNewRepo:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"namespace, branch", "namespace, branch",
[ [
("rpms", "rawhide"), ("tests", "main"),
("container", "rawhide"), ("container", "rawhide"),
("flatpaks", "stable"), ("flatpaks", "stable"),
], ],
) )
def test_create_new_repo_namespaces(self, namespace, branch): def test_process_new_repo_namespaces(self, namespace, branch):
""" """
Assert that ticket will be processed when everything is in order and namespace is correct. Assert that maintainers will be notified when repo is created in
non rpms namespace.
""" """
# Preparation # Preparation
user = "zlopez" user = "zlopez"
@ -1120,7 +1123,8 @@ class TestCreateNewRepo:
"id": 100, "id": 100,
"user": {"name": user}, "user": {"name": user},
"comments": [ "comments": [
{"comment": "valid", "user": {"name": user}, "notification": False} {"notification": False, "comment": "First"},
{"notification": True},
], ],
} }
@ -1146,57 +1150,43 @@ class TestCreateNewRepo:
self.toddler.pagure_io.get_project_contributors.return_value = { self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []} "users": {"admin": [user], "commit": [], "ticket": []}
} }
self.toddler.validation_comment = "valid"
# Method to test # Method to test
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
# asserts # asserts
self.toddler.dist_git.get_project.assert_called_with(namespace, repo) self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
self.toddler.dist_git.new_project.assert_called_with( issue["id"],
namespace, repo, "", "", branch, initial_commit=True, alias=True
)
self.toddler.dist_git.set_monitoring_status.assert_called_with(
namespace, repo, monitor
)
self.toddler.dist_git.change_project_main_admin.assert_called_with(
namespace, repo, "zlopez"
)
message = "The Pagure repository was created at {0}/{1}/{2}".format(
dist_git_url, namespace, repo
)
self.toddler.pagure_io.close_issue.assert_called_with(
100,
namespace=scm_request_processor.PROJECT_NAMESPACE, namespace=scm_request_processor.PROJECT_NAMESPACE,
message=message, comment="@" + user,
reason="Processed",
) )
def test_create_new_repo_tests_namespace(self): def test_process_new_repo_ping_comment_exists(self, caplog):
""" """
Assert that ticket will be processed when everything is in order and namespace Assert that maintainers are not notified multiple times.
is set to tests.
""" """
# Preparation # Preparation
caplog.set_level(logging.INFO)
user = "zlopez" user = "zlopez"
issue = { issue = {
"id": 100, "id": 100,
"user": {"name": user}, "user": {"name": user},
"comments": [ "comments": [
{"comment": "valid", "user": {"name": user}, "notification": False} {
"comment": "@" + user,
"notification": False,
}
], ],
} }
repo = "repo" repo = "repo"
branch = "main" namespace = "rpms"
namespace = "tests" branch = "rawhide"
bug_id = "" bug_id = ""
action = "new_repo" action = "new_repo"
sls = {branch: "2050-06-01"} sls = {branch: "2050-06-01"}
monitor = "monitor" monitor = "monitor"
exception = False exception = True
json = { json = {
"repo": repo, "repo": repo,
"branch": branch, "branch": branch,
@ -1213,42 +1203,18 @@ class TestCreateNewRepo:
self.toddler.pagure_io.get_project_contributors.return_value = { self.toddler.pagure_io.get_project_contributors.return_value = {
"users": {"admin": [user], "commit": [], "ticket": []} "users": {"admin": [user], "commit": [], "ticket": []}
} }
self.toddler.validation_comment = "valid"
self.toddler.create_new_repo(issue, json) # Method to test
self.toddler.process_new_repo(issue, json)
# asserts # asserts
self.toddler.pagure_io.get_project_contributors.assert_called_with( assert caplog.records[-1].message == "- Ping comment found."
scm_request_processor.PROJECT_NAMESPACE.split("/")[0],
scm_request_processor.PROJECT_NAMESPACE.split("/")[1],
)
self.toddler.dist_git.get_project.assert_called_with(namespace, repo)
self.toddler.dist_git.new_project.assert_called_with(
namespace, repo, "", "", branch, initial_commit=True, alias=True
)
self.toddler.dist_git.set_monitoring_status.assert_called_with(
namespace, repo, monitor
)
self.toddler.dist_git.change_project_main_admin.assert_called_with(
namespace, repo, "zlopez"
)
message = "The Pagure repository was created at {0}/{1}/{2}".format(
dist_git_url, namespace, repo
)
self.toddler.pagure_io.close_issue.assert_called_with(
100,
namespace=scm_request_processor.PROJECT_NAMESPACE,
message=message,
reason="Processed",
)
@patch("toddlers.plugins.scm_request_processor.bugzilla_system") @patch("toddlers.plugins.scm_request_processor.bugzilla_system")
@patch( @patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
) )
def test_create_new_repo_non_default_branch( def test_process_new_repo_non_default_branch(
self, mock_validate_review_bug, mock_bz self, mock_validate_review_bug, mock_bz
): ):
""" """
@ -1281,7 +1247,7 @@ class TestCreateNewRepo:
self.toddler.dist_git.get_project.return_value = None self.toddler.dist_git.get_project.return_value = None
self.toddler.dist_git._pagure_url = dist_git_url self.toddler.dist_git._pagure_url = dist_git_url
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
# asserts # asserts
mock_validate_review_bug.assert_called_with( mock_validate_review_bug.assert_called_with(
@ -1317,7 +1283,7 @@ class TestCreateNewRepo:
@patch( @patch(
"toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug" "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
) )
def test_create_new_repo_default_brach(self, mock_validate_review_bug, mock_bz): def test_process_new_repo_default_brach(self, mock_validate_review_bug, mock_bz):
""" """
Assert that repo will be created with default branch when everything is in order. Assert that repo will be created with default branch when everything is in order.
""" """
@ -1346,7 +1312,7 @@ class TestCreateNewRepo:
self.toddler.dist_git.get_project.return_value = None self.toddler.dist_git.get_project.return_value = None
self.toddler.dist_git._pagure_url = dist_git_url self.toddler.dist_git._pagure_url = dist_git_url
self.toddler.create_new_repo(issue, json) self.toddler.process_new_repo(issue, json)
# asserts # asserts
mock_validate_review_bug.assert_called_with( mock_validate_review_bug.assert_called_with(

View file

@ -249,8 +249,6 @@ pagure_url = "https://pagure.io"
pagure_api_key = "API token for pagure" pagure_api_key = "API token for pagure"
# Monitoring choices for release-monitoring.org # Monitoring choices for release-monitoring.org
monitor_choices = ['no-monitoring', 'monitoring', 'monitoring-with-scratch'] monitor_choices = ['no-monitoring', 'monitoring', 'monitoring-with-scratch']
# What we should look for in validation comment
validation_comment = "valid"
# Text for the ping if the ticket needs to be manually verified # Text for the ping if the ticket needs to be manually verified
ping_comment = "This request wants to skip bugzilla validation! {maintainers} could you check if this is correct? If yes, please respond to this ticket with 'valid' comment" ping_comment = "This request wants to skip bugzilla validation! {maintainers} could you check if this is correct? If yes, please respond to this ticket with 'valid' comment"

View file

@ -186,7 +186,10 @@ class SCMRequestProcessor(ToddlerBase):
bugzilla_system.set_bz(config) bugzilla_system.set_bz(config)
try: try:
self.process_ticket(issue) if message.topic.endswith("pagure.issue.comment.added"):
self.process_comment(issue)
else:
self.process_ticket(issue)
except BaseException: except BaseException:
self.pagure_io.add_comment_to_issue( self.pagure_io.add_comment_to_issue(
issue["id"], issue["id"],
@ -196,6 +199,80 @@ class SCMRequestProcessor(ToddlerBase):
).format(traceback.format_exc()), ).format(traceback.format_exc()),
) )
def process_comment(self, issue: dict):
"""
Process comment on the issue.
Params:
issue: A dictionary containing the issue.
"""
_log.info("Processing comment on pagure ticket '{0}'".format(issue["full_url"]))
last_comment = issue["comments"][-1]
# Comment is notification
if last_comment["notification"]:
_log.info("Comment is notification. Ignoring.")
return
# Check if the comment is addressed to bot user
if "@" + self.pagure_user not in last_comment["comment"]:
_log.info("Comment is not for '{0}'. Ignoring.".format(self.pagure_user))
return
# Extract command from comment @releng-bot command
command = last_comment["comment"].split()[1]
if command == "help":
self.pagure_io.add_comment_to_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
comment=(
"{0} recognizes following commands on {1}.\n"
"help - prints this help message\n"
"retry - retries the failed request processing\n"
"valid - can be used by authorized user to mark the request as valid\n"
).format(self.pagure_user, PROJECT_NAMESPACE),
)
elif command == "retry":
self.process_ticket(issue)
elif command == "valid":
# Get full list of contributors
contributors = self.pagure_io.get_project_contributors(
PROJECT_NAMESPACE.split("/")[0], PROJECT_NAMESPACE.split("/")[1]
)
# Get the list of maintainers of the scm_requests repository
maintainers = (
set(contributors["users"]["admin"])
| set(contributors["users"]["commit"])
| set(contributors["users"]["ticket"])
)
if last_comment["user"]["name"] in maintainers:
_log.info(
"Ticket is validated by {}, continue processing".format(
last_comment["user"]["name"]
)
)
issue_body_json = json.loads(issue["content"].strip("`").strip("\n"))
if issue_body_json.get("action") == "new_repo":
self._create_new_repo(issue, issue_body_json)
else:
self.pagure_io.add_comment_to_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
comment=(
"Validation can be done for 'new_repo' requests only."
),
)
else:
self.pagure_io.add_comment_to_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
comment=(
"User {0} is not a maintainer of {1}."
"Can't validate this request."
).format(last_comment["user"]["name"], PROJECT_NAMESPACE),
)
def process_ticket(self, issue: dict): def process_ticket(self, issue: dict):
""" """
Process a single ticket Process a single ticket
@ -294,7 +371,7 @@ class SCMRequestProcessor(ToddlerBase):
return return
if issue_body.get("action") == "new_repo": if issue_body.get("action") == "new_repo":
self.create_new_repo( self.process_new_repo(
issue, issue_body, initial_commit=issue_body.get("initial_commit", True) issue, issue_body, initial_commit=issue_body.get("initial_commit", True)
) )
elif issue_body.get("action") == "new_branch": elif issue_body.get("action") == "new_branch":
@ -350,11 +427,11 @@ class SCMRequestProcessor(ToddlerBase):
'The SL "{0}" must expire on June 1st or December 1st'.format(eol) 'The SL "{0}" must expire on June 1st or December 1st'.format(eol)
) )
def create_new_repo( def process_new_repo(
self, issue: dict, issue_body_json: dict, initial_commit: bool = True self, issue: dict, issue_body_json: dict, initial_commit: bool = True
): ):
""" """
A helper function that process request for new repo. Process request for new repo.
Params: Params:
issue: the dictionary representing the issue issue: the dictionary representing the issue
@ -382,6 +459,23 @@ class SCMRequestProcessor(ToddlerBase):
) )
return return
# Validate the request first
if self._validate_new_repo_request(issue, issue_body_json):
_log.info("Ticket passed all validations. Creating repository.")
self._create_new_repo(issue, issue_body_json, initial_commit)
def _validate_new_repo_request(self, issue: dict, issue_body_json: dict) -> bool:
"""
Validate request for new repo.
Params:
issue: the dictionary representing the issue
issue_body_json: a partially validated dictionary of the JSON in the
issue body
Returns:
True if the request is valid, false otherwise.
"""
requester = issue["user"]["name"] requester = issue["user"]["name"]
namespace = issue_body_json["namespace"].strip() namespace = issue_body_json["namespace"].strip()
repo = issue_body_json["repo"].strip() repo = issue_body_json["repo"].strip()
@ -403,7 +497,7 @@ class SCMRequestProcessor(ToddlerBase):
message=message, message=message,
reason="Invalid", reason="Invalid",
) )
return return False
if not re.match(PROJECT_NAME_REGEX, repo): if not re.match(PROJECT_NAME_REGEX, repo):
error = ( error = (
@ -418,7 +512,57 @@ class SCMRequestProcessor(ToddlerBase):
message=error, message=error,
reason="Invalid", reason="Invalid",
) )
return return False
# This should never trigger because if the user requested an EPEL branch
# here, it'd have to be accompanied by a Bugzilla bug for the "Fedora EPEL"
# product. So this will only trigger if the reviewer made a mistake.
if re.match(EPEL_REGEX, branch_name) and not self.valid_epel_package(
repo, branch_name
):
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message=INVALID_EPEL_ERROR,
reason="Invalid",
)
return False
_log.info(
"- Checking if user {0} has an account in dist-git.".format(requester)
)
if not self.dist_git.user_exists(requester):
sync_comment = (
"@{0} needs to login to {1} to sync accounts "
"before we can proceed.".format(requester, self.dist_git._pagure_url)
)
self.pagure_io.add_comment_to_issue(
issue["id"], namespace=PROJECT_NAMESPACE, comment=sync_comment
)
return False
_log.info(
"- Checking if {0}/{1} already exists in dist-git.".format(namespace, repo)
)
project = self.dist_git.get_project(namespace, repo)
if project:
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message="The Pagure project already exists",
reason="Invalid",
)
return False
# Close the ticket if the requested default branch is master
if branch_name == "master":
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message="Branch `master` cannot be created, please request the right branch.",
reason="Invalid",
)
return False
if exception is True or namespace in ( if exception is True or namespace in (
"flatpaks", "flatpaks",
@ -436,44 +580,24 @@ class SCMRequestProcessor(ToddlerBase):
| set(contributors["users"]["ticket"]) | set(contributors["users"]["ticket"])
) )
# Ping the maintainers if toddler didn't comment on the ticket yet # Ping the maintainers if toddler didn't comment on the ticket yet
ping_comment = True ping_comment = self.ping_comment.format(
# This is set to true if the validation comment is set by maintainer maintainers=" ".join(["@" + user for user in sorted(maintainers)])
valid = False )
# There is already a comment on this ticket, we should check if this is
# the validation ticket and if we already responded to the ticket
if len(issue["comments"]) > 0: if len(issue["comments"]) > 0:
_log.info("- Check for validation comment") _log.info("- Check for ping comment")
for comment in issue["comments"]: for comment in issue["comments"]:
# Skip the notification comments # Skip the notification comments
if not comment["notification"]: if not comment["notification"]:
if comment["comment"].strip() == self.validation_comment: # We already pinged the maintainers
if comment["user"]["name"] in maintainers: if comment["comment"].strip() == ping_comment:
_log.info( _log.info("- Ping comment found.")
"- Ticket is validated by {}, continue processing".format( return False
comment["user"]["name"]
)
)
valid = True
# Toddler already commented on the ticket
if comment["user"]["name"] == self.pagure_user:
ping_comment = False
if not valid and not ping_comment:
_log.info("- Ticket is not yet validated. Skipping...")
return
# We didn't commented on the ticket yet, ping the maintainers # We didn't commented on the ticket yet, ping the maintainers
if ping_comment and not valid: _log.info("- Notify the responsible users")
_log.info("- Notify the responsible users") self.pagure_io.add_comment_to_issue(
self.pagure_io.add_comment_to_issue( issue["id"], namespace=PROJECT_NAMESPACE, comment=ping_comment
issue["id"], )
namespace=PROJECT_NAMESPACE, return False
comment=self.ping_comment.format(
maintainers=" ".join(
["@" + user for user in sorted(maintainers)]
)
),
)
return
else: else:
if not bug_id: if not bug_id:
self.pagure_io.close_issue( self.pagure_io.close_issue(
@ -482,7 +606,7 @@ class SCMRequestProcessor(ToddlerBase):
message="An invalid Bugzilla bug was provided", message="An invalid Bugzilla bug was provided",
reason="Invalid", reason="Invalid",
) )
return return False
try: try:
_log.info("- Checking that #{0} is a valid RHBZ.".format(bug_id)) _log.info("- Checking that #{0} is a valid RHBZ.".format(bug_id))
self.validate_review_bug( self.validate_review_bug(
@ -499,59 +623,30 @@ class SCMRequestProcessor(ToddlerBase):
message=str(error), message=str(error),
reason="Invalid", reason="Invalid",
) )
return return False
# This should never trigger because if the user requested an EPEL branch return True
# here, it'd have to be accompanied by a Bugzilla bug for the "Fedora EPEL"
# product. So this will only trigger if the reviewer made a mistake.
if re.match(EPEL_REGEX, branch_name) and not self.valid_epel_package(
repo, branch_name
):
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message=INVALID_EPEL_ERROR,
reason="Invalid",
)
return
_log.info( def _create_new_repo(
"- Checking if user {0} has an account in dist-git.".format(requester) self, issue: dict, issue_body_json: dict, initial_commit: bool = True
) ):
if not self.dist_git.user_exists(requester): """
sync_comment = ( Create new repository.
"@{0} needs to login to {1} to sync accounts "
"before we can proceed.".format(requester, self.dist_git._pagure_url)
)
self.pagure_io.add_comment_to_issue(
issue["id"], namespace=PROJECT_NAMESPACE, comment=sync_comment
)
return
_log.info(
"- Checking if {0}/{1} already exists in dist-git.".format(namespace, repo)
)
project = self.dist_git.get_project(namespace, repo)
if project:
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message="The Pagure project already exists",
reason="Invalid",
)
return
Params:
issue: the dictionary representing the issue
issue_body_json: a partially validated dictionary of the JSON in the
issue body
initial_commit: indicate whether to create an initial commit.
"""
requester = issue["user"]["name"]
namespace = issue_body_json["namespace"].strip()
repo = issue_body_json["repo"].strip()
bug_id = str(issue_body_json["bug_id"]).strip()
branch_name = issue_body_json["branch"].strip()
description = issue_body_json.get("description", "").strip() description = issue_body_json.get("description", "").strip()
upstreamurl = issue_body_json.get("upstreamurl", "").strip() upstreamurl = issue_body_json.get("upstreamurl", "").strip()
# Close the ticket if the requested default branch is master
if branch_name == "master":
self.pagure_io.close_issue(
issue["id"],
namespace=PROJECT_NAMESPACE,
message="Branch `master` cannot be created, please request the right branch.",
reason="Invalid",
)
return
if namespace in ["rpms", "container"]: if namespace in ["rpms", "container"]:
default_branch = "rawhide" default_branch = "rawhide"
elif namespace in ["flatpaks"]: elif namespace in ["flatpaks"]:
@ -559,8 +654,6 @@ class SCMRequestProcessor(ToddlerBase):
elif namespace in ["tests"]: elif namespace in ["tests"]:
default_branch = "main" default_branch = "main"
_log.info("Ticket passed all validations. Creating repository.")
# Create the Pagure repo # Create the Pagure repo
dist_git_url = "{0}/{1}/{2}".format( dist_git_url = "{0}/{1}/{2}".format(
self.dist_git._pagure_url.rstrip("/"), namespace, repo self.dist_git._pagure_url.rstrip("/"), namespace, repo