Skip to content

Instantly share code, notes, and snippets.

@mburgs
Last active August 31, 2024 19:48
Show Gist options
  • Select an option

  • Save mburgs/16a232d189b26b2afb124c417a208adb to your computer and use it in GitHub Desktop.

Select an option

Save mburgs/16a232d189b26b2afb124c417a208adb to your computer and use it in GitHub Desktop.
Migrate cards from Trello to Github issues

Trello -> Github Issue Migration

  • create API token and paste into file
  • update repo owner and repo name values
  • download JSON export of trello board
  • move export to same directory as script, name it trello.json
  • run script - it will create issues from each card. Fields supported:
    • card name -> issue title
    • description
    • labels (only name, not colors)
    • state
  • features:
    • avoids creating duplicate issues in case it needs to be re-run
    • backs off if a rate-limit is hit and retries until successful
import csv
import json
import requests
import time
import random
class ExponentialBackoff:
def __init__(self, base=1, max_wait=60, factor=2, jitter=True):
"""
Initializes the exponential backoff class.
:param base: Base wait time in seconds.
:param max_wait: Maximum wait time in seconds.
:param factor: Factor by which the wait time is multiplied in each iteration.
:param jitter: If True, adds random jitter to the wait times.
"""
self.base = base
self.max_wait = max_wait
self.factor = factor
self.jitter = jitter
self.attempts = 0
def wait(self):
"""
Calculate the next wait time and sleep for that duration.
"""
wait_time = min(self.base * (self.factor ** self.attempts), self.max_wait)
if self.jitter:
wait_time = random.uniform(self.base, wait_time)
time.sleep(wait_time)
self.attempts += 1
def reset(self):
"""
Reset the number of attempts.
"""
self.attempts = 0
# Replace these with your own values
API_TOKEN = "<ADD APIT TOKEN HERE WITH ACCESS TO WRITE ISSUES>"
repo_owner = "<REPO_OWNER>"
repo_name = "<REPO_NAME>"
API_URL = f'https://api.github.com/repos/{repo_owner}/{repo_name}/issues'
AUTH_HEADERS = {
'Authorization': f'token {API_TOKEN}',
'Accept': 'application/vnd.github.v3+json'
}
def make_request(data, method="get", url_suffix=""):
if method == "get":
kwargs = {
"params": data
}
else:
kwargs = {"data": json.dumps(data)}
while True:
response = getattr(requests, method)(API_URL + url_suffix, headers=AUTH_HEADERS, **kwargs)
if 200 <= response.status_code < 300:
backoff.reset()
return response.json()
elif "rate limit" in response.content.decode():
# github has headers for the main rate limits but also has "secondary" rate limits that can only
# be detected by checking the body of the response - this is a quick n dirty way to check for either
print(f"\nRate limit hit - waiting a couple secs")
backoff.wait()
else:
raise Exception(f"Unknown error trying to make request: code: {response.status_code} \n\n HEADERS:\n{response.headers}\n\nCONTENT\n{response.content}")
def load_existing_issues():
page = 1
issues = []
MAX_PER_DAY = 100
while True:
fresh_issues = make_request({"state": "all", "page": page, "per_page": MAX_PER_DAY})
issues += fresh_issues
if len(fresh_issues) < MAX_PER_DAY:
# less than a full page means this is the last page
# the per_page param doesn't appear to work correclty
# so limited to 30 per page
break
page += 1
return {i["title"] for i in issues}
print("Initializing")
EXISTING_ISSUES = load_existing_issues()
for l in data["lists"]:
if l["name"] == "Done":
done_list_id = l["id"]
break
else:
raise Exception("Can't find Done ID")
to_create = [card for card in data["cards"] if card["name"] not in EXISTING_ISSUES]
backoff = ExponentialBackoff()
while to_create:
card = to_create.pop()
issue_data = {
'title': card["name"],
'body': card["desc"],
"labels": [l["name"] for l in card["labels"]],
}
response_data = make_request(issue_data, method="post")
print('Successfully created Issue:', response_data["title"])
if card["idList"] == done_list_id:
# issue is closed but imort for reference - needs to be done in an update
response = make_request({"state": "closed"}, method="patch", url_suffix=f"/{response_data['number']}")
print('Successfully closed Issue:', response['title'])
print("Done!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment