1+ import os
2+ import re
3+ import click
4+ from configparser import ConfigParser
5+ from packaging .version import InvalidVersion , Version
6+ from simple_logger .logger import get_logger
7+ from jira import JIRA , JIRAError
8+
9+ from apps .utils import all_python_files
10+
11+ LOGGER = get_logger (name = __name__ )
12+
13+
14+ def get_jira_connection_params (conf_file_name ):
15+ if os .path .exists (conf_file_name ):
16+ parser = ConfigParser ()
17+ parser .read (conf_file_name , encoding = "utf-8" )
18+ params_dict = {}
19+ for params in parser .items ("DEFAULT" ):
20+ params_dict [params [0 ]] = params [1 ]
21+ return params_dict
22+ click .echo ("Jira config file is required." )
23+ raise click .Abort ()
24+
25+
26+ class JiraConnector :
27+ def __init__ (self , cfg_file ):
28+ self .cfg_file = cfg_file
29+ config_dict = get_jira_connection_params (conf_file_name = self .cfg_file )
30+ self .token = config_dict ["token" ]
31+ self .server = config_dict ["url" ]
32+ if not (self .token and self .server ):
33+ raise ValueError ("Jira config file must contain token and server information." )
34+
35+ self .resolved_statuses = config_dict .get ('resolved_statuses' )
36+ self .project_ids = config_dict .get ('project_ids' )
37+ self .jira = None
38+ self .authenticate_to_jira_server ()
39+
40+ def authenticate_to_jira_server (self ):
41+ try :
42+ self .jira = JIRA (token_auth = self .token , options = {"server" : self .server })
43+ LOGGER .info ("Connected to Jira" )
44+ except JIRAError as e :
45+ LOGGER .error ("Failed to connect to Jira: %s" , e )
46+ raise
47+
48+ def get_issue_metadata (self , jira_id , max_retry = 3 ):
49+ retries = 0
50+ while True :
51+ try :
52+ return self .jira .issue (id = jira_id , fields = "status, issuetype, fixVersions" ).fields
53+ except JIRAError as e :
54+ # Check for inactivity error (adjust based on your library)
55+ if "401 Unauthorized" in str (e ) or "Session timed out" in str (e ):
56+ retries += 1
57+ LOGGER .warning ("Failed to get issue due to inactivity, retrying (%d/%d)" , retries , max_retry )
58+ if retries < max_retry :
59+ self .authenticate_to_jira_server () # Attempt reconnection
60+ else :
61+ raise # Re-raise the error after exceeding retries
62+ else :
63+ raise
64+
65+
66+ def get_all_jiras_from_file (file_content ):
67+ """
68+ Try to find all jira_utils tickets in the file.
69+ Looking for the following patterns:
70+ - jira_id=CNV-12345 # call in is_jira_open
71+ - https://issues.redhat.com/browse/CNV-12345 # when jira_utils is in a link in comments
72+ - pytest.mark.jira_utils(CNV-12345) # when jira_utils is in a marker
73+
74+ Args:
75+ file_content (str): The content of the file.
76+
77+ Returns:
78+ list: A list of jira_utils tickets.
79+ """
80+ issue_pattern = r"([A-Z]+-[0-9]+)"
81+ _pytest_jira_marker_bugs = re .findall (
82+ rf"pytest.mark.jira.*?{ issue_pattern } .*" , file_content , re .DOTALL
83+ )
84+ _is_jira_open = re .findall (rf"jira_id\s*=[\s*\"\']*{ issue_pattern } .*" , file_content )
85+ _jira_url_jiras = re .findall (
86+ rf"https://issues.redhat.com/browse/{ issue_pattern } (?! <skip-jira_utils-check>)" ,
87+ file_content ,
88+ )
89+ return list (set (_pytest_jira_marker_bugs + _is_jira_open + _jira_url_jiras ))
90+
91+
92+ def get_jiras_from_python_files ():
93+ jira_found = {}
94+ for filename in all_python_files ():
95+ with open (filename ) as fd :
96+ if unique_jiras := get_all_jiras_from_file (file_content = fd .read ()):
97+ jira_found [filename ] = unique_jiras
98+ LOGGER .warning (f"File: { filename } , { unique_jiras } " )
99+ return jira_found
100+
101+
102+ def get_closed_jira_ids (jira_connector , jira_ids_dict ):
103+ jira_errors = {}
104+ for file_name in jira_ids_dict :
105+ for jira_id in jira_ids_dict [file_name ]:
106+ try :
107+ current_jira_status = jira_connector .get_issue_metadata (jira_id = jira_id ).status .name .lower ()
108+ if current_jira_status in jira_connector .resolved_statuses :
109+ jira_errors .setdefault (file_name , []).append (
110+ f"{ jira_id } [{ current_jira_status } ]"
111+ )
112+ except JIRAError as exp :
113+ jira_errors .setdefault (file_name , []).append (
114+ f"{ jira_id } [{ exp .text } ]"
115+ )
116+ continue
117+ return jira_errors
118+
119+
120+ def get_jira_version_mismatch (jira_connector , jira_id_dict , jira_expected_versions = None ):
121+ jira_mismatch = {}
122+ for file_name in jira_id_dict :
123+ unique_jira_ids = jira_id_dict [file_name ]
124+ if jira_connector .project_ids :
125+ unique_jira_ids = [jira_id for jira_id in unique_jira_ids
126+ if jira_id .startswith (tuple (jira_connector .project_ids ))]
127+ for jira_id in unique_jira_ids :
128+ jira_issue = jira_connector .get_issue_metadata (jira_id = jira_id )
129+ fix_version = re .search (r"([\d.]+)" , jira_issue .fixVersions [0 ].name ) if jira_issue .fixVersions else None
130+ jira_target_release_version = fix_version .group (1 ) if fix_version else "vfuture"
131+ LOGGER .info (f"issue: { jira_id } , version: { jira_target_release_version } , { jira_expected_versions } " )
132+ if not jira_target_release_version .startswith (tuple (jira_expected_versions )):
133+ jira_mismatch .setdefault (file_name , []).append (
134+ f"{ jira_id } [{ jira_target_release_version } ]"
135+ )
136+
137+ return jira_mismatch
0 commit comments