#! /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())