Skip to content

Instantly share code, notes, and snippets.

@juergenpointinger
Created May 14, 2020 21:01
Show Gist options
  • Save juergenpointinger/c9f40b5a02e9b376a1fbc67b77c3d87a to your computer and use it in GitHub Desktop.
Save juergenpointinger/c9f40b5a02e9b376a1fbc67b77c3d87a to your computer and use it in GitHub Desktop.
Import Jira to GitLab
# import_jira_gitlab.py
import requests
from requests.auth import HTTPBasicAuth
import re
import uuid
import json
##############################################################################
## General
# *False* if Jira / GitLab is using self-signed certificates, otherwhise *True*
VERIFY_SSL_CERTIFICATE = True
##############################################################################
## Jira specifics
# Jira URL
JIRA_URL = 'https://your-jira-instance.com/rest/api/latest'
# Jira user credentials (incl. API token)
JIRA_ACCOUNT = ('your-jira-username', 'your-jira-api-token')
# Jira project ID (short)
JIRA_PROJECT = 'PRJ'
# Jira Query (JQL)
JQL = 'project=%s+AND+issueType=Epic+AND+resolution=Unresolved+ORDER+BY+createdDate+ASC&maxResults=100' % JIRA_PROJECT
# Jira Epic custom field
JIRA_EPIC_FIELD = 'customfield_10005'
# Jira Sprints custom field
JIRA_SPRINT_FIELD = 'customfield_10010'
# Jira story points custom field
JIRA_STORY_POINTS_FIELD = 'customfield_10014'
##############################################################################
## GitLab specifics
# GitLab URL
GITLAB_URL = 'https://gitlab.com/api/v4'
# GitLab token will be used whenever the API is invoked
GITLAB_TOKEN = 'your-private-gitlab-token'
# GitLab group
GITLAB_GROUP = 'your-group-name'
# GitLab group id
GITLAB_GROUP_ID = 'your-group-id'
# Gitlab project with group/namespace
GITLAB_PROJECT = 'your-group-name/your-project-name'
# GitLab project id
GITLAB_PROJECT_ID = 'your-project-id'
##############################################################################
## Import configuration
# Add a comment with the link to the Jira issue
ADD_A_LINK = True
# Add an Epic to the GitLab issue
ADD_EPIC = True
# Add a milestone/sprint to the GitLab issue
ADD_SPRINT = True
# Jira username as key, GitLab as value
# Note: If you want dates and times to be correct, make sure every user is (temporarily) owner
USERNAMES_MAP = {
'your-jira-user-1': 'your-gitlab-user1',
'your-jira-user-2': 'your-gitlab-user2'
}
# Convert Jira issue types to Gitlab labels
# Note: If a Jira issue type isn't in the map, the issue will be skipped
ISSUE_TYPES_MAP = {
'Bug': 'Bug::high',
'Improvement': 'Enhancement',
'Spike': 'Spike',
'Story': 'Story',
'Task': 'Task',
'Sub-task': 'Sub-task'
}
# Convert Jira story points to Gitlab issue weight
STORY_POINTS_MAP = {
1.0: 1,
2.0: 2,
3.0: 3,
5.0: 5,
8.0: 8,
13.0: 13,
21.0: 21,
}
##############################################################################
# GET request
def gl_get_request(endpoint):
response = requests.get(
GITLAB_URL + endpoint,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE
)
if response.status_code != 200:
raise Exception("Unable to read data from %s!" % GITLAB_PROJECT_ID)
return response.json()
# POST request
def gl_post_request(endpoint, data):
response = requests.get(
GITLAB_URL + endpoint,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE,
data=data
)
if response.status_code != 200:
raise Exception("Unable to write data from %s!" % GITLAB_PROJECT_ID)
return response.json()
# POST request
def gl_put_request(endpoint, data):
response = requests.put(
GITLAB_URL + endpoint,
headers={'PRIVATE-TOKEN': GITLAB_TOKEN},
verify=VERIFY_SSL_CERTIFICATE,
data=data
)
if response.status_code != 200:
raise Exception("Unable to change data from %s!" % GITLAB_PROJECT_ID)
return response.json()
# GET request
def jira_get_request(endpoint):
response = requests.get(
JIRA_URL + endpoint,
auth=HTTPBasicAuth(*JIRA_ACCOUNT),
verify=VERIFY_SSL_CERTIFICATE,
headers={'Content-Type': 'application/json'}
)
if response.status_code != 200:
raise Exception("Unable to read data from %s!" % JIRA_PROJECT)
return response.json()
##############################################################################
## Early exit scenario
if not GITLAB_PROJECT_ID:
# find out the ID of the project.
for project in gl_get_request('/projects'):
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)
##############################################################################
# Get milestone or create one
def get_milestone_id(title):
for milestone in gl_milestones:
if milestone['title'] == title:
return milestone['id']
# Milestone doesn't yet exist, so we create it
# Note: Group Milestone MUST not exist
milestone = gl_post_request('/projects/%s/milestones' % GITLAB_PROJECT_ID, { 'title': title })
gl_milestones.append(milestone)
return milestone['id']
##############################################################################
# Get all milestones
gl_milestones = gl_get_request('/projects/%s/milestones' % GITLAB_PROJECT_ID)
# Get all GitLab members
gl_members = gl_get_request('/groups/%s/members' % GITLAB_GROUP_ID)
# Get Jira issues
jira_issues = jira_get_request('/search?jql=' + JQL)
for issue in jira_issues['issues']:
jira_key = issue['key']
jira_issue_type = issue['fields']['issuetype']['name']
gl_issue_type = ''
if jira_issue_type not in ISSUE_TYPES_MAP:
print("Unknown issue type detected. Jira issue %s skipped" % jira_key)
continue
else:
gl_issue_type = ISSUE_TYPES_MAP[jira_issue_type]
##################
print("Start import of Jira issue %s" % jira_key)
jira_title = issue['fields']['summary']
jira_description = issue['fields']['description']
jira_issue_status = issue['fields']['status']['statusCategory']['name']
jira_reporter = issue['fields']['reporter']['displayName']
# Get Jira assignee
jira_assignee = ''
if issue['fields']['assignee'] is not None and issue['fields']['assignee']['displayName'] is not None:
jira_assignee = issue['fields']['assignee']['displayName']
# Get GitLab assignee
gl_assignee_id = 0
for member in gl_members:
if member['name'] == jira_assignee:
gl_assignee_id = member['id']
break
# Add GitLab issue type as label
gl_labels = []
gl_labels.append(gl_issue_type)
# Add "In Progress" to labels
if jira_issue_status == "In Progress":
gl_labels.append(jira_issue_type)
# Add Epic name to labels
if ADD_EPIC and JIRA_EPIC_FIELD in issue['fields'] and issue['fields'][JIRA_EPIC_FIELD] is not None:
epic_info = jira_get_request('/issue/%s/?fields=summary' % issue['fields'][JIRA_EPIC_FIELD])
gl_labels.append(epic_info['fields']['summary'])
# Add Jira ticket to labels
gl_labels.append('jira-import::' + jira_key)
# Use the name of the last Jira sprint as GitLab milestone
gl_milestone = None
if ADD_SPRINT and JIRA_SPRINT_FIELD in issue['fields'] and issue['fields'][JIRA_SPRINT_FIELD] is not None:
for sprint in issue['fields'][JIRA_SPRINT_FIELD]:
match = re.search(r'name=([^,]+),', sprint)
if match:
name = match.group(1)
if name:
gl_milestone = get_milestone_id(match.group(1))
# Get Jira attachments and comments
jira_info = jira_get_request('/issue/%s/?fields=attachment,comment' % issue['id'])
# Issue weight
gl_weight = 0
if JIRA_STORY_POINTS_FIELD in issue['fields'] and issue['fields'][JIRA_STORY_POINTS_FIELD]:
gl_weight = STORY_POINTS_MAP[issue['fields'][JIRA_STORY_POINTS_FIELD]]
# Create GitLab issue
gl_issue = gl_post_request('/projects/%s/issues' % GITLAB_PROJECT_ID, {
'assignee_ids': [gl_assignee_id],
'title': jira_title,
'description': jira_description,
'milestone_id': gl_milestone,
'labels': ", ".join(gl_labels),
'weight': gl_weight
})
# Add a comment with the link to the Jira issue
if ADD_A_LINK:
gl_post_request('/projects/%s/issues/%s/notes' % (GITLAB_PROJECT_ID, gl_issue['iid']), {
'body': "Imported from Jira issue [%(k)s](%(u)sbrowse/%(k)s)" % {'k': jira_key, 'u': JIRA_URL}
})
# Change GitLab issue status
if jira_issue_status == "Done":
gl_put_request('api/v4/projects/%s/issues/%s' % (GITLAB_PROJECT_ID, gl_issue['iid']), {
'state_event': 'close'
})
@Cali0707
Copy link

Cali0707 commented Jul 16, 2020

Hi @juergenpointinger, what is the status of this gist?

It seems to have a bunch of things that we would like to re-use, as well as some parts that are unfinished, eg the data from https://gist.github.com/juergenpointinger/c9f40b5a02e9b376a1fbc67b77c3d87a#file-import_jira_gitlab-py-L240 doesn't seem to be pushed to gitlab. Do you have a more recent version you could share? We'd be happy to share back any fixes or enhancements we might make.

FWIW, I'm a new intern at https://github.com/JMAConsulting/.

@juergenpointinger
Copy link
Author

Hey @Cali0707, thanks for your interest. I have successfully used this for a project, but, as you correctly mentioned, without the attachments. That was a bit tricky and time consuming, so I decided to leave it out. Just like the comments. I found it more important to have a reference to the original Jira ticket. The follow-up communication took place in GitLab. Please also note the epics that are not included here. You can find an example of this import here

Furthermore I have decided not to import closed tickets. As you can see in the code, the first 100 Jira tickets (maxResults) are used. This JQL may have to be adapted to your needs.

It is a good suggestion to do the import step-by-step anyway. If you have any further questions, I am at your disposal.

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