""" Walk all files in a directory, checking whether any TODOs are linked to a resolved JIRA ticket, and labeling those JIRA tickets. """ #!/usr/bin/env python3 import argparse import os import re import sys import structlog from jira import JIRAError from structlog.stdlib import LoggerFactory # Get relative imports to work when the package is not installed on the PYTHONPATH. if __name__ == "__main__" and __package__ is None: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from buildscripts.client.jiraclient import JiraAuth, JiraClient structlog.configure(logger_factory=LoggerFactory()) LOG = structlog.getLogger(__name__) JIRA_SERVER = "https://jira.mongodb.org" def find_todos(search_file, jira, file_name): """Iterate through a file, finding TODOs with resolved tickets and creating new tickets.""" for i, line in enumerate(search_file): if "todo" in line.lower(): issue_key = get_issue_key_from_line(line) if issue_key: LOG.info("\n") LOG.info("=== Found Issue Key ===") LOG.info(line.lstrip().rstrip()) LOG.info(f"{JIRA_SERVER}/browse/{issue_key}") LOG.info(f"Found in {file_name} on line {i+1}") # It is possible the referenced ticket in the code was deleted, so we need to # handle the possibility that searching for it will return nothing. try: issue = jira.issue(issue_key) except JIRAError: LOG.warn(f"{issue_key} not found in Jira. Skipping.") continue status = str(issue.fields.status) if status not in ["Resolved", "Closed"]: LOG.info(f"{issue_key} is not resolved. Skipping.") continue if todo_ticket_exists(jira, issue): LOG.info(f"Autogenerated ticket linked to {issue_key} already exists.") continue create_todo_ticket(jira, issue) def todo_ticket_exists(jira, resolved_issue): """Check if we have already created a ticket for this resolved issue""" jql = ( "labels = autogen-todo AND resolution is empty AND issueFunction in" f" linkedIssuesOf('key={resolved_issue.key}')" ) results = jira.search_issues(jql, maxResults=1000) if not results or results.total == 0: return False return True def create_todo_ticket(jira, resolved_issue): """Given a resolved ticket, create a new ticket for that work and link to the resolved ticket.""" key = resolved_issue.key assignee = get_assignable_user(resolved_issue) assigned_team = resolved_issue.fields.customfield_12751 # Derive Team from Resolved ticket team = assigned_team[0].value if assigned_team else "Server Triage" issue_dict = { "project": {"key": "SERVER"}, "issuetype": {"name": "Task"}, "summary": "Complete TODO listed in " + key, "assignee": {"name": assignee}, "description": construct_description(key), "labels": ["autogen-todo"], "customfield_12751": [{"value": team}], } if "REP" in key: issue_dict["project"] = {"key": "REP"} # It's possible for us to try and create a ticket with an illegal assignee (most commonly # a former employee) so for now we default to hard assigning these to Joe to reassign. # This situation should be very infrequent. try: new_issue = jira.create_issue(fields=issue_dict) except JIRAError: issue_dict["assignee"]["name"] = "joseph.kanaan@mongodb.com" new_issue = jira.create_issue(fields=issue_dict) jira.create_issue_link( type="Related", inwardIssue=resolved_issue.key, outwardIssue=new_issue.key ) def construct_description(key): repo = "10gen/mongosync" if "REP" in key else "10gen/mongo" return ( "There is a TODO in the codebase referencing a resolved ticket which is" " assigned to you.\n\nPlease follow this link to see the lines of code" " referencing this resolved" f" ticket:\nhttps://github.com/{repo}/search?q={key}&type=Code\n\nThe next" " steps for this ticket are to either remove the outdated TODO or follow the" " steps in the TODO if it is correct. If the latter, please update the summary" " and description of this ticket to represent the work you're actually doing." ) def get_assignable_user(ticket): """If the original ticket is assigned to an existing employee, return the assignee""" assignee = None if ticket.fields.assignee: assignee = ticket.fields.assignee.name return assignee def get_issue_key_from_line(line): """Given a string of text, find and return any issue keys from relevent projects.""" match = re.search( "(BUILD|SERVER|WT|SPM|TOOLS|TIG|PERF|BF|REP|BACKPORT|WRITING|STAR)-[0-9]+", line, re.IGNORECASE, ) if match: return match.group(0) def main(): """Execute main function.""" argparser = argparse.ArgumentParser(description="") argparser.add_argument("--env", "-e", help="Jira environment {stg, prod}", required=False) argparser.add_argument("--path", "-p", help="File path to walk", required=True) args = vars(argparser.parse_args()) jira = JiraClient(JIRA_SERVER, JiraAuth(), dry_run=False) for root, _, files in os.walk(args["path"]): for file_name in files: # ignore .git/ if ".git" in root: continue try: with open(os.path.join(root, file_name), "r") as search_file: find_todos(search_file, jira._jira, file_name) search_file.close() except UnicodeDecodeError: continue if __name__ == "__main__": main()