Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storypoint touchup #237

Merged
merged 3 commits into from
Nov 25, 2024
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
13 changes: 5 additions & 8 deletions sync2jira/downstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,10 @@ def _get_preferred_issue_types(config, issue):
# 'enhancement': 'Story'
# }
type_list = []
log.debug(config)

map = config['sync2jira'].get('map', {})
conf = map.get('github', {}).get(issue.upstream, {})

log.debug(conf)
# we consider the issue_types mapping if it exists. If it does, exclude all other logic.
if 'issue_types' in conf:
for tag, issue_type in conf['issue_types'].items():
Expand Down Expand Up @@ -616,11 +614,6 @@ def _create_jira_issue(client, issue, config):
:returns: Returns JIRA issue that was created
:rtype: jira.resources.Issue
"""
log.info("Creating %r issue for %r", issue.downstream, issue)
if config['sync2jira']['testing']:
log.info("Testing flag is true. Skipping actual creation.")
return

custom_fields = issue.downstream.get('custom_fields', {})
preferred_types = _get_preferred_issue_types(config, issue)
description = _build_description(issue)
Expand All @@ -646,7 +639,11 @@ def _create_jira_issue(client, issue, config):
if 'labels' in issue.downstream.keys():
kwargs['labels'] = issue.downstream['labels']

log.info("Creating issue.")
log.info("Creating issue for %r: %r", issue, kwargs)
webbnh marked this conversation as resolved.
Show resolved Hide resolved
if config['sync2jira']['testing']:
log.info("Testing flag is true. Skipping actual creation.")
return

downstream = client.create_issue(**kwargs)

# Add Epic link, QA, EXD-Service field if present
Expand Down
80 changes: 49 additions & 31 deletions sync2jira/upstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,39 +325,46 @@ def github_issues(upstream, config):

issue_updates = config['sync2jira']['map']['github'][upstream].get('issue_updates', {})

if 'github_project_fields' in issue_updates:
if '/pull/' in issue.get('html_url', ''):
webbnh marked this conversation as resolved.
Show resolved Hide resolved
log.debug(
"Issue %s/%s#%s is a pull request; skipping project fields",
orgname, reponame, issue['number'])
elif 'github_project_fields' in issue_updates:
issue['storypoints'] = None
issue['priority'] = ''
issuenumber = issue['number']
github_project_fields = config['sync2jira']['map']['github'][upstream]['github_project_fields']
variables = {"orgname": orgname, "reponame": reponame, "issuenumber": issuenumber}
response = requests.post(graphqlurl, headers=headers, json={"query": ghquery, "variables": variables})
if response.status_code != 200:
log.debug("Error while fetching %s/%s/Issue#%s: %s", orgname, reponame, issuenumber, response.text)
log.debug("HTTP error while fetching issue %s/%s#%s: %s",
orgname, reponame, issuenumber, response.text)
continue
data = response.json()
gh_issue = data['data']['repository']['issue']
if not gh_issue:
log.debug("Error fetching issue from GitHub: %s. org: %s, repo: %s", response.text, orgname, reponame)
log.debug("GitHub error while fetching issue %s/%s#%s: %s",
orgname, reponame, issuenumber, response.text)
continue
gh_field_name = ''
try:
currentProjectNode = _get_current_project_node(upstream, project_number, issuenumber, gh_issue)
if not currentProjectNode:
continue
for item in currentProjectNode['fieldValues']['nodes']:
if 'fieldName' in item:
gh_field_name = item['fieldName'].get('name')
if 'priority' in github_project_fields\
and gh_field_name == github_project_fields['priority']['gh_field']:
issue['priority'] = item.get('name')
if 'storypoints' in github_project_fields\
and gh_field_name == github_project_fields['storypoints']['gh_field']:
project_node = _get_current_project_node(
upstream, project_number, issuenumber, gh_issue)
if project_node:
item_nodes = project_node.get('fieldValues', {}).get('nodes', {})
for item in item_nodes:
gh_field_name = item.get('fieldName', {}).get('name')
if not gh_field_name:
continue
prio_field = github_project_fields.get('priority', {}).get('gh_field')
if gh_field_name == prio_field:
issue['priority'] = item.get('name')
sp_field = github_project_fields.get('storypoints', {}).get('gh_field')
if gh_field_name == sp_field:
try:
issue['storypoints'] = int(item['number'])
except KeyError as err:
log.debug("Error fetching %s!r from GitHub %s/%s#%s: %s",
gh_field_name, orgname, reponame, issuenumber, err)
continue
except (ValueError, KeyError) as err:
log.debug(
"Error while processing storypoints for issue %s/%s#%s: %s",
orgname, reponame, issuenumber, err)

final_issues.append(issue)

Expand All @@ -373,25 +380,36 @@ def _get_current_project_node(upstream, project_number, issue_number, gh_issue):
project_items = gh_issue['projectItems']['nodes']
# If there are no project items, there is nothing to return.
if len(project_items) == 0:
log.debug("Issue %s/%s is not associated with any project", upstream, issue_number)
log.debug("Issue %s#%s is not associated with any project",
upstream, issue_number)
return None
# If there is exactly one project item, return it if we don't have a configured project.
if not project_number and len(project_items) == 1:
return project_items[0]
# There are multiple projects associated with this issue; if we don't have a configured
# project, then we don't know which one to return, so return none.

if not project_number:
prj = ", ".join([":".join([x['project']['url'], x['project']['title']]) for x in project_items])
# We don't have a configured project. If there is exactly one project
# item, we'll assume it's the right one and return it.
if len(project_items) == 1:
return project_items[0]

# There are multiple projects associated with this issue; since we
# don't have a configured project, we don't know which one to return,
# so return none.
prj = (f"{x['project']['url']}: {x['project']['title']}" for x in project_items)
log.debug(
"Project number is not configured, and the issue %s/%s is associated with more than one project: %s",
upstream, issue_number, prj)
"Project number is not configured, and the issue %s#%s"
" is associated with more than one project: %s",
upstream, issue_number, ", ".join(prj))
return None
# Return the associated project which matches the configured project if we can find it.

# Return the associated project which matches the configured project if we
# can find it.
for item in project_items:
if item['project']['number'] == project_number:
return item

log.debug("Issue is not associated with the configured project, but other associations exist.")
log.debug(
"Issue %s#%s is associated with multiple projects, "
"but none match the configured project.",
upstream, issue_number)
return None


Expand Down
61 changes: 20 additions & 41 deletions tests/test_upstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,47 +670,26 @@ def test_fetch_github_data(self, mock_requests):
headers='mock_headers'
)

def test_get_current_project_node_no_projects(self):
"""
This function tests '_get_current_project_node' where there are no project nodes
"""
gh_issue = {'projectItems': {'nodes': []}}
result = u._get_current_project_node('org/repo', 1, 'mock_number', gh_issue)
self.assertIsNone(result)

def test_get_current_project_node_single_project_no_project_number(self):
"""
This function tests '_get_current_project_node' where there is a single project
node and no project number
"""
gh_issue = {'projectItems': {'nodes': [{'project': {'number': 1}}]}}
result = u._get_current_project_node('org/repo', None, 'mock_number', gh_issue)
self.assertEqual(result, {'project': {'number': 1}})
def test_get_current_project_node(self):
"""This function tests '_get_current_project_node' in a matrix of cases.

