Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Migrate Jira issues to Gitlab
import requests
from requests.auth import HTTPBasicAuth
import re
from StringIO import StringIO
import uuid
# Inspired from https://gist.github.com/toudi/67d775066334dc024c24
# Tested on Jira 7.4 and Gitlab 2.2 with Python 2.7
JIRA_URL = 'https://your-jira-url.tld/'
JIRA_ACCOUNT = ('jira-username', 'jira-password')
# the JIRA project ID (short)
JIRA_PROJECT = 'PRO'
# Jira Query
#JQL = 'key=PRO-1182'
JQL = 'project=%s+AND+(resolution=Unresolved+OR+Sprint+in+openSprints())+ORDER+BY+createdDate+ASC&maxResults=10000' % JIRA_PROJECT
GITLAB_URL = 'http://your-gitlab-url.tld/'
# this is needed for importing attachments. The script will login to gitlab under the hood.
GITLAB_ACCOUNT = ('gitlab-username', 'gitlab-password')
# this token will be used whenever the API is invoked and
# the script will be unable to match the jira's author of the comment / attachment / issue
# this identity will be used instead.
GITLAB_TOKEN = 'get-this-token-from-your-profile'
# the project in gitlab that you are importing issues to.
GITLAB_PROJECT = 'namespaced/project/name'
# the numeric project ID. If you don't know it, the script will search for it
# based on the project name.
GITLAB_PROJECT_ID = None
# set this to false if JIRA / Gitlab is using self-signed certificate.
VERIFY_SSL_CERTIFICATE = True
# Add a comment with the link to the Jira issue
ADD_A_LINK = True
# the Jira Epic custom field
JIRA_EPIC_FIELD = 'customfield_10540'
# the Jira Sprints custom field
JIRA_SPRINT_FIELD = 'customfield_10340'
# the Jira story points custom field
JIRA_STORY_POINTS_FIELD = 'customfield_10002'
# IMPORTANT !!!
# make sure that user (in gitlab) has access to the project you are trying to
# import into. Otherwise the API request will fail.
# jira user name as key, gitlab as value
# if you want dates and times to be correct, make sure every user is (temporarily) admin
GITLAB_USER_NAMES = {
'jira': 'gitlab',
}
# Convert Jira issue types to Gitlab labels
# Warning: If a Jira issue type isn't in the map, the issue will be skipped!
ISSUE_TYPES_MAP = {
'Bug': 'bug',
'Improvement': 'enhancement',
'Spike': 'spike',
'Story': 'story',
'Task': 'task'
}
# (Enterprise Edition) Convert Jira story points to Gitlab issue weight
STORY_POINTS_MAP = {
1.0: 1,
2.0: 2,
3.0: 3,
5.0: 4,
8.0: 5,
13.0: 6,
20.0: 7,
40.0: 8,
100.0: 9
}
# TODO: Do all replacements once
# Gitlab markdown : https://docs.gitlab.com/ee/user/markdown.html
# Jira text formatting notation : https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all
def multiple_replace(text, adict):
if text is None:
return ''
t = text
t = re.sub(r'(\r\n){1}', r' \1', t) # line breaks
t = re.sub(r'\{code:([a-z]+)\}\s*', r'\n```\1\n', t) # Block code
t = re.sub(r'\{code\}\s*', r'\n```\n', t) # Block code
t = re.sub(r'\n\s*bq\. (.*)\n', r'\n\> \1\n', t) # Block quote
t = re.sub(r'\{quote\}', r'\n\>\>\>\n', t) # Block quote #2
t = re.sub(r'\{color:[\#\w]+\}(.*)\{color\}', r'> **\1**', t) # Colors
t = re.sub(r'\n-{4,}\n', r'---', t) # Ruler
t = re.sub(r'\[~([a-z]+)\]', r'@\1', t) # Links to users
t = re.sub(r'\[([^|\]]*)\]', r'\1', t) # Links without alt
t = re.sub(r'\[(?:(.+)\|)([a-z]+://.+)\]', r'[\1](\2)', t) # Links with alt
t = re.sub(r'(\b%s-\d+\b)' % JIRA_PROJECT, r'[\1](%sbrowse/\1)' % JIRA_URL, t) # Links to other issues
# Lists
t = re.sub(r'\n *\# ', r'\n 1. ', t) # Ordered list
t = re.sub(r'\n *[\*\-\#]\# ', r'\n 1. ', t) # Ordered sub-list
t = re.sub(r'\n *[\*\-\#]{2}\# ', r'\n 1. ', t) # Ordered sub-sub-list
t = re.sub(r'\n *\* ', r'\n - ', t) # Unordered list
t = re.sub(r'\n *[\*\-\#][\*\-] ', r'\n - ', t) # Unordered sub-list
t = re.sub(r'\n *[\*\-\#]{2}[\*\-] ', r'\n - ', t) # Unordered sub-sub-list
# Text effects
t = re.sub(r'(^|[\W])\*(\S.*\S)\*([\W]|$)', r'\1**\2**\3', t) # Bold
t = re.sub(r'(^|[\W])_(\S.*\S)_([\W]|$)', r'\1*\2*\3', t) # Emphasis
t = re.sub(r'(^|[\W])-(\S.*\S)-([\W]|$)', r'\1~~\2~~\3', t) # Deleted / Strikethrough
t = re.sub(r'(^|[\W])\+(\S.*\S)\+([\W]|$)', r'\1__\2__\3', t) # Underline
t = re.sub(r'(^|[\W])\{\{(.*)\}\}([\W]|$)', r'\1`\2`\3', t) # Inline code
# Titles
t = re.sub(r'\n?\bh1\. ', r'\n# ', t)
t = re.sub(r'\n?\bh2\. ', r'\n## ', t)
t = re.sub(r'\n?\bh3\. ', r'\n### ', t)
t = re.sub(r'\n?\bh4\. ', r'\n#### ', t)
t = re.sub(r'\n?\bh5\. ', r'\n##### ', t)
t = re.sub(r'\n?\bh6\. ', r'\n###### ', t)
# Emojis : https://emoji.codes
t = re.sub(r':\)', r':smiley:', t)
t = re.sub(r':\(', r':disappointed:', t)
t = re.sub(r':P', r':yum:', t)
t = re.sub(r':D', r':grin:', t)
t = re.sub(r';\)', r':wink:', t)
t = re.sub(r'\(y\)', r':thumbsup:', t)
t = re.sub(r'\(n\)', r':thumbsdown:', t)
t = re.sub(r'\(i\)', r':information_source:', t)
t = re.sub(r'\(/\)', r':white_check_mark:', t)
t = re.sub(r'\(x\)', r':x:', t)
t = re.sub(r'\(!\)', r':warning:', t)
t = re.sub(r'\(\+\)', r':heavy_plus_sign:', t)
t = re.sub(r'\(-\)', r':heavy_minus_sign:', t)
t = re.sub(r'\(\?\)', r':grey_question:', t)
t = re.sub(r'\(on\)', r':bulb:', t)
#t = re.sub(r'\(off\)', r'::', t) # Not found
t = re.sub(r'\(\*[rgby]?\)', r':star:', t)
for k, v in adict.iteritems():
t = re.sub(k, v, t)
return t
# We use UUID in place of the filename to prevent 500 errors on unicode chars
def move_attachements(attachments):
replacements = {}
if len(attachments):
for attachment in attachments:
author = attachment['author']['name']
_file = requests.get(
attachment['content'],
auth=HTTPBasicAuth(*JIRA_ACCOUNT),
verify=VERIFY_SSL_CERTIFICATE,
)
_content = StringIO(_file.content)
file_info = requests.post(
GITLAB_URL + 'api/v4/projects/%s/uploads' % GITLAB_PROJECT_ID,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN,'SUDO': resolve_login(author)},
files={
'file': (
str(uuid.uuid4()),
_content
)
},
verify=VERIFY_SSL_CERTIFICATE
).json()
del _content
# now we got the upload URL. Let's post the comment with an
# attachment
if file_info.has_key('url'):
key = "!%s[^!]*!" % attachment['filename']
value = "![%s](%s)" % (attachment['filename'], file_info['url'])
replacements[key] = value
return replacements
def get_milestone_id(string):
for milestone in gl_milestones:
if milestone['title'] == string:
return milestone['id']
# Milestone doesn't yet exist, so we create it
milestone = requests.post(
GITLAB_URL + 'api/v4/projects/%s/milestones' % GITLAB_PROJECT_ID,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE,
data={
'title': string
}
).json()
gl_milestones.append(milestone)
return milestone['id']
# Get the user name from the GITLAB_USER_NAMES dict
# Or if logins match between Jira and Gitlab, use it
# In other cases (eg. inactive Jira user not created in Gitlab) we use GITLAB_ACCOUNT
def resolve_login(jira_user):
if GITLAB_USER_NAMES.has_key(jira_user):
return GITLAB_USER_NAMES[jira_user]
for user in gl_users:
if user['username'] == jira_user:
return user['username']
return GITLAB_ACCOUNT[0]
if not GITLAB_PROJECT_ID:
# find out the ID of the project.
for project in requests.get(
GITLAB_URL + 'api/v4/projects',
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE
).json():
if project['path_with_namespace'] == GITLAB_PROJECT:
GITLAB_PROJECT_ID = project['id']
break
if not GITLAB_PROJECT_ID:
raise Exception("Unable to find %s in gitlab!" % GITLAB_PROJECT)
gl_milestones = requests.get(
GITLAB_URL + 'api/v4/projects/%s/milestones' % GITLAB_PROJECT_ID,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE
).json()
gl_users = requests.get(
GITLAB_URL + 'api/v4/users',
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE
).json()
# Jira API documentation : https://developer.atlassian.com/static/rest/jira/6.1.html
jira_issues = requests.get(
JIRA_URL + 'rest/api/2/search?jql=' + JQL,
auth=HTTPBasicAuth(*JIRA_ACCOUNT),
verify=VERIFY_SSL_CERTIFICATE,
headers={'Content-Type': 'application/json'}
).json()['issues']
for issue in jira_issues:
if issue['fields']['issuetype']['name'] not in ISSUE_TYPES_MAP:
continue
gl_assignee = ''
if issue['fields']['assignee']:
for user in gl_users:
if user['username'] == issue['fields']['assignee']['name']:
gl_assignee = user['id']
break
labels = [ISSUE_TYPES_MAP[issue['fields']['issuetype']['name']]]
if issue['fields']['status']['statusCategory']['name'] == "In Progress":
labels.append(issue['fields']['status']['name'])
# Add Epic name to labels
if issue['fields'][JIRA_EPIC_FIELD]:
epic_info = requests.get(
JIRA_URL + 'rest/api/2/issue/%s/?fields=summary' % issue['fields'][JIRA_EPIC_FIELD],
auth=HTTPBasicAuth(*JIRA_ACCOUNT),
verify=VERIFY_SSL_CERTIFICATE,
headers={'Content-Type': 'application/json'}
).json()
labels.append(epic_info['fields']['summary'])
# Use the name of the last sprint as milestone
milestone_id = None
if issue['fields'][JIRA_SPRINT_FIELD]:
for sprint in issue['fields'][JIRA_SPRINT_FIELD]:
m = re.search(r'name=([^,]+),', sprint)
if m:
name = m.group(1)
if name:
milestone_id = get_milestone_id(m.group(1))
# Gitlab expect the timezone in +00:00 format without milliseconds while Jira gives +0000 with milliseconds
reporter = issue['fields']['reporter']['name']
# get comments and attachments from Jira
issue_info = requests.get(
JIRA_URL + 'rest/api/2/issue/%s/?fields=attachment,comment' % issue['id'],
auth=HTTPBasicAuth(*JIRA_ACCOUNT),
verify=VERIFY_SSL_CERTIFICATE,
headers={'Content-Type': 'application/json'}
).json()
replacements = move_attachements(issue_info['fields']['attachment'])
data = {
'assignee_ids': [gl_assignee],
'title': issue['fields']['summary'],
'description': multiple_replace(issue['fields']['description'], replacements),
'milestone_id': milestone_id,
'labels': ", ".join(labels),
}
# Issue weight
if JIRA_STORY_POINTS_FIELD in issue['fields'] and issue['fields'][JIRA_STORY_POINTS_FIELD]:
data['weight'] = STORY_POINTS_MAP[issue['fields'][JIRA_STORY_POINTS_FIELD]]
gl_issue = requests.post(
GITLAB_URL + 'api/v4/projects/%s/issues' % GITLAB_PROJECT_ID,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN,'SUDO': resolve_login(reporter)},
verify=VERIFY_SSL_CERTIFICATE,
data=data
).json()
#Add a comment with the link to the Jira issue
if ADD_A_LINK:
body = "Imported from Jira issue [%(k)s](%(u)sbrowse/%(k)s)" % {'k': issue['key'], 'u': JIRA_URL}
note_add = requests.post(
GITLAB_URL + 'api/v4/projects/%s/issues/%s/notes' % (GITLAB_PROJECT_ID, gl_issue['iid']),
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE,
data={'body': body}
)
for comment in issue_info['fields']['comment']['comments']:
author = comment['author']['name']
note_add = requests.post(
GITLAB_URL + 'api/v4/projects/%s/issues/%s/notes' % (GITLAB_PROJECT_ID, gl_issue['iid']),
headers={'PRIVATE-TOKEN': GITLAB_TOKEN,'SUDO': resolve_login(author)},
verify=VERIFY_SSL_CERTIFICATE,
data={'body': multiple_replace(comment['body'], replacements)}
)
if issue['fields']['status']['statusCategory']['key'] == "done":
requests.put(
GITLAB_URL + 'api/v4/projects/%s/issues/%s' % (GITLAB_PROJECT_ID, gl_issue['iid']),
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE,
data={'state_event': 'close'}
)
@Podolyan98
Copy link

Podolyan98 commented Oct 25, 2020

It works?

I create a issue on GitLab like this(Python 3):

user_id = int(11)

gl_issue = requests.post(
    GITLAB_URL + '/api/v4/projects/%s/issues' % GITLAB_PROJECT_ID,
    headers={'PRIVATE-TOKEN': GITLAB_TOKEN,'SUDO': GITLAB_USER_NAMES.get(reporter, reporter)},
    verify=VERIFY_SSL_CERTIFICATE,
    data={
        'title': issue['fields']['summary'],
        'description': issue['fields']['description'],
        'created_at': issue['fields']['created'],
        'assignee_ids' : [user_id],
    })

Issue created but the assignee is not set. What am I doing wrong?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment