-
-
Save juergenpointinger/c9f40b5a02e9b376a1fbc67b77c3d87a to your computer and use it in GitHub Desktop.
# 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' | |
}) |
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.
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/.