diff --git a/mlx/jira_traceability/jira_interaction.py b/mlx/jira_traceability/jira_interaction.py index 02dfad6..df09aa1 100644 --- a/mlx/jira_traceability/jira_interaction.py +++ b/mlx/jira_traceability/jira_interaction.py @@ -3,38 +3,9 @@ from jira import JIRA, JIRAError from sphinx.util.logging import getLogger +from .jira_utils import format_jira_error -LOGGER = getLogger(__name__) - - -def fetch_user(jira, username): - """ Fetch Jira User based on username, including inactive users. - - If no matching user is found, a warning is logged and None is returned. - If multiple users are found, a warning is logged and the first returned user is used. - - Args: - jira (jira.JIRA): Jira interface object - username (str): Username (should be email address in case of Jira Cloud) - - Returns: - jira.User: User object found for username - None: No Jira user was found - """ - is_jira_cloud = '@' in username - if is_jira_cloud: - users = jira.search_users(query=username, includeInactive=True) - else: - users = jira.search_users(user=username, includeInactive=True) - if len(users) != 1: - if len(users) == 0: - warning_msg = f"Could not find any Jira user based on {username!r}" - else: - warning_msg = f"Could not find a deterministic Jira user based on {username!r}: got {users}. Using first." - LOGGER.warning(warning_msg, location=__file__) - if users: - return users[0] - return None +LOGGER = getLogger('mlx.jira_traceability') def create_jira_issues(settings, traceability_collection): @@ -65,8 +36,12 @@ def create_jira_issues(settings, traceability_collection): relevant_item_ids = traceability_collection.get_items(settings['item_to_ticket_regex']) if relevant_item_ids: - jira = JIRA({"server": settings['api_endpoint']}, basic_auth=(settings['username'], settings['password'])) - create_unique_issues(relevant_item_ids, jira, general_fields, settings, traceability_collection) + try: + jira = JIRA({"server": settings['api_endpoint']}, basic_auth=(settings['username'], settings['password'])) + create_unique_issues(relevant_item_ids, jira, general_fields, settings, traceability_collection) + except JIRAError as err: + error_msg = format_jira_error(err) + raise Exception(error_msg) from err def create_unique_issues(item_ids, jira, general_fields, settings, traceability_collection): @@ -125,9 +100,7 @@ def create_unique_issues(item_ids, jira, general_fields, settings, traceability_ fields['description'] = description if assignee and not settings.get('notify_watchers', False): - user = fetch_user(jira, assignee) - if user: - fields['assignee'] = {'id': user.accountId} if hasattr(user, 'accountId') else {'name': user.name} + fields['assignee'] = {'name': assignee} assignee = '' issue = push_item_to_jira(jira, {**fields, **general_fields}, item, attendees, assignee) @@ -159,19 +132,14 @@ def push_item_to_jira(jira, fields, item, attendees, assignee): try: issue.update(update={"timetracking": [{"edit": {"originalEstimate": effort}}]}) except JIRAError: + # If effort update fails, append to description instead issue.update(description="{}\n\nEffort estimate: {}".format(item.content, effort)) for attendee in attendees: - user = fetch_user(jira, attendee) - if user is None: - continue - account_id_or_name = user.accountId if hasattr(user, 'accountId') else user.name try: - jira.add_watcher(issue, account_id_or_name) + jira.add_watcher(issue, attendee) except JIRAError as err: - LOGGER.warning("Jira interaction failed: item {}: error code {}: {}" - .format(item.identifier, err.status_code, err.response.text)) - + LOGGER.warning("Could not add watcher {} to issue {}: {}".format(attendee, issue.key, err.text)) if assignee: jira.assign_issue(issue, assignee) return issue diff --git a/mlx/jira_traceability/jira_traceability.py b/mlx/jira_traceability/jira_traceability.py index 3a103c2..6d043b1 100644 --- a/mlx/jira_traceability/jira_traceability.py +++ b/mlx/jira_traceability/jira_traceability.py @@ -2,7 +2,7 @@ from .jira_interaction import create_jira_issues -LOGGER = getLogger(__name__) +LOGGER = getLogger('mlx.jira_traceability') def jira_interaction(app): @@ -15,7 +15,7 @@ def jira_interaction(app): create_jira_issues(app.config.traceability_jira_automation, app.builder.env.traceability_collection) except Exception as err: # pylint: disable=broad-except if app.config.traceability_jira_automation.get('errors_to_warnings', True): - LOGGER.warning("Jira interaction failed: {}".format(err)) + LOGGER.warning("Jira interaction failed: %s", str(err)) else: raise err diff --git a/mlx/jira_traceability/jira_utils.py b/mlx/jira_traceability/jira_utils.py new file mode 100644 index 0000000..18ce385 --- /dev/null +++ b/mlx/jira_traceability/jira_utils.py @@ -0,0 +1,27 @@ +"""Utility functions for JIRA error handling and formatting""" + +from jira.exceptions import JIRAError + + +def format_jira_error(err): + """Format a JIRAError into a readable error message using its attributes.""" + if isinstance(err, JIRAError): + # Use the documented JIRAError attributes + parts = [] + + if hasattr(err, 'text') and err.text: + parts.append(err.text) + + if hasattr(err, 'status_code') and err.status_code: + parts.append(f"HTTP {err.status_code}") + + if hasattr(err, 'url') and err.url: + parts.append(f"at {err.url}") + + if parts: + return f"JIRA error: {' - '.join(parts)}" + else: + # Fallback to string representation if attributes are missing + return f"JIRA error: {str(err)}" + else: + return f"Error: {str(err)}" diff --git a/tests/test_jira_interaction.py b/tests/test_jira_interaction.py index b617146..73cf043 100644 --- a/tests/test_jira_interaction.py +++ b/tests/test_jira_interaction.py @@ -84,7 +84,7 @@ def test_missing_endpoint(self, *_): dut.create_jira_issues(self.settings, None) self.assertEqual( cm.output, - ["WARNING:sphinx.mlx.jira_traceability.jira_interaction:Jira interaction failed: configuration is " + ["WARNING:sphinx.mlx.jira_traceability:Jira interaction failed: configuration is " "missing mandatory values for keys ['api_endpoint']"] ) @@ -94,7 +94,7 @@ def test_missing_username(self, *_): dut.create_jira_issues(self.settings, None) self.assertEqual( cm.output, - ["WARNING:sphinx.mlx.jira_traceability.jira_interaction:Jira interaction failed: configuration is " + ["WARNING:sphinx.mlx.jira_traceability:Jira interaction failed: configuration is " "missing mandatory values for keys ['username']"] ) @@ -106,7 +106,7 @@ def test_missing_all_mandatory(self, *_): dut.create_jira_issues(self.settings, None) self.assertEqual( cm.output, - ["WARNING:sphinx.mlx.jira_traceability.jira_interaction:Jira interaction failed: configuration is " + ["WARNING:sphinx.mlx.jira_traceability:Jira interaction failed: configuration is " "missing mandatory values for keys {}".format(mandatory_keys)] ) @@ -119,7 +119,7 @@ def test_missing_all_optional_one_mandatory(self, *_): dut.create_jira_issues(self.settings, None) self.assertEqual( cm.output, - ["WARNING:sphinx.mlx.jira_traceability.jira_interaction:Jira interaction failed: configuration is " + ["WARNING:sphinx.mlx.jira_traceability:Jira interaction failed: configuration is " "missing mandatory values for keys ['password']"] ) @@ -242,10 +242,10 @@ def test_prevent_duplication(self, jira): self.assertEqual( cm.output, - ["WARNING:sphinx.mlx.jira_traceability.jira_interaction:Won't create a Task for item " + ["WARNING:sphinx.mlx.jira_traceability:Won't create a Task for item " "'ACTION-12345_ACTION_1' because the Jira API query to check to prevent " "duplication returned ['Jira already contains this ticket']", - "WARNING:sphinx.mlx.jira_traceability.jira_interaction:Won't create a Task for item " + "WARNING:sphinx.mlx.jira_traceability:Won't create a Task for item " "'ACTION-12345_ACTION_2' because the Jira API query to check to prevent " "duplication returned ['Jira already contains this ticket']"] ) @@ -293,10 +293,9 @@ def test_default_project(self, jira): def test_add_watcher_jira_error(self, jira): self.maxDiff = None - Response = namedtuple('Response', 'text') def jira_add_watcher_mock(*_): - raise JIRAError(status_code=401, response=Response('dummy msg')) + raise JIRAError(status_code=401, text='dummy msg') jira_mock = jira.return_value jira_mock.search_issues.return_value = [] @@ -305,11 +304,14 @@ def jira_add_watcher_mock(*_): with self.assertLogs(level=WARNING) as cm: dut.create_jira_issues(self.settings, self.coll) - error_msg = ("WARNING:sphinx.mlx.jira_traceability.jira_interaction:Jira interaction failed: " - "item ACTION-12345_ACTION_1: error code 401: dummy msg") + issue = jira_mock.create_issue.return_value + error_msg_abc = (f"WARNING:sphinx.mlx.jira_traceability:Could not add watcher ABC to issue " + f"{issue.key}: dummy msg") + error_msg_zzz = (f"WARNING:sphinx.mlx.jira_traceability:Could not add watcher ZZZ to issue " + f"{issue.key}: dummy msg") self.assertEqual( cm.output, - [error_msg, error_msg] + [error_msg_abc, error_msg_zzz] ) def test_tuple_for_relationship_to_parent(self, jira):