Skip to content

Commit 04b0d8f

Browse files
committed
jira initial pr
1 parent fed2a0d commit 04b0d8f

File tree

9 files changed

+362
-2
lines changed

9 files changed

+362
-2
lines changed

apps/jira_utils/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os
2+
import click
3+
4+
from simple_logger.logger import get_logger
5+
6+
from apps.jira_utils.utils import get_jiras_from_python_files, get_closed_jira_ids, get_jira_connection_params, \
7+
get_jira_version_mismatch, JiraConnector
8+
from apps.utils import ListParamType
9+
10+
LOGGER = get_logger(name=__name__)
11+
12+
13+
@click.command()
14+
@click.option(
15+
"--jira-cfg-file",
16+
help="Provide absolute path to the jira_utils config file. ",
17+
type=click.Path(),
18+
default=os.path.expanduser("~/.config/python-utility-scripts/jira_utils/config.cfg"),
19+
)
20+
@click.option(
21+
"--jira-target-versions",
22+
help="Provide comma separated list of Jira target version for the bugs.",
23+
type=ListParamType(),
24+
required=True
25+
)
26+
def get_closed_jiras(jira_cfg_file, jira_target_versions):
27+
jira_connector = JiraConnector(cfg_file=jira_cfg_file)
28+
jira_id_dict = get_jiras_from_python_files()
29+
if jira_errors:= get_closed_jira_ids(jira_connector=jira_connector,
30+
jira_ids_dict=jira_id_dict):
31+
click.echo(f"Following jiras are not open or could not be accessed: {jira_errors}")
32+
raise click.Abort()
33+
if version_mismatch := get_jira_version_mismatch(jira_connector=jira_connector, jira_id_dict=jira_id_dict,
34+
jira_expected_versions=jira_target_versions):
35+
click.echo(f"Following jiras are not matching expected version{jira_target_versions}: {version_mismatch}")
36+
raise click.Abort()
37+
38+
39+
if __name__ == "__main__":
40+
get_closed_jiras()

apps/jira_utils/utils.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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

apps/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def all_python_files() -> Iterable[str]:
8080
for root, _, files in os.walk(os.path.abspath(os.curdir)):
8181
if [_dir for _dir in exclude_dirs if _dir in root]:
8282
continue
83-
83+
LOGGER.info(root)
8484
for filename in files:
8585
if filename.endswith(".py") and filename != os.path.split(__file__)[-1]:
8686
yield os.path.join(root, filename)

example.jira.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[DEFAULT]
2+
url = https://www.atlassian.com/
3+
token = mytoken
4+
resolved_statuses = (verified, release pending, closed)

0 commit comments

Comments
 (0)