diff --git a/configs/scripts/guru-sync-issues b/configs/scripts/guru-sync-issues new file mode 100755 index 0000000..c968415 --- /dev/null +++ b/configs/scripts/guru-sync-issues @@ -0,0 +1,199 @@ +#! /usr/bin/env python3 +import argparse +import sys +import logging +import requests +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Optional, Dict, Any +from enum import Enum, auto +from datetime import datetime + +logger = logging.getLogger(__name__) + +GITLAB_PAT_FILE_PATH = Path("~/.config/guru-sync-issues/gitlab_pat.txt").expanduser() +GITLAB_ENDPOINT = "http://gitlab.guru-domain.gurustudio.com/api/v4" +MY_USER_ID = 64 +TRELLO_API_TOKEN_FILE_PATH = Path( + "~/.config/guru-sync-issues/trello_api_token.txt" +).expanduser() +TRELLO_API_KEY = "fba640a85f15c91e93e6b3f88e59489c" +TRELLO_BOARD = "tw3Cn3L6" +TRELLO_TODO_LIST = "6348a3ce5208f505b61d29bf" +TRELLO_LABELS = { + "GURU": "64e03ac77d27032282436d28" +} + +# Load the PAT +try: + with open(GITLAB_PAT_FILE_PATH) as f: + GITLAB_PAT = f.read().strip() +except FileNotFoundError: + logger.error( + f"Could not find GitLab PAT file at {GITLAB_PAT_FILE_PATH}. Please create it and try again." + ) + sys.exit(1) + +# Load Trello key +try: + with open(TRELLO_API_TOKEN_FILE_PATH) as f: + TRELLO_API_TOKEN = f.read().strip() +except FileNotFoundError: + logger.error( + f"Could not find Trello API key file at {TRELLO_API_TOKEN_FILE_PATH}. Please create it and try again." + ) + sys.exit(1) + +TrelloCardId = str + + +class IssueState(Enum): + OPEN = "opened" + CLOSED = "closed" + + +@dataclass +class GitLabIssue: + title: str + issue_id: int + global_id: int + state: IssueState + created: datetime + updated: datetime + web_url: str + reference_string: str + due_date: Optional[datetime] = None + + +def get_personal_gitlab_issues(user_id: int = MY_USER_ID) -> List[GitLabIssue]: + # Make an API call + response = requests.get( + f"{GITLAB_ENDPOINT}/issues", + params={ + "assignee_id": user_id, + "private_token": GITLAB_PAT, + "per_page": 100, + "scope": "all", + }, + ) + response.raise_for_status() + + # Parse the response + output = [] + for issue in response.json(): + output.append( + GitLabIssue( + title=issue["title"], + issue_id=issue["iid"], + global_id=issue["id"], + state=IssueState(issue["state"]), + created=datetime.fromisoformat(issue["created_at"]), + updated=datetime.fromisoformat(issue["updated_at"]), + web_url=issue["web_url"], + reference_string=issue["references"]["full"], + due_date=datetime.fromisoformat(issue["due_date"]) + if issue["due_date"] + else None, + ) + ) + + return output + + +def get_all_trello_cards() -> List[Dict[str, Any]]: + # Get a list of cards on the board + response = requests.get( + f"https://api.trello.com/1/boards/{TRELLO_BOARD}/cards", + params={ + "key": TRELLO_API_KEY, + "token": TRELLO_API_TOKEN, + }, + ) + response.raise_for_status() + return response.json() + + +def find_or_create_trello_issue_for( + trello_cards: List[Dict[str, Any]], gitlab_issue: GitLabIssue +) -> TrelloCardId: + # Look for a card that matches the issue + for card in trello_cards: + # Check the first line of the description for metadata + description = card["desc"] + desc_first_line = description.split("\n")[0] + if not desc_first_line.startswith("**Sync Metadata:** "): + continue + + # Parse the metadata + metadata = json.loads(desc_first_line.split("`")[1]) + + # Check if the card matches + if metadata.get("ns") == "guru-gitlab" and ( + gitlab_issue.global_id in metadata.get("ids", []) + ): + print(card["labels"], card["idLabels"]) + logger.info(f"Found matching card {card['id']}") + return card["id"] + + # Build the description + card_description = "\n\n".join( + [ + f"**Sync Metadata:** `{json.dumps({'ns': 'guru-gitlab', 'ids': [gitlab_issue.global_id]})}`", + f"**GitLab Issue:** [`{gitlab_issue.reference_string}`]({gitlab_issue.web_url})\n", + "---" + ] + ) + + # Make a new card + response = requests.post( + "https://api.trello.com/1/cards", + params={ + "idList": TRELLO_TODO_LIST, + "name": gitlab_issue.title, + "desc": card_description, + "idLabels": ",".join([TRELLO_LABELS["GURU"]]), + "pos": "top", + "key": TRELLO_API_KEY, + "token": TRELLO_API_TOKEN, + }, + ) + response.raise_for_status() + + return response.json()["id"] + + +def main() -> int: + # Handle program arguments + ap = argparse.ArgumentParser(description="Syncs issues from GitLab to Trello") + + ap.add_argument( + "-v", "--verbose", help="Enable verbose logging", action="store_true" + ) + args = ap.parse_args() + + # Configure logging + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(levelname)s: %(message)s", + ) + + # Find all issues + issues = get_personal_gitlab_issues() + logger.info(f"Found {len(issues)} issues") + + # Get a list of cards on the board + trello_cards = get_all_trello_cards() + logger.info(f"Found {len(trello_cards)} cards on the board") + + # Handle each issue + for issue in issues: + # Find the trello card id for this issue + trello_card_id = find_or_create_trello_issue_for(trello_cards, issue) + logger.info(f"GitLab Issue {issue.global_id} is Trello Card {trello_card_id}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main())