Skip to content

Commit bb89d28

Browse files
JIRA cloud issue creation and availability issue
1 parent a5cbc78 commit bb89d28

2 files changed

Lines changed: 80 additions & 34 deletions

File tree

sync2jira/downstream_issue.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import operator
2424
import os
2525
import re
26-
from typing import Any, Dict, Optional, Union
26+
from typing import Any, Dict, Optional, Tuple, Union
2727
import unicodedata
2828

2929
from dotenv import load_dotenv
@@ -316,22 +316,34 @@ def get_existing_jira_issue(client, issue, config):
316316
:rtype: JIssue or None
317317
"""
318318

319-
# Fetch the Jira issue objects using the key list.
320-
jql = _get_existing_jira_issue_query(issue)
319+
issue_keys, jql = _get_existing_jira_issue_query(issue)
321320
if not jql:
322321
return None
323322
results: ResultList[JIssue] = client.search_issues(jql)
324323
if not results:
325-
# It is _possible_ that the downstream issue exists (i.e., we did hit
326-
# it in the cache or in Snowflake) but the Jira search cannot find it.
327-
# (This happens when the issue has been archived.) Fail as gracefully
328-
# as we can here, but our caller will probably create it again.
329-
log.warning(
330-
"Previously-existing downstream issue %s not found for upstream issue %s.",
331-
issue.id,
332-
issue.url,
333-
)
334-
return None
324+
# JQL/search index can lag right after create, or hide archived issues.
325+
# Resolve by issue key (direct GET) before giving up and duplicating.
326+
results = ResultList[JIssue]()
327+
for key in issue_keys:
328+
try:
329+
found = client.issue(key)
330+
except JIRAError:
331+
continue
332+
log.info(
333+
"JQL missed key %s for upstream %s; using direct issue fetch.",
334+
key,
335+
issue.url,
336+
)
337+
results = ResultList[JIssue]((found,))
338+
break
339+
if not results:
340+
log.warning(
341+
"Downstream issue not found for upstream %s after JQL %r and direct fetch for keys %s.",
342+
issue.url,
343+
jql,
344+
issue_keys,
345+
)
346+
return None
335347

336348
# If there is more than one issue, remove duplicates and filter the list
337349
# down to one.
@@ -358,25 +370,27 @@ def get_existing_jira_issue(client, issue, config):
358370
return results[0]
359371

360372

361-
def _get_existing_jira_issue_query(issue: Issue) -> Optional[str]:
373+
def _get_existing_jira_issue_query(
374+
issue: Issue,
375+
) -> Tuple[tuple[str, ...], Optional[str]]:
362376
"""
363377
Generate a JQL query to find downstream issues corresponding to a given
364-
upstream issue. Return None if no matches were found in either our local
378+
upstream issue. Return empty tuple and None if no matches were found in either our local
365379
cache or in the Dataverse.
366380
367381
:param sync2jira.intermediary.Issue issue: Issue object
368382
:returns: A string containing the JQL query or None if no matches
369-
:rtype: str or None
383+
:rtype: Tuple[tuple[str, ...], Optional[str]]
370384
"""
371385
if result := jira_cache.get(issue.url):
372386
issue_keys = (result,)
373387
else:
374388
results = execute_snowflake_query(issue)
375389
if not results:
376-
return None
377-
issue_keys = (row[0] for row in results)
390+
return (), None
391+
issue_keys = tuple(row[0] for row in results)
378392

379-
return f"key in ({','.join(issue_keys)})"
393+
return issue_keys, f"key in ({','.join(issue_keys)})"
380394

381395

382396
def _filter_downstream_issues(
@@ -445,7 +459,11 @@ def check_comments_for_duplicate(client, result, username):
445459
"""
446460
for comment in client.comments(result):
447461
search = re.search(r"Marking as duplicate of (\w*)-(\d*)", comment.body)
448-
if search and comment.author.name == username:
462+
author = comment.author
463+
author_label = getattr(author, "displayName", None) or getattr(
464+
author, "name", None
465+
)
466+
if search and author_label == username:
449467
issue_id = search.groups()[0] + "-" + search.groups()[1]
450468
return client.issue(issue_id)
451469
return None
@@ -1139,7 +1157,9 @@ def _update_assignee(client, existing, issue, overwrite):
11391157
else:
11401158
# Without an upstream owner, update only if the downstream is not
11411159
# assigned to the project owner.
1142-
update = issue.downstream.get("owner") != existing.fields.assignee.name
1160+
update = (
1161+
issue.downstream.get("owner") != existing.fields.assignee.displayName
1162+
)
11431163
else:
11441164
# We're not overwriting, so call assign_user() only if the downstream
11451165
# doesn't already have an assignment.

tests/test_downstream_issue.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -287,55 +287,78 @@ def test_get_existing_newstyle(self, mock_client, mock_get_query, mock_filter):
287287
mock_issue_3.fields = MagicMock()
288288
mock_issue_3.fields.updated = "2025-11-30T00:00:00.0+0000"
289289

