Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions jbi/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ Let's create a `new_action`!

```python
from jbi import ActionResult, Operation
from jbi.models import BugzillaBug, BugzillaWebhookEvent

JIRA_REQUIRED_PERMISSIONS = {"CREATE_ISSUES"}

def init(jira_project_key, optional_param=42):

def execute(payload) -> ActionResult:
def execute(bug: BugzillaBug, event: BugzillaWebhookEvent) -> ActionResult:
print(f"{optional_param}, going to {jira_project_key}!")
return True, {"result": 42}

Expand All @@ -26,10 +27,10 @@ Let's create a `new_action`!

1. In the above example the `jira_project_key` parameter is required
1. `optional_param`, which has a default value, is not required to run this action
1. `init()` returns a `__call__`able object that the system calls with the Bugzilla request payload
1. `init()` returns a `__call__`able object that the system calls with the Bugzilla bug and WebHook event objects
1. The returned `ActionResult` features a boolean to indicate whether something was performed or not, along with a `Dict` (used as a response to the WebHook endpoint).

1. Use the `payload` to perform the desired processing!
1. Use the `bug` and `event` information to perform the desired processing!
1. List the required Jira permissions to be set on projects that will use this action in the `JIRA_REQUIRED_PERMISSIONS` constant. The list of built-in permissions is [available on Atlanssian API docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-permission-schemes/#built-in-permissions).
1. Use the available service calls from `jbi/services` (or make new ones)
1. Update the `README.md` to document your action
Expand Down
103 changes: 53 additions & 50 deletions jbi/actions/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
from jbi import ActionResult, Operation
from jbi.environment import get_settings
from jbi.errors import ActionError
from jbi.models import (
ActionLogContext,
BugzillaBug,
BugzillaWebhookRequest,
JiraContext,
)
from jbi.models import ActionLogContext, BugzillaBug, BugzillaWebhookEvent, JiraContext
from jbi.services import bugzilla, jira

settings = get_settings()
Expand Down Expand Up @@ -54,32 +49,36 @@ def __init__(self, jira_project_key, **kwargs):
self.jira_client = jira.get_client()

def __call__( # pylint: disable=inconsistent-return-statements
self, payload: BugzillaWebhookRequest
self,
bug: BugzillaBug,
event: BugzillaWebhookEvent,
) -> ActionResult:
"""Called from BZ webhook when default action is used. All default-action webhook-events are processed here."""
target = payload.event.target # type: ignore
target = event.target # type: ignore
if target == "comment":
return self.comment_create_or_noop(payload=payload) # type: ignore
return self.comment_create_or_noop(bug=bug, event=event) # type: ignore
if target == "bug":
return self.bug_create_or_update(payload=payload)
return self.bug_create_or_update(bug=bug, event=event)
logger.debug(
"Ignore event target %r",
target,
extra=ActionLogContext(
request=payload,
bug=bug,
event=event,
operation=Operation.IGNORE,
).dict(),
)
return False, {}

def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResult:
def comment_create_or_noop(
self, bug: BugzillaBug, event: BugzillaWebhookEvent
) -> ActionResult:
"""Confirm issue is already linked, then apply comments; otherwise noop"""
bug_obj = payload.bug
linked_issue_key = bug_obj.extract_from_see_also()
linked_issue_key = bug.extract_from_see_also()

log_context = ActionLogContext(
request=payload,
bug=bug_obj,
event=event,
bug=bug,
operation=Operation.COMMENT,
jira=JiraContext(
issue=linked_issue_key,
Expand All @@ -89,19 +88,19 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResul
if not linked_issue_key:
logger.debug(
"No Jira issue linked to Bug %s",
bug_obj.id,
bug.id,
extra=log_context.dict(),
)
return False, {}

if bug_obj.comment is None:
if bug.comment is None:
logger.debug(
"No matching comment found in payload",
extra=log_context.dict(),
)
return False, {}

formatted_comment = payload.map_as_jira_comment()
formatted_comment = bug.map_event_as_comment(event)
jira_response = self.jira_client.issue_add_comment(
issue_key=linked_issue_key,
comment=formatted_comment,
Expand All @@ -113,45 +112,45 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResul
)
return True, {"jira_response": jira_response}

def jira_fields(self, bug_obj: BugzillaBug):
def jira_fields(self, bug: BugzillaBug):
"""Extract bug info as jira issue dictionary"""
fields: dict[str, Any] = {
"summary": bug_obj.summary,
"summary": bug.summary,
}

if self.sync_whiteboard_labels:
fields["labels"] = bug_obj.get_jira_labels()
fields["labels"] = bug.get_jira_labels()

return fields

def jira_comments_for_update(
self,
payload: BugzillaWebhookRequest,
bug: BugzillaBug,
event: BugzillaWebhookEvent,
):
"""Returns the comments to post to Jira for a changed bug"""
return payload.map_as_comments()
return bug.map_changes_as_comments(event)

def update_issue(
self,
payload: BugzillaWebhookRequest,
bug_obj: BugzillaBug,
bug: BugzillaBug,
event: BugzillaWebhookEvent,
linked_issue_key: str,
is_new: bool,
):
"""Allows sub-classes to modify the Jira issue in response to a bug event"""

def bug_create_or_update(
self, payload: BugzillaWebhookRequest
self, bug: BugzillaBug, event: BugzillaWebhookEvent
) -> ActionResult: # pylint: disable=too-many-locals
"""Create and link jira issue with bug, or update; rollback if multiple events fire"""
bug_obj = payload.bug
linked_issue_key = bug_obj.extract_from_see_also() # type: ignore
linked_issue_key = bug.extract_from_see_also() # type: ignore
if not linked_issue_key:
return self.create_and_link_issue(payload, bug_obj)
return self.create_and_link_issue(bug=bug, event=event)

log_context = ActionLogContext(
request=payload,
bug=bug_obj,
event=event,
bug=bug,
operation=Operation.LINK,
jira=JiraContext(
issue=linked_issue_key,
Expand All @@ -162,14 +161,14 @@ def bug_create_or_update(
logger.debug(
"Update fields of Jira issue %s for Bug %s",
linked_issue_key,
bug_obj.id,
bug.id,
extra=log_context.dict(),
)
jira_response_update = self.jira_client.update_issue_field(
key=linked_issue_key, fields=self.jira_fields(bug_obj)
key=linked_issue_key, fields=self.jira_fields(bug)
)

comments = self.jira_comments_for_update(payload)
comments = self.jira_comments_for_update(bug=bug, event=event)
jira_response_comments = []
for i, comment in enumerate(comments):
logger.debug(
Expand All @@ -184,33 +183,35 @@ def bug_create_or_update(
)
)

self.update_issue(payload, bug_obj, linked_issue_key, is_new=False)
self.update_issue(bug, event, linked_issue_key, is_new=False)

return True, {"jira_responses": [jira_response_update, jira_response_comments]}

def create_and_link_issue( # pylint: disable=too-many-locals
self, payload, bug_obj
self,
bug,
event,
) -> ActionResult:
"""create jira issue and establish link between bug and issue; rollback/delete if required"""
log_context = ActionLogContext(
request=payload,
bug=bug_obj,
event=event,
bug=bug,
operation=Operation.CREATE,
jira=JiraContext(
project=self.jira_project_key,
),
)
logger.debug(
"Create new Jira issue for Bug %s",
bug_obj.id,
bug.id,
extra=log_context.dict(),
)
comment_list = self.bugzilla_client.get_comments(bug_obj.id)
comment_list = self.bugzilla_client.get_comments(bug.id)
description = comment_list[0].text[:JIRA_DESCRIPTION_CHAR_LIMIT]

fields = {
**self.jira_fields(bug_obj), # type: ignore
"issuetype": {"name": bug_obj.issue_type()},
**self.jira_fields(bug), # type: ignore
"issuetype": {"name": bug.issue_type()},
"description": description,
"project": {"key": self.jira_project_key},
}
Expand All @@ -236,9 +237,9 @@ def create_and_link_issue( # pylint: disable=too-many-locals

# In the time taken to create the Jira issue the bug may have been updated so
# re-retrieve it to ensure we have the latest data.
bug_obj = self.bugzilla_client.get_bug(bug_obj.id)
bug = self.bugzilla_client.get_bug(bug.id)

jira_key_in_bugzilla = bug_obj.extract_from_see_also()
jira_key_in_bugzilla = bug.extract_from_see_also()
_duplicate_creation_event = (
jira_key_in_bugzilla is not None
and jira_key_in_response != jira_key_in_bugzilla
Expand All @@ -247,7 +248,7 @@ def create_and_link_issue( # pylint: disable=too-many-locals
logger.warning(
"Delete duplicated Jira issue %s from Bug %s",
jira_key_in_response,
bug_obj.id,
bug.id,
extra=log_context.update(operation=Operation.DELETE).dict(),
)
jira_response_delete = self.jira_client.delete_issue(
Expand All @@ -259,14 +260,14 @@ def create_and_link_issue( # pylint: disable=too-many-locals
logger.debug(
"Link %r on Bug %s",
jira_url,
bug_obj.id,
bug.id,
extra=log_context.update(operation=Operation.LINK).dict(),
)
bugzilla_response = self.bugzilla_client.update_bug(
bug_obj.id, see_also_add=jira_url
bug.id, see_also_add=jira_url
)

bugzilla_url = f"{settings.bugzilla_base_url}/show_bug.cgi?id={bug_obj.id}"
bugzilla_url = f"{settings.bugzilla_base_url}/show_bug.cgi?id={bug.id}"
logger.debug(
"Link %r on Jira issue %s",
bugzilla_url,
Expand All @@ -282,7 +283,9 @@ def create_and_link_issue( # pylint: disable=too-many-locals
icon_title=icon_url,
)

self.update_issue(payload, bug_obj, jira_key_in_response, is_new=True)
self.update_issue(
bug=bug, event=event, linked_issue_key=jira_key_in_response, is_new=True
)

return True, {
"bugzilla_response": bugzilla_response,
Expand Down
34 changes: 14 additions & 20 deletions jbi/actions/default_with_assignee_and_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@
JIRA_REQUIRED_PERMISSIONS as DEFAULT_JIRA_REQUIRED_PERMISSIONS,
)
from jbi.actions.default import DefaultExecutor
from jbi.models import (
ActionLogContext,
BugzillaBug,
BugzillaWebhookRequest,
JiraContext,
)
from jbi.models import ActionLogContext, BugzillaBug, BugzillaWebhookEvent, JiraContext

logger = logging.getLogger(__name__)

Expand All @@ -45,25 +40,26 @@ def __init__(self, status_map, resolution_map, **kwargs):

def jira_comments_for_update(
self,
payload: BugzillaWebhookRequest,
bug: BugzillaBug,
event: BugzillaWebhookEvent,
):
"""Returns the comments to post to Jira for a changed bug"""
return payload.map_as_comments(
status_log_enabled=False, assignee_log_enabled=False
return bug.map_changes_as_comments(
event, status_log_enabled=False, assignee_log_enabled=False
)

def update_issue(
self,
payload: BugzillaWebhookRequest,
bug_obj: BugzillaBug,
bug: BugzillaBug,
event: BugzillaWebhookEvent,
linked_issue_key: str,
is_new: bool,
):
changed_fields = payload.event.changed_fields() or []
changed_fields = event.changed_fields() or []

log_context = ActionLogContext(
request=payload,
bug=bug_obj,
event=event,
bug=bug,
operation=Operation.UPDATE,
jira=JiraContext(
project=self.jira_project_key,
Expand All @@ -85,17 +81,15 @@ def clear_assignee():
# If this is a new issue or if the bug's assignee has changed then
# update the assignee.
if is_new or "assigned_to" in changed_fields:
if bug_obj.assigned_to == "[email protected]":
if bug.assigned_to == "[email protected]":
clear_assignee()
else:
logger.debug(
"Attempting to update assignee",
extra=log_context.dict(),
)
# Look up this user in Jira
users = self.jira_client.user_find_by_user_string(
query=bug_obj.assigned_to
)
users = self.jira_client.user_find_by_user_string(query=bug.assigned_to)
if len(users) == 1:
try:
# There doesn't appear to be an easy way to verify that
Expand Down Expand Up @@ -125,7 +119,7 @@ def clear_assignee():
# changed then update the issue status.
if is_new or "status" in changed_fields or "resolution" in changed_fields:
# If action has configured mappings for the issue resolution field, update it.
bz_resolution = bug_obj.resolution
bz_resolution = bug.resolution
jira_resolution = self.resolution_map.get(bz_resolution)
if jira_resolution:
logger.debug(
Expand All @@ -150,7 +144,7 @@ def clear_assignee():
)

# We use resolution if one exists or status otherwise.
bz_status = bz_resolution or bug_obj.status
bz_status = bz_resolution or bug.status
jira_status = self.status_map.get(bz_status)
if jira_status:
logger.debug(
Expand Down
Loading