def test_get_current_project_node_multiple_projects_no_project_number(self):
"""
This function tests '_get_current_project_node' where there are multiple project nodes
and no project number
It tests issues with zero, one, and two associated projects when the
call is made with no configured project, with a project which matches
none of the associated projects, and with a project which matches one.
"""
gh_issue = {'projectItems': {'nodes': [
nodes = [
{'project': {'number': 1, 'url': 'url1', 'title': 'title1'}},
{'project': {'number': 2, 'url': 'url2', 'title': 'title2'}}]}}
result = u._get_current_project_node('org/repo', None, 'mock_number', gh_issue)
self.assertIsNone(result)

def test_get_current_project_node_project_not_associated(self):
"""
This function tests '_get_current_project_node' where the issue is not associated with
the configured project
"""
gh_issue = {'projectItems': {'nodes': [{'project': {'number': 1}}]}}
result = u._get_current_project_node('org/repo', 2, 'mock_number', gh_issue)
self.assertIsNone(result)

def test_get_current_project_node_success(self):
"""
This function tests '_get_current_project_node' where everything goes smoothly
"""
gh_issue = {'projectItems': {'nodes': [{'project': {'number': 1}}, {'project': {'number': 2}}]}}
result = u._get_current_project_node('org/repo', 2, 'mock_number', gh_issue)
self.assertEqual(result, {'project': {'number': 2}})
{'project': {'number': 2, 'url': 'url2', 'title': 'title2'}}]
projects = [None, 2, 5]

for project in projects:
for node_count in range(len(nodes)+1):
gh_issue = {'projectItems': {'nodes': nodes[:node_count]}}
result = u._get_current_project_node(
'org/repo', project, 'mock_number', gh_issue)
expected_result = (
None if node_count == 0 else
(nodes[0] if project is None else None) if node_count == 1 else
nodes[1] if project == 2 else
None)
self.assertEqual(result, expected_result)