Skip to content

Commit 39d0bd8

Browse files
PR to issue full sync (#470)
* PR to issue full sync
1 parent 8ca9df6 commit 39d0bd8

8 files changed

Lines changed: 429 additions & 169 deletions

File tree

fedmsg.d/sync2jira.py

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,64 @@
6565
},
6666
'map': {
6767
'github': {
68-
'GITHUB_USERNAME/Demo_project': {'project': 'FACTORY', 'component': 'gitbz',
69-
'issue_updates': [
70-
'comments',
71-
'upstream_id',
72-
'title',
73-
'description',
74-
'github_markdown',
75-
'upstream_id',
76-
'url',
77-
{'transition': 'Closed'},
78-
{'assignee': {'overwrite': False}},
79-
'github_project_fields'],
80-
'github_project_number': '1',
81-
'github_project_fields': {'storypoints': {'gh_field': 'Estimate'},
82-
'priority': {'gh_field': 'Priority', 'options':
83-
{'P0': 'Blocker', 'P1': 'Critical', 'P2': 'Major',
84-
'P3': 'Minor', 'P4': 'Optional', 'P5': 'Trivial'}}},
85-
'sync': ['pullrequest', 'issue']},
68+
'GITHUB_USERNAME/Demo_project': {
69+
'project': 'FACTORY',
70+
'component': 'gitbz',
71+
72+
# ----- Issue synchronization -----
73+
'issue_updates': [
74+
'comments', # Sync GitHub issue comments to Jira
75+
'title', # Sync issue title
76+
'description', # Sync issue description/body
77+
'github_markdown', # Convert GitHub Markdown to Jira wiki format
78+
'upstream_id', # Add comment with upstream issue link on create
79+
'url', # Include upstream URL in description
80+
'github_project_fields', # Sync storypoints & priority from GitHub Projects
81+
{'fixVersion': {'overwrite': False}}, # Sync milestone as fixVersion
82+
{'transition': 'Closed'}, # Transition Jira when upstream issue closes
83+
{'assignee': {'overwrite': False}}, # Sync assignee (don't overwrite existing)
84+
{'on_close': {'apply_labels': ['closed-upstream']}}, # Label on close
85+
],
86+
87+
# ----- PR synchronization -----
88+
# pr_updates supports the same options as issue_updates,
89+
# plus merge_transition and link_transition.
90+
'pr_updates': [
91+
'comments', # Sync GitHub PR comments to Jira
92+
'title', # Sync PR title
93+
'description', # Sync PR description/body
94+
'github_markdown', # Convert GitHub Markdown to Jira wiki format
95+
'upstream_id', # Add comment with upstream PR link on create
96+
'url', # Include upstream URL in description
97+
'github_project_fields', # Sync storypoints & priority from GitHub Projects
98+
{'fixVersion': {'overwrite': False}}, # Sync milestone as fixVersion
99+
{'transition': 'Closed'}, # Transition Jira when upstream issue closes
100+
{'merge_transition': 'Closed'}, # Transition Jira when PR is merged
101+
{'link_transition': 'In Progress'}, # Transition Jira when PR is first linked
102+
{'assignee': {'overwrite': False}}, # Sync assignee (don't overwrite existing)
103+
{'on_close': {'apply_labels': ['closed-upstream']}}, # Label on close
104+
],
105+
106+
# ----- GitHub Projects (shared by issue & PR) -----
107+
'github_project_number': '1',
108+
'github_project_fields': {
109+
'storypoints': {'gh_field': 'Estimate'},
110+
'priority': {
111+
'gh_field': 'Priority',
112+
'options': {
113+
'P0': 'Blocker',
114+
'P1': 'Critical',
115+
'P2': 'Major',
116+
'P3': 'Minor',
117+
'P4': 'Optional',
118+
'P5': 'Trivial',
119+
},
120+
},
121+
},
122+
123+
# What to sync: 'issue', 'pullrequest', or both
124+
'sync': ['issue', 'pullrequest'],
125+
},
86126
},
87127
},
88128
'filters': {

sync2jira/downstream_issue.py

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ def _create_jira_issue(client, issue, config):
963963

964964
# Update relevant information (i.e., tags, assignees, etc.) if the User
965965
# opted in
966-
_update_jira_issue(downstream, issue, client, config)
966+
update_jira_issue(downstream, issue, client, config)
967967

968968
return downstream
969969

@@ -985,21 +985,25 @@ def _label_matching(jira_labels, issue_labels):
985985
return updated_labels
986986

987987

988-
def _update_jira_issue(existing, issue, client, config):
988+
def update_jira_issue(existing, issue, client, config, updates_key="issue_updates"):
989989
"""
990990
Updates an existing JIRA issue (i.e., tags, assignee, comments, etc.).
991991
992+
Works for both Issue and PR intermediary objects. The ``updates_key``
993+
parameter selects which downstream config list to read
994+
("issue_updates" or "pr_updates").
995+
992996
:param jira.resources.Issue existing: Existing JIRA issue that was found
993-
:param sync2jira.intermediary.Issue issue: Upstream issue we're pulling data from
997+
:param issue: Upstream Issue or PR we're pulling data from
994998
:param jira.client.JIRA client: JIRA Client
999+
:param dict config: Config dict
1000+
:param str updates_key: Config key for the updates list
9951001
:returns: Nothing
9961002
"""
997-
# Start with comments
998-
# Only synchronize comments for listings that op-in
999-
log.info("Updating information for upstream issue: %s", issue.url)
1003+
log.info("Updating information for upstream %s: %s", updates_key, issue.url)
10001004

10011005
# Get a list of what the user wants to update for the upstream issue
1002-
updates = issue.downstream.get("issue_updates", [])
1006+
updates = issue.downstream.get(updates_key, [])
10031007

10041008
# Update relevant data if needed.
10051009
# If the user has specified nothing, just return.
@@ -1008,7 +1012,7 @@ def _update_jira_issue(existing, issue, client, config):
10081012

10091013
# Get fields representing project item fields in GitHub and Jira
10101014
github_project_fields = issue.downstream.get("github_project_fields", {})
1011-
# Only synchronize comments for listings that op-in
1015+
# Only synchronize github_project_fields for listings that op-in
10121016
if "github_project_fields" in updates and github_project_fields:
10131017
log.info("Looking for GitHub project fields")
10141018
_update_github_project_fields(
@@ -1026,7 +1030,8 @@ def _update_jira_issue(existing, issue, client, config):
10261030
_update_tags(updates, existing, issue)
10271031

10281032
# Only synchronize fixVersion for listings that op-in
1029-
if issue.fixVersion and any("fixVersion" in item for item in updates):
1033+
fix_version = getattr(issue, "fixVersion", None)
1034+
if fix_version and any("fixVersion" in item for item in updates):
10301035
log.info("Looking for new fixVersions")
10311036
_update_fixVersion(updates, existing, issue, client)
10321037

@@ -1043,7 +1048,7 @@ def _update_jira_issue(existing, issue, client, config):
10431048
# Only synchronize descriptions for listings that op-in
10441049
if "description" in updates:
10451050
log.info("Looking for new description")
1046-
_update_description(existing, issue)
1051+
_update_description(existing, issue, updates_key)
10471052

10481053
# Only synchronize title for listings that op-in
10491054
if "title" in updates:
@@ -1055,44 +1060,45 @@ def _update_jira_issue(existing, issue, client, config):
10551060
# Only synchronize transition (status) for listings that op-in
10561061
if any("transition" in item for item in updates):
10571062
log.info("Looking for new transition(s)")
1058-
_update_transition(client, existing, issue)
1063+
_update_transition(client, existing, issue, updates_key)
10591064

1060-
# Only execute 'on_close' events for listings that opt-in
1061-
# and when the issue is closed.
1065+
# Execute 'on_close' events when the issue is closed
1066+
# (opt-in is checked inside _update_on_close).
10621067
if issue.status == "Closed":
10631068
log.info("Attempting to update downstream issue on upstream closed event")
10641069
_update_on_close(existing, updates)
10651070

10661071
log.info("Done updating %s!", issue.url)
10671072

10681073

1069-
def _update_transition(client, existing, issue):
1074+
def _update_transition(client, existing, issue, updates_key="issue_updates"):
10701075
"""
10711076
Helper function to update the transition of a downstream JIRA issue.
10721077
10731078
Supports an optional ``issue_types`` filter on transition entries in
1074-
``issue_updates``. When present, the transition only fires if the
1079+
the updates list. When present, the transition only fires if the
10751080
downstream JIRA issue's type is in the list.
10761081
10771082
:param jira.client.JIRA client: JIRA client
10781083
:param jira.resource.Issue existing: Existing JIRA issue
10791084
:param sync2jira.intermediary.Issue issue: Upstream issue
1085+
:param str updates_key: Config key for the updates list
10801086
:returns: Nothing
10811087
"""
1082-
for entry in issue.downstream.get("issue_updates", []):
1088+
for entry in issue.downstream.get(updates_key, []):
10831089
if not isinstance(entry, dict) or "transition" not in entry:
10841090
continue
10851091

10861092
closed_status = entry["transition"]
10871093

1088-
# Normalize legacy True value to "Closed"
10891094
if closed_status is True:
10901095
closed_status = "Closed"
10911096
if not isinstance(closed_status, str):
10921097
log.warning(
10931098
"Ignoring malformed transition value %r (expected a string) in "
1094-
"issue_updates config for %s",
1099+
"%s config for %s",
10951100
closed_status,
1101+
updates_key,
10961102
existing.key,
10971103
)
10981104
continue
@@ -1395,19 +1401,18 @@ def _update_tags(updates, existing, issue):
13951401
_update_jira_labels(existing, updated_labels)
13961402

13971403

1398-
def _build_description(issue):
1404+
def _build_description(issue, updates_key="issue_updates"):
13991405
# Build the description of the JIRA issue.
14001406
#
14011407
# Truncate issue.content *before* wrapping it with {quote} and the
14021408
# metadata prefix so the closing {quote} and decoration are never lost.
1403-
issue_updates = issue.downstream.get("issue_updates", [])
1409+
issue_updates = issue.downstream.get(updates_key, [])
14041410

14051411
# Build the prefix lines that will appear above the quoted content.
14061412
prefix_parts = []
1407-
if issue.reporter:
1408-
prefix_parts.append(
1409-
f"[{issue.id}] Upstream Reporter: {issue.reporter['fullname']}"
1410-
)
1413+
reporter = getattr(issue, "reporter", None)
1414+
if reporter:
1415+
prefix_parts.append(f"[{issue.id}] Upstream Reporter: {reporter}")
14111416
if issue.status and any("transition" in item for item in issue_updates):
14121417
prefix_parts.append("Upstream issue status: " + issue.status)
14131418

@@ -1433,16 +1438,17 @@ def _build_description(issue):
14331438
return description
14341439

14351440

1436-
def _update_description(existing, issue):
1441+
def _update_description(existing, issue, updates_key="issue_updates"):
14371442
"""
14381443
Helper function to sync description between upstream issue and downstream JIRA issue.
14391444
14401445
:param jira.resource.Issue existing: Existing JIRA issue
1441-
:param sync2jira.intermediary.Issue issue: Upstream issue
1446+
:param sync2jira.intermediary.Issue or sync2jira.intermediary.PR issue: Upstream Issue or PR
1447+
:param str updates_key: Config key for the updates list
14421448
:returns: Nothing
14431449
"""
14441450

1445-
new_description = _build_description(issue)
1451+
new_description = _build_description(issue, updates_key)
14461452

14471453
# Now we can update the JIRA issue if we need to
14481454
if new_description != existing.fields.description:
@@ -1574,19 +1580,24 @@ def convert_content(content: str) -> str:
15741580
return content
15751581

15761582

1583+
def maybe_convert_markdown(issue, updates_key="issue_updates"):
1584+
"""Apply GitHub-markdown-to-Jira conversion if the config opts in.
1585+
1586+
Works for both Issue and PR objects.
1587+
"""
1588+
updates = issue.downstream.get(updates_key)
1589+
if updates and issue.source == "github" and issue.content:
1590+
if "github_markdown" in updates:
1591+
issue.content = convert_content(issue.content)
1592+
1593+
15771594
def update_jira(client, config, issue):
15781595
# Check the status of the JIRA client
15791596
if not config["sync2jira"]["develop"] and not check_jira_status(client):
15801597
log.warning("The JIRA server looks like its down. Shutting down...")
15811598
raise RuntimeError("Jira server status check failed; aborting...")
15821599

1583-
if issue.downstream.get("issue_updates"):
1584-
if (
1585-
issue.source == "github"
1586-
and issue.content
1587-
and "github_markdown" in issue.downstream["issue_updates"]
1588-
):
1589-
issue.content = convert_content(issue.content)
1600+
maybe_convert_markdown(issue)
15901601

15911602
# First, check to see if we have a matching issue using the new method.
15921603
# If we do, then bail out. No sync needed.
@@ -1599,7 +1610,7 @@ def update_jira(client, config, issue):
15991610
log.info("Testing flag is true. Skipping actual update.")
16001611
return
16011612
# Update relevant metadata (i.e. tags, assignee, etc)
1602-
_update_jira_issue(existing, issue, client, config)
1613+
update_jira_issue(existing, issue, client, config)
16031614
return
16041615

16051616
# If we're *not* configured to do legacy matching (upgrade mode), then

0 commit comments

Comments
 (0)