-
-
Save davidklaw/a6a744044f83ea600b360d56946fd1bb to your computer and use it in GitHub Desktop.
JIRA to ZenHub Migration Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
1. Create a new repo and setup your pipeline to match JIRA | |
2. Edit the constants to suit your needs | |
3. Run `source jira.env` | |
4. Run `python jira-migration.py` | |
""" | |
import os | |
import requests | |
from jira import JIRA # pip install jira | |
from github import Github # pip install PyGithub | |
COMPONENT_LABEL_COLOR = "" # 6-digit hex color, e.g. 70B0A3 | |
JIRA_PROJECT_KEY = "" # the prefix to your issues, e.g. KEY-1313 | |
GITHUB_REPO = "" # org/repo | |
JIRA_URL = "" # https://YOURDOMAIN.atlassian.net | |
PEOPLE_MAP = {} # JIRA username to Github username mapping, e.g. { "jirausername": "githubusername" } | |
ZENHUB_BASE_URL = "https://api.zenhub.io/p1/repositories/" | |
COMPONENTS = set() | |
ZENHUB_HEADERS = { | |
"X-Authentication-Token": os.environ["ZENHUB_TOKEN"] | |
} | |
def get_github_repo(): | |
g = Github(os.environ["GITHUB_TOKEN"]) | |
return g.get_repo(GITHUB_REPO) | |
def get_zenhub_board(repo_id): | |
return requests.get( | |
"https://api.zenhub.io/p1/repositories/{}/board".format(repo_id), | |
headers=ZENHUB_HEADERS | |
).json() | |
def get_zenhub_pipelines(repo_id): | |
board = get_zenhub_board(repo_id) | |
return { | |
p["name"]: p["id"] | |
for p in board["pipelines"] | |
} | |
def get_jira(): | |
return JIRA(JIRA_URL, basic_auth=( | |
os.environ["JIRA_USER"], | |
os.environ["JIRA_PASS"] | |
)) | |
def get_epics(jira): | |
query = "project={} and type=\"Epic\"".format(JIRA_PROJECT_KEY) | |
return { | |
i.key: {"epic_issue": None, "issues": []} | |
for i in jira.search_issues(query, maxResults=800) | |
} | |
def get_issues(jira): | |
query = """ | |
project={} | |
and status != "Closed" | |
and status != "Done" | |
and status != "Accepted" | |
and status != "Resolved" | |
""".format(JIRA_PROJECT_KEY) | |
return jira.search_issues(query, maxResults=800, json_result=True) | |
def zh_move_issue_to_pipeline(repo_id, issue_number, pipeline_id): | |
url = "{}{}/issues/{}/moves".format(ZENHUB_BASE_URL, repo_id, issue_number) | |
if pipeline_id: | |
requests.post( | |
url, | |
data={"pipeline_id": pipeline_id, "position": "top"}, | |
headers=ZENHUB_HEADERS | |
) | |
def zh_set_estimate(repo_id, issue_number, estimate): | |
url = "{}{}/issues/{}/estimate".format(ZENHUB_BASE_URL, repo_id, issue_number) # noqa | |
if estimate: | |
requests.put(url, data={"estimate": estimate}, headers=ZENHUB_HEADERS) | |
def zh_convert_to_epic(repo_id, issue_number): | |
url = "{}{}/issues/{}/convert_to_epic".format(ZENHUB_BASE_URL, repo_id, issue_number) # noqa | |
requests.post(url, headers=ZENHUB_HEADERS) | |
def zh_set_epics(repo_id, epics): | |
for epic in epics.keys(): | |
url = "{}{}/epics/{}/update_issues".format(ZENHUB_BASE_URL, repo_id, epics[epic]["epic_issue"].number) # noqa | |
response = requests.post(url, json={ | |
"add_issues": [ | |
{"repo_id": repo_id, "issue_number": issue.number} | |
for issue in epics[epic]["issues"] | |
] | |
}, headers=ZENHUB_HEADERS) | |
print(response.status_code, response.json()) | |
def gh_set_labels_color(repo): | |
""" | |
The only labels so far in the fresh repo are ones created via components. | |
Get all labels. Set them all to COMPONENT_LABEL_COLOR. | |
""" | |
for label in COMPONENTS: | |
l = repo.get_label(label) | |
l.edit(l.name, COMPONENT_LABEL_COLOR) | |
def migrate_issue(jira_issue, repo, pipelines): | |
jira_id = jira_issue["key"] | |
pipeline = jira_issue["fields"]["status"]["name"] | |
pipeline_id = pipelines.get(pipeline) | |
title = jira_issue["fields"]["summary"] | |
if jira_issue["fields"]["description"]: | |
description = jira_issue["fields"]["description"].encode("utf-8") | |
else: | |
description = "" | |
body = "{}\n\n[{}]({}/browse/{})".format( | |
description, | |
jira_id, | |
JIRA_URL, | |
jira_id | |
) | |
estimate = jira_issue["fields"].get("customfield_10021") | |
jira_assignee = jira_issue["fields"].get("assignee") | |
if jira_assignee is not None: | |
assigned = PEOPLE_MAP.get(jira_assignee.get("key")) | |
else: | |
assigned = None | |
labels = [ | |
component["name"] | |
for component in | |
jira_issue["fields"].get("components", []) | |
] | |
COMPONENTS.update(labels) | |
# 1. Create the Issue | |
i = repo.create_issue( | |
title=title, | |
body=body, | |
assignees=[assigned] if assigned is not None else [], | |
labels=labels | |
) | |
# 2. Move to right Pipeline | |
zh_move_issue_to_pipeline(repo.id, i.number, pipeline_id) | |
# 3. Set any existing estimate | |
zh_set_estimate(repo.id, i.number, estimate) # noqa | |
return i | |
def migrate_issues(): | |
repo = get_github_repo() | |
pipelines = get_zenhub_pipelines(repo.id) | |
jira = get_jira() | |
epics = get_epics(jira) | |
issues = get_issues(jira)["issues"] | |
for issue in issues: | |
gh_issue = migrate_issue(issue, repo, pipelines) | |
epic_id = issue["fields"].get("customfield_10017") | |
jira_id = issue["key"] | |
# 4a. Collect Epic information | |
if epics.get(jira_id): | |
epics[jira_id]["epic_issue"] = gh_issue | |
zh_convert_to_epic(repo.id, gh_issue.number) | |
if epic_id and epics.get(epic_id): | |
epics[epic_id]["issues"].append(gh_issue) | |
print("Created Issue #{} - {}".format(gh_issue.number, jira_id)) | |
# 4b. Set Epics | |
zh_set_epics(repo.id, epics) | |
# Set label color | |
gh_set_labels_color(repo) | |
if __name__ == "__main__": | |
migrate_issues() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export ZENHUB_TOKEN= | |
export GITHUB_TOKEN= | |
export JIRA_USER= | |
export JIRA_PASS= |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment