272 lines
8.7 KiB
Python
Executable File
272 lines
8.7 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
import argparse
|
|
import sys
|
|
import logging
|
|
import requests
|
|
import subprocess
|
|
from textwrap import dedent
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
try:
|
|
import gitlab
|
|
except ImportError:
|
|
print("Please install the 'python-gitlab' package from pip", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
TRELLO_KEY = "fba640a85f15c91e93e6b3f88e59489c"
|
|
TRELLO_BOARD_ID = "tw3Cn3L6"
|
|
TRELLO_LIST_ID = "6593166e9dd338621ed6848d"
|
|
TRELLO_TAGS = {
|
|
"company": "64e03ac77d27032282436d28", # Tag used to sort by company
|
|
"waiting_to_merge": "65524315edf2d2edb0cc5d09", # Tag used to indicate a MR is waiting to merge
|
|
"draft": "65fdd81c83e5d6e00f1b9721", # Tag used to indicate a draft MR
|
|
}
|
|
GITLAB_ENDPOINT = "http://gitlab.guru-domain.gurustudio.com"
|
|
MY_ID = 64
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def main() -> int:
|
|
# Handle program arguments
|
|
ap = argparse.ArgumentParser(
|
|
prog="guru-sync-issues", description="Sync 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",
|
|
)
|
|
|
|
# Call `ewp-secrets` to obtain the GitLab token
|
|
secrets_proc = subprocess.run(
|
|
[
|
|
(Path(__file__).parent / "ewp-secrets").as_posix(),
|
|
"load",
|
|
"-n",
|
|
"gurustudio",
|
|
"-k",
|
|
"gitlab-pat",
|
|
],
|
|
capture_output=True,
|
|
)
|
|
|
|
# If the secret manager failed, exit
|
|
if secrets_proc.returncode != 0:
|
|
print("Failed to load GitLab PAT", file=sys.stderr)
|
|
print(
|
|
"Please run `ewp-secrets store -n gurustudio -k gitlab-pat` to set a token",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
# Extract the GitLab token
|
|
gitlab_token = secrets_proc.stdout.decode().strip()
|
|
|
|
# Call `ewp-secrets` to obtain the Trello API token
|
|
secrets_proc = subprocess.run(
|
|
[
|
|
(Path(__file__).parent / "ewp-secrets").as_posix(),
|
|
"load",
|
|
"-n",
|
|
"ewpratten",
|
|
"-k",
|
|
"trello-api-token",
|
|
],
|
|
capture_output=True,
|
|
)
|
|
|
|
# If the secret manager failed, exit
|
|
if secrets_proc.returncode != 0:
|
|
print("Failed to load Trello API token", file=sys.stderr)
|
|
print(
|
|
"Please run `ewp-secrets store -n ewpratten -k trello-api-token` to set a token",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
# Extract the Trello API token
|
|
trello_api_token = secrets_proc.stdout.decode().strip()
|
|
|
|
# Try to ping the gitlab server, and exit if it fails
|
|
try:
|
|
logger.debug("Attempting to connect to GitLab server")
|
|
requests.get(GITLAB_ENDPOINT, timeout=2)
|
|
except requests.exceptions.ConnectionError:
|
|
logger.error("Could not connect to GitLab server")
|
|
return 1
|
|
|
|
# Authenticate with GitLab
|
|
gitlab_client = gitlab.Gitlab(
|
|
GITLAB_ENDPOINT, private_token=gitlab_token, user_agent="guru-sync-issues"
|
|
)
|
|
|
|
# Access the main group
|
|
pp_group = gitlab_client.groups.get("pipeline-planning")
|
|
|
|
# Find all open issues I'm assigned to
|
|
open_issues = []
|
|
open_issues.extend(
|
|
gitlab_client.issues.list(state="opened", get_all=True, assignee_id=MY_ID)
|
|
)
|
|
open_issues.extend(
|
|
pp_group.issues.list(state="opened", get_all=True, assignee_id=MY_ID)
|
|
)
|
|
|
|
# De-dupe the issues
|
|
open_issues = list(set(open_issues))
|
|
|
|
# Find all open MRs I'm assigned to
|
|
open_mrs = []
|
|
open_mrs.extend(
|
|
gitlab_client.mergerequests.list(
|
|
state="opened", get_all=True, assignee_id=MY_ID
|
|
)
|
|
)
|
|
open_mrs.extend(
|
|
pp_group.mergerequests.list(state="opened", get_all=True, assignee_id=MY_ID)
|
|
)
|
|
|
|
# De-dupe the MRs
|
|
open_mrs = list(set(open_mrs))
|
|
|
|
# Log findings
|
|
logger.info(f"Found {len(open_issues)} open issues")
|
|
logger.info(f"Found {len(open_mrs)} open merge requests")
|
|
|
|
# Get all cards in Trello
|
|
response = requests.get(
|
|
f"https://api.trello.com/1/boards/{TRELLO_BOARD_ID}/cards",
|
|
params={
|
|
"key": TRELLO_KEY,
|
|
"token": trello_api_token,
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
trello_cards = response.json()
|
|
|
|
# Iterate over each GitLab actionable
|
|
actionables = [("issue", issue) for issue in open_issues] + [
|
|
("merge request", mr) for mr in open_mrs
|
|
]
|
|
for ty, issue in actionables:
|
|
# Get the URL of the issue
|
|
issue_url = issue.web_url
|
|
|
|
# Check if there is a card that references this URL
|
|
card = next((c for c in trello_cards if issue_url in c["desc"]), None)
|
|
|
|
# If none exists, make a new card
|
|
if not card:
|
|
logger.info(f"Creating card for issue: {issue.title}")
|
|
|
|
# Build params
|
|
card_params = {
|
|
"idList": TRELLO_LIST_ID,
|
|
"name": issue.title,
|
|
"key": TRELLO_KEY,
|
|
"token": trello_api_token,
|
|
"pos": "top",
|
|
"desc": dedent(
|
|
f"""
|
|
## Linked GitLab Issues
|
|
- {issue_url}
|
|
|
|
---
|
|
"""
|
|
),
|
|
"idLabels": TRELLO_TAGS["company"],
|
|
}
|
|
|
|
# Make the card
|
|
response = requests.post(
|
|
"https://api.trello.com/1/cards",
|
|
params=card_params,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# Capture the card for later
|
|
card = response.json()
|
|
logger.info(f"Created card: {card['id']}")
|
|
|
|
# Apply lables to MRs
|
|
if ty == "merge request":
|
|
if issue.title.startswith("Draft:") or issue.title.startswith("WIP:"):
|
|
# Check if the card already has the 'Draft' label
|
|
if any(
|
|
label["id"] == TRELLO_TAGS["draft"] for label in card["labels"]
|
|
):
|
|
continue
|
|
|
|
logger.info(f"Adding 'Draft' label to card {card['id']}")
|
|
response = requests.post(
|
|
f"https://api.trello.com/1/cards/{card['id']}/idLabels",
|
|
params={
|
|
"key": TRELLO_KEY,
|
|
"token": trello_api_token,
|
|
"value": TRELLO_TAGS["draft"],
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
|
|
else:
|
|
# Check if the card already has the 'Waiting to Merge' label
|
|
if any(
|
|
label["id"] == TRELLO_TAGS["waiting_to_merge"] for label in card["labels"]
|
|
):
|
|
continue
|
|
|
|
logger.info(f"Adding 'Waiting to Merge' label to card {card['id']}")
|
|
response = requests.post(
|
|
f"https://api.trello.com/1/cards/{card['id']}/idLabels",
|
|
params={
|
|
"key": TRELLO_KEY,
|
|
"token": trello_api_token,
|
|
"value": TRELLO_TAGS["waiting_to_merge"],
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# Determine the due date assigned in GitLab
|
|
if ty != "merge request":
|
|
due_date: Optional[str] = issue.due_date
|
|
if due_date:
|
|
# Check if the card already has a due date
|
|
card_due_date = card.get("due", None)
|
|
if card_due_date:
|
|
card_due_date = card_due_date.split("T")[0]
|
|
|
|
# Convert to a useful format
|
|
due_date = datetime.strptime(due_date, "%Y-%m-%d")
|
|
card_due_date = datetime.strptime(card_due_date, "%Y-%m-%d") if card_due_date else None
|
|
|
|
# If the card has an earlier due date, skip
|
|
if card_due_date and due_date < card_due_date:
|
|
logger.debug(f"Skipping due date update for {card['id']} because it has an earlier due date already")
|
|
continue
|
|
|
|
# Update the due date
|
|
logger.info(f"Updating due date for card {card['id']} to {due_date}")
|
|
response = requests.put(
|
|
f"https://api.trello.com/1/cards/{card['id']}",
|
|
params={
|
|
"key": TRELLO_KEY,
|
|
"token": trello_api_token,
|
|
"due": due_date.isoformat(),
|
|
},
|
|
)
|
|
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|