- 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
Last active
August 31, 2024 19:48
-
-
Save mburgs/16a232d189b26b2afb124c417a208adb to your computer and use it in GitHub Desktop.
Migrate cards from Trello to Github issues
This file contains hidden or 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
| 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