From ffcf88cb1695894651b6315e0a0062ec0b8b0ba8 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 17 May 2024 11:21:27 -0400 Subject: [PATCH] Clean out old Trello code --- python_modules/ewconfig/secret_manager.py | 24 -- python_modules/ewconfig/trello/__init__.py | 8 - python_modules/ewconfig/trello/boards.py | 20 -- python_modules/ewconfig/trello/cards.py | 80 ------- scripts/github-to-trello | 241 +++++++++------------ 5 files changed, 100 insertions(+), 273 deletions(-) delete mode 100644 python_modules/ewconfig/secret_manager.py delete mode 100644 python_modules/ewconfig/trello/__init__.py delete mode 100644 python_modules/ewconfig/trello/boards.py delete mode 100644 python_modules/ewconfig/trello/cards.py diff --git a/python_modules/ewconfig/secret_manager.py b/python_modules/ewconfig/secret_manager.py deleted file mode 100644 index e76ae72..0000000 --- a/python_modules/ewconfig/secret_manager.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging -from pathlib import Path -from typing import Optional - -SEMI_SECRET_BASE_PATH = Path("~/.config/ewconfig/secrets/semi-secret").expanduser() - -logger = logging.getLogger(__name__) - -def get_semi_secret_string(name: str, namespace: Optional[str] = None) -> str: - logger.debug(f"Attempting to load secret: {name} (ns: {namespace})") - - # Construct file path - file = SEMI_SECRET_BASE_PATH - if namespace: - file = file / namespace - file = file / name - - # Make sure it exists - if not file.exists(): - raise FileNotFoundError(f"Could not load secret from: {file}") - - # Read the value - with open(file, "r") as f: - return f.read().strip() diff --git a/python_modules/ewconfig/trello/__init__.py b/python_modules/ewconfig/trello/__init__.py deleted file mode 100644 index f22a8a7..0000000 --- a/python_modules/ewconfig/trello/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from ..secret_manager import get_semi_secret_string - -TRELLO_API_KEY = "fba640a85f15c91e93e6b3f88e59489c" -"""Public api key to do things to personal Trello""" - - -def get_trello_api_token() -> str: - return get_semi_secret_string("trello_api_token") diff --git a/python_modules/ewconfig/trello/boards.py b/python_modules/ewconfig/trello/boards.py deleted file mode 100644 index 1c991fd..0000000 --- a/python_modules/ewconfig/trello/boards.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from typing import Dict - - -@dataclass -class TrelloBoardInfo: - id: str - lists: Dict[str, str] - tags: Dict[str, str] - - -PERSONAL_TASKS_BOARD = TrelloBoardInfo( - id="tw3Cn3L6", - lists={"To Do": "6348a3ce5208f505b61d29bf"}, - tags={ - "GURU": "64e03ac77d27032282436d28", - "Github: Issue": "64eb5d72fb694cd8f0ba7a8d", - "Github: Pull Request": "652d4b775f5c59a8e6308216", - }, -) diff --git a/python_modules/ewconfig/trello/cards.py b/python_modules/ewconfig/trello/cards.py deleted file mode 100644 index d51e303..0000000 --- a/python_modules/ewconfig/trello/cards.py +++ /dev/null @@ -1,80 +0,0 @@ -import requests -import logging -from typing import Any, Dict, List, Optional - -logger = logging.getLogger(__name__) - - -def get_all_trello_cards( - board_id: str, api_key: str, api_token: str -) -> List[Dict[str, Any]]: - # Get a list of cards on the board - logger.debug(f"Getting all cards on board: {board_id}") - response = requests.get( - f"https://api.trello.com/1/boards/{board_id}/cards", - params={ - "key": api_key, - "token": api_token, - }, - ) - response.raise_for_status() - cards = response.json() - logger.debug(f"Found {len(cards)} cards on board: {board_id}") - return cards - - -def create_card( - list_id: str, - name: str, - api_key: str, - api_token: str, - description: Optional[str] = None, - label_ids: Optional[List[str]] = None, - position: str = "top", -) -> str: - logger.debug(f"Creating card: {name}") - - # Build out params - params = { - "idList": list_id, - "name": name, - "key": api_key, - "token": api_token, - "pos": position, - } - if description: - params["desc"] = description - if label_ids: - params["idLabels"] = ",".join(label_ids) - - # Make a new card - response = requests.post( - "https://api.trello.com/1/cards", - params=params, - ) - response.raise_for_status() - - # Get the new card's id - card_id = response.json()["id"] - - logger.debug(f"Created card: {card_id}") - return card_id - - -def add_attachment( - card_id: str, api_key: str, api_token: str, url: Optional[str] = None -) -> None: - logger.debug(f"Adding attachment to card: {card_id}") - params = { - "key": api_key, - "token": api_token, - } - if url: - params["url"] = url - - response = requests.post( - f"https://api.trello.com/1/cards/{card_id}/attachments", - params=params, - ) - response.raise_for_status() - logger.debug(f"Added attachment to card: {card_id}") diff --git a/scripts/github-to-trello b/scripts/github-to-trello index 6cf0063..a797ea2 100755 --- a/scripts/github-to-trello +++ b/scripts/github-to-trello @@ -1,96 +1,33 @@ #! /usr/bin/env python3 - -# fmt:off -import sys -import os -from pathlib import Path -sys.path.append((Path(os.environ["EWCONFIG_ROOT"]) / "python_modules").as_posix()) -# fmt:on - import argparse import sys import logging +import subprocess import requests -from pathlib import Path -from dataclasses import dataclass, field -from typing import List, Optional, Dict, Any -from ewconfig.secret_manager import get_semi_secret_string -from ewconfig.trello import TRELLO_API_KEY, get_trello_api_token -from ewconfig.trello.cards import get_all_trello_cards, create_card, add_attachment -from ewconfig.trello.boards import PERSONAL_TASKS_BOARD + +try: + import github +except ImportError: + print("Please install the 'PyGithub' package from pip", file=sys.stderr) + sys.exit(1) + +TRELLO_BOARD_ID = "tw3Cn3L6" +TRELLO_LIST_ID = "6348a3ce5208f505b61d29bf" # To Do +TRELLO_TAGS = { + "waiting_to_merge": "65524315edf2d2edb0cc5d09", + "draft": "65fdd81c83e5d6e00f1b9721", + "issue": "64eb5d72fb694cd8f0ba7a8d" +} logger = logging.getLogger(__name__) -GITHUB_API_VERSION = "2022-11-28" -GITHUB_PAT = get_semi_secret_string("github_pat", namespace="trello-sync") -TRELLO_API_TOKEN = get_trello_api_token() - - -def get_all_issues() -> List[Dict[str, Any]]: - issues = [] - - # Get all issues assigned to me - response = requests.get( - "https://api.github.com/issues", - headers={ - "Authorization": f"token {GITHUB_PAT}", - "Accept": "application/vnd.github.raw+json", - "X-GitHub-Api-Version": GITHUB_API_VERSION, - }, - params={"state": "open", "per_page": 100}, - ) - response.raise_for_status() - issues.extend(response.json()) - - # Get all issues that mention me - response = requests.get( - "https://api.github.com/user/issues", - headers={ - "Authorization": f"token {GITHUB_PAT}", - "Accept": "application/vnd.github.raw+json", - "X-GitHub-Api-Version": GITHUB_API_VERSION, - }, - params={"state": "open", "per_page": 100, "filter": "mentioned"}, - ) - response.raise_for_status() - issues.extend(response.json()) - - # Get all issues that exist in my repos - response = requests.get( - "https://api.github.com/user/issues", - headers={ - "Authorization": f"token {GITHUB_PAT}", - "Accept": "application/vnd.github.raw+json", - "X-GitHub-Api-Version": GITHUB_API_VERSION, - }, - params={"state": "open", "per_page": 100, "filter": "repos"}, - ) - response.raise_for_status() - issues.extend(response.json()) - - # Get all issues that I have made in other people's repos - response = requests.get( - "https://api.github.com/user/issues", - headers={ - "Authorization": f"token {GITHUB_PAT}", - "Accept": "application/vnd.github.raw+json", - "X-GitHub-Api-Version": GITHUB_API_VERSION, - }, - params={"state": "open", "per_page": 100, "filter": "subscribed"}, - ) - response.raise_for_status() - issues.extend(response.json()) - - # De-dupe issues - issues = list({issue["id"]: issue for issue in issues}.values()) - - return issues - def main() -> int: # Handle program arguments - ap = argparse.ArgumentParser(prog="", description="") - ap.add_argument("--dry-run", help="Don't actually do anything", action="store_true") + ap = argparse.ArgumentParser( + prog="github-to-trello", description="Syncs GitHub issues to Trello cards" + ) + ap.add_argument( "-v", "--verbose", help="Enable verbose logging", action="store_true" ) @@ -102,72 +39,94 @@ def main() -> int: format="%(levelname)s: %(message)s", ) - # Get a list of all issues assigned to me - my_issues = get_all_issues() - logger.info(f"Found {len(my_issues)} issues assigned to me") + # Read Trello API credentials + trello_api_key = subprocess.check_output( + "op read -n 'op://ewconfig/cbnd5vv3germmc4korkxx3nsim/api key'", shell=True + ).decode() + trello_api_token = subprocess.check_output( + "op read -n 'op://ewconfig/cbnd5vv3germmc4korkxx3nsim/credential'", + shell=True, + ).decode() - # Get all cards on the personal tasks board - trello_cards = get_all_trello_cards( - board_id=PERSONAL_TASKS_BOARD.id, - api_key=TRELLO_API_KEY, - api_token=TRELLO_API_TOKEN, - ) - logger.info(f"Found {len(trello_cards)} cards in Trello") + # Read GitHub credential + logger.info("Authenticating with GitHub") + github_pat = subprocess.check_output( + "op read -n 'op://ewconfig/obs3gaeg7lcff7v5ewbvgxvwgu/credential'", shell=True + ).decode() + gh_api = github.Github(auth=github.Auth.Token(github_pat)) - # Handle each GitHub issue - for issue in my_issues: - # Ignore archived repos - if issue["repository"]["archived"]: - logger.info(f"Ignoring archived repo: {issue['repository']['full_name']}") - continue + # Get my user object + user = gh_api.get_user() + logger.info(f"Authenticated as {user.login}") + + # Get all my issues + issues = list(user.get_issues()) + logger.info(f"Found {len(issues)} issues/prs") + + # Filter out any issue that is in an archived repo + issues = [issue for issue in issues if not issue.repository.archived] + logger.info(f"{len(issues)} of those are actually in active repos") + + # Fetch a list of all of my Trello cards + trello_cards = requests.get( + f"https://api.trello.com/1/boards/{TRELLO_BOARD_ID}/cards", + params={"key": trello_api_key, "token": trello_api_token}, + ).json() + + for issue in issues: + repo = issue.repository + is_pr = issue.pull_request is not None + author = issue.user.login + logger.info( + f"Found {'pr' if is_pr else 'issue'} {repo.full_name}#{issue.number} by {author}" + ) - # Ignore anything by dependabot - if issue["user"]["login"] == "dependabot[bot]": - logger.debug(f"Ignoring dependabot issue: {issue['repository']['full_name']}#{issue['number']}") - continue - - # Search each card for anything that links to the github issue + # Check if any trello card already mentions this issue in the description for card in trello_cards: - if issue["html_url"] in card["desc"]: - logger.info( - f"Found GitHub Issue {issue['number']} in Trello Card {card['id']}" - ) + if issue.html_url in card["desc"]: + logger.info(f"Found Trello card {card['id']} for this issue") break else: - logger.info( - f"Going to create trello card for GitHub Issue: [{issue['repository']['full_name']}] {issue['title']}" + logger.info(f"Creating Trello card for this issue") + + # Sort out the appropriate labels + card_labels = [] + if is_pr: + # If this is a draft PR, add the draft label + if issue.draft: + card_labels.append(TRELLO_TAGS["draft"]) + else: + # Otherwise, add the waiting to merge label + card_labels.append(TRELLO_TAGS["waiting_to_merge"]) + else: + # If this is an issue, add the issue label + card_labels.append(TRELLO_TAGS["issue"]) + + print(card_labels) + # Create the card + card = requests.post( + f"https://api.trello.com/1/cards", + params={ + "key": trello_api_key, + "token": trello_api_token, + "idList": TRELLO_LIST_ID, + "name": f"[{repo.full_name}] {issue.title}", + "desc": f"**GitHub Link:** {issue.html_url}\n\n**Author:** [{author}]({issue.user.html_url})\n\n---", + "idLabels": card_labels + }, + ).json() + logger.info(f"Created Trello card {card['id']} for this issue") + + # Attach the issue to the card + requests.post( + f"https://api.trello.com/1/cards/{card['id']}/attachments", + params={ + "key": trello_api_key, + "token": trello_api_token, + "url": issue.html_url + } ) - if not args.dry_run: - # Check if this is an issue or pr - is_pr = "pull_request" in issue - type_label = ( - PERSONAL_TASKS_BOARD.tags["Github: Pull Request"] - if is_pr - else PERSONAL_TASKS_BOARD.tags["Github: Issue"] - ) - - # Create a new trello card for this issue - card_id = create_card( - list_id=PERSONAL_TASKS_BOARD.lists["To Do"], - name=f"[{issue['repository']['full_name']}] {issue['title']}", - description=( - f"**GitHub Link:** [`{issue['repository']['full_name']}#{issue['number']}`]({issue['html_url']})\n\n" - f"**Author:** [`{issue['user']['login']}`]({issue['user']['html_url']})\n\n" - "---" - ), - label_ids=[type_label], - api_key=TRELLO_API_KEY, - api_token=TRELLO_API_TOKEN, - ) - add_attachment( - card_id=card_id, - api_key=TRELLO_API_KEY, - api_token=TRELLO_API_TOKEN, - url=issue["html_url"], - ) - logger.info( - f"Created Trello Card {card_id} for GitHub Issue {issue['repository']['full_name']}#{issue['number']}" - ) + logger.info(f"Attached issue to Trello card {card['id']}") return 0