290+
def _q(keys):
291+
if not keys:
292+
return (), None
293+
return keys, f"key in ({','.join(keys)})"
294+
290295
scenarios = (
291296
{
292-
"scenario": "_get_existing_jira_issue_query returns None",
293-
"jira_results": None,
297+
"scenario": "_get_existing_jira_issue_query returns no keys",
298+
"query_return": _q(()),
294299
"search_issues": None,
295300
"filter_results": None,
296301
"expected": None,
302+
"issue_side_effect": None,
297303
},
298304
{
299-
"scenario": "Jira search returns no items",
300-
"jira_results": "mock issue key query string",
305+
"scenario": "Jira search returns no items, direct fetch fails",
306+
"query_return": _q(("MOCK-1",)),
301307
"search_issues": ResultList[JIssue](()),
302308
"filter_results": None,
303309
"expected": None,
310+
"issue_side_effect": JIRAError(),
311+
},
312+
{
313+
"scenario": "Jira search returns no items, direct fetch succeeds",
314+
"query_return": _q(("MOCK-1",)),
315+
"search_issues": ResultList[JIssue](()),
316+
"filter_results": None,
317+
"expected": mock_issue_1,
318+
"issue_side_effect": None,
304319
},
305320
{
306321
"scenario": "Jira search returns one item",
307-
"jira_results": "mock issue key query string",
322+
"query_return": _q(("MOCK-1",)),
308323
"search_issues": ResultList[JIssue]((mock_issue_1,)),
309324
"filter_results": None,
310325
"expected": mock_issue_1,
326+
"issue_side_effect": None,
311327
},
312328
{
313329
"scenario": "_filter_downstream_issues returns one item",
314-
"jira_results": "mock issue key query string",
330+
"query_return": _q(("MOCK-1", "MOCK-2", "MOCK-3")),
315331
"search_issues": ResultList[JIssue](
316332
(mock_issue_1, mock_issue_2, mock_issue_3)
317333
),
318334
"filter_results": ResultList[JIssue]((mock_issue_3,)),
319335
"expected": mock_issue_3,
336+
"issue_side_effect": None,
320337
},
321338
{
322339
"scenario": "_filter_downstream_issues returns multiple items",
323-
"jira_results": "mock issue key query string",
340+
"query_return": _q(("MOCK-1", "MOCK-2", "MOCK-3")),
324341
"search_issues": ResultList[JIssue](
325342
(mock_issue_1, mock_issue_2, mock_issue_3)
326343
),
327344
"filter_results": ResultList[JIssue](
328345
(mock_issue_1, mock_issue_2, mock_issue_3)
329346
),
330347
"expected": mock_issue_2, # Most-recently updated
348+
"issue_side_effect": None,
331349
},
332350
)
333351

334352
for x in scenarios:
335353
d.jira_cache = d.UrlCache() # Clear the cache
336-
mock_get_query.return_value = x["jira_results"]
354+
mock_get_query.return_value = x["query_return"]
337355
mock_client.search_issues.return_value = x["search_issues"]
338356
mock_filter.return_value = x["filter_results"]
357+
if x["issue_side_effect"] is not None:
358+
mock_client.issue.side_effect = x["issue_side_effect"]
359+
else:
360+
mock_client.issue.side_effect = None
361+
mock_client.issue.return_value = mock_issue_1
339362
result = d.get_existing_jira_issue(
340363
client=mock_client, issue=self.mock_issue, config=self.mock_config
341364
)
@@ -349,17 +372,17 @@ def test_get_existing_jira_issue_query(self, mock_snowflake):
349372
{
350373
"jira_cache": {self.mock_issue.url: "issue_key"},
351374
"snowflake": (),
352-
"expected": "key in (issue_key)",
375+
"expected": (("issue_key",), "key in (issue_key)"),
353376
},
354377
{
355378
"jira_cache": {},
356379
"snowflake": (),
357-
"expected": None,
380+
"expected": ((), None),
358381
},
359382
{
360383
"jira_cache": {},
361384
"snowflake": (("issue_key",),),
362-
"expected": "key in (issue_key)",
385+
"expected": (("issue_key",), "key in (issue_key)"),
363386
},
364387
{
365388
"jira_cache": {},
@@ -368,7 +391,10 @@ def test_get_existing_jira_issue_query(self, mock_snowflake):
368391
("issue_key_2",),
369392
("issue_key_3",),
370393
),
371-
"expected": "key in (issue_key_1,issue_key_2,issue_key_3)",
394+
"expected": (
395+
("issue_key_1", "issue_key_2", "issue_key_3"),
396+
"key in (issue_key_1,issue_key_2,issue_key_3)",
397+
),
372398
},
373399
)
374400

@@ -1697,7 +1723,7 @@ def test_check_comments_for_duplicates(self, mock_client):
16971723
# Set up return values
16981724
mock_comment = MagicMock()
16991725
mock_comment.body = "Marking as duplicate of TEST-1234"
1700-
mock_comment.author.name = "mock_user"
1726+
mock_comment.author.displayName = "mock_user"
17011727
mock_client.comments.return_value = [mock_comment]
17021728
mock_client.issue.return_value = "Successful Call!"
17031729

0 commit comments

Comments
 (0)