Skip to content

Commit 50a2bb9

Browse files
GHA-169 Normalize Jira version names by removing .0 patch suffix (#79)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f8f3b05 commit 50a2bb9

2 files changed

Lines changed: 152 additions & 36 deletions

File tree

create-jira-version/create_jira_version.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@
77

88
import argparse
99
import os
10+
import re
1011
import sys
1112
from jira import JIRA
1213
from jira.exceptions import JIRAError
1314

1415

16+
def normalize_version_name(version_name):
17+
"""
18+
Normalizes a version name by removing .0 patch suffix.
19+
For example: 1.2.0 -> 1.2, but 1.2.4 stays as 1.2.4
20+
"""
21+
# Match versions ending in .0 (e.g., 1.2.0, 10.20.0)
22+
match = re.match(r'^(.+)\.0$', version_name)
23+
if match:
24+
return match.group(1)
25+
return version_name
26+
27+
1528
# noinspection DuplicatedCode
1629
def eprint(*args, **kwargs):
1730
"""Prints messages to the standard error stream (stderr) for logging."""
@@ -47,6 +60,27 @@ def get_jira_instance(jira_url):
4760
sys.exit(1)
4861

4962

63+
def find_existing_version(jira, project_key, version_name):
64+
"""Finds and returns an existing version by name in a project."""
65+
project = jira.project(project_key)
66+
for version in project.versions:
67+
if version.name == version_name:
68+
return version
69+
return None
70+
71+
72+
def handle_existing_version(jira, project_key, version_name):
73+
"""Handles the case when a version already exists."""
74+
eprint(f"Warning: Version '{version_name}' already exists. Skipping creation.")
75+
existing_version = find_existing_version(jira, project_key, version_name)
76+
if existing_version:
77+
print(f"new_version_id={existing_version.id}")
78+
print(f"new_version_name={existing_version.name}")
79+
else:
80+
eprint(f"Error: Could not find existing version '{version_name}' in project.")
81+
sys.exit(1)
82+
83+
5084
def main():
5185
"""Main function to orchestrate the creation process."""
5286
parser = argparse.ArgumentParser(
@@ -60,32 +94,21 @@ def main():
6094

6195
jira = get_jira_instance(args.jira_url)
6296

63-
eprint(f"Try to create new version '{args.version_name}'")
97+
version_name = normalize_version_name(args.version_name)
98+
if version_name != args.version_name:
99+
eprint(f"Normalized version name from '{args.version_name}' to '{version_name}'")
100+
101+
eprint(f"Try to create new version '{version_name}'")
64102
try:
65-
new_version = jira.create_version(name=args.version_name, project=args.project_key)
103+
new_version = jira.create_version(name=version_name, project=args.project_key)
66104
eprint(f"✅ Successfully created new version '{new_version.name}'")
67105

68106
print(f"new_version_id={new_version.id}")
69107
print(f"new_version_name={new_version.name}")
70108

71109
except JIRAError as e:
72110
if "A version with this name already exists" in e.text:
73-
eprint(f"Warning: Version '{args.version_name}' already exists. Skipping creation.")
74-
75-
# Fetch the existing version details
76-
project = jira.project(args.project_key)
77-
existing_version = None
78-
for version in project.versions:
79-
if version.name == args.version_name:
80-
existing_version = version
81-
break
82-
83-
if existing_version:
84-
print(f"new_version_id={existing_version.id}")
85-
print(f"new_version_name={existing_version.name}")
86-
else:
87-
eprint(f"Error: Could not find existing version '{args.version_name}' in project.")
88-
sys.exit(1)
111+
handle_existing_version(jira, args.project_key, version_name)
89112
else:
90113
eprint(f"Error: Failed to create new version. Status: {e.status_code}, Text: {e.text}")
91114
sys.exit(1)

create-jira-version/test_create_jira_version.py

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,46 @@
1414
# Add the current directory to the path to import our module
1515
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1616

17-
from create_jira_version import get_jira_instance, main
17+
from create_jira_version import get_jira_instance, main, normalize_version_name
1818
from jira.exceptions import JIRAError
1919

2020

21+
class TestNormalizeVersionName(unittest.TestCase):
22+
"""Tests for the normalize_version_name function."""
23+
24+
def test_removes_zero_patch(self):
25+
"""Test that .0 patch is removed from version names."""
26+
self.assertEqual(normalize_version_name('1.2.0'), '1.2')
27+
28+
def test_keeps_non_zero_patch(self):
29+
"""Test that non-zero patches are kept."""
30+
self.assertEqual(normalize_version_name('1.2.4'), '1.2.4')
31+
self.assertEqual(normalize_version_name('1.2.1'), '1.2.1')
32+
self.assertEqual(normalize_version_name('10.20.30'), '10.20.30')
33+
34+
def test_handles_two_part_versions(self):
35+
"""Test that two-part versions are unchanged."""
36+
self.assertEqual(normalize_version_name('1.2'), '1.2')
37+
38+
def test_handles_single_zero(self):
39+
"""Test edge case with just 0."""
40+
self.assertEqual(normalize_version_name('0'), '0')
41+
42+
def test_handles_version_with_prefix(self):
43+
"""Test versions with prefixes like 'v'."""
44+
self.assertEqual(normalize_version_name('v1.2.0'), 'v1.2')
45+
self.assertEqual(normalize_version_name('v1.2.4'), 'v1.2.4')
46+
47+
def test_handles_four_part_version_with_zero(self):
48+
"""Test four-part versions ending in .0."""
49+
self.assertEqual(normalize_version_name('1.2.3.0'), '1.2.3')
50+
51+
def test_handles_multiple_zeros(self):
52+
"""Test versions with multiple zeros."""
53+
self.assertEqual(normalize_version_name('1.0.0'), '1.0')
54+
self.assertEqual(normalize_version_name('0.0.0'), '0.0')
55+
56+
2157
class TestCreateJiraVersion(unittest.TestCase):
2258

2359
def setUp(self):
@@ -59,7 +95,7 @@ def test_get_jira_instance_auth_failure(self, mock_jira_class):
5995
get_jira_instance('https://prod.com')
6096
self.assertEqual(cm.exception.code, 1)
6197

62-
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.0.0', '--jira-url', 'https://test.jira.com'])
98+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.5.0', '--jira-url', 'https://test.jira.com'])
6399
@patch('create_jira_version.get_jira_instance')
64100
@patch('sys.stdout', new_callable=StringIO)
65101
@patch('sys.stderr', new_callable=StringIO)
@@ -69,7 +105,7 @@ def test_main_successful_version_creation(self, mock_stderr, mock_stdout, mock_g
69105
mock_jira = Mock()
70106
mock_version = Mock()
71107
mock_version.id = '12345'
72-
mock_version.name = '1.0.0'
108+
mock_version.name = '1.5'
73109
mock_jira.create_version.return_value = mock_version
74110
mock_get_jira.return_value = mock_jira
75111

@@ -78,19 +114,20 @@ def test_main_successful_version_creation(self, mock_stderr, mock_stdout, mock_g
78114
# Verify get_jira_instance was called with correct URL
79115
mock_get_jira.assert_called_once_with('https://test.jira.com')
80116

81-
# Verify the version was created with correct parameters
82-
mock_jira.create_version.assert_called_once_with(name='1.0.0', project='TEST')
117+
# Verify the version was created with normalized name (1.5 instead of 1.5.0)
118+
mock_jira.create_version.assert_called_once_with(name='1.5', project='TEST')
83119

84120
# Verify output
85121
stdout_output = mock_stdout.getvalue()
86122
self.assertIn('new_version_id=12345', stdout_output)
87-
self.assertIn('new_version_name=1.0.0', stdout_output)
123+
self.assertIn('new_version_name=1.5', stdout_output)
88124

89125
stderr_output = mock_stderr.getvalue()
90-
self.assertIn("Try to create new version '1.0.0'", stderr_output)
91-
self.assertIn("✅ Successfully created new version '1.0.0'", stderr_output)
126+
self.assertIn("Normalized version name from '1.5.0' to '1.5'", stderr_output)
127+
self.assertIn("Try to create new version '1.5'", stderr_output)
128+
self.assertIn("✅ Successfully created new version '1.5'", stderr_output)
92129

93-
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.0.0', '--jira-url', 'https://test.jira.com'])
130+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.5.0', '--jira-url', 'https://test.jira.com'])
94131
@patch('create_jira_version.get_jira_instance')
95132
@patch('sys.stdout', new_callable=StringIO)
96133
@patch('sys.stderr', new_callable=StringIO)
@@ -105,33 +142,33 @@ def test_main_version_already_exists(self, mock_stderr, mock_stdout, mock_get_ji
105142
text="A version with this name already exists in this project."
106143
)
107144

108-
# Mock project and existing version
145+
# Mock project and existing version (normalized to 1.5)
109146
mock_project = Mock()
110147
mock_existing_version = Mock()
111148
mock_existing_version.id = '67890'
112-
mock_existing_version.name = '1.0.0'
149+
mock_existing_version.name = '1.5'
113150
mock_project.versions = [mock_existing_version]
114151
mock_jira.project.return_value = mock_project
115152

116153
mock_get_jira.return_value = mock_jira
117154

118155
main()
119156

120-
# Verify the version creation was attempted
121-
mock_jira.create_version.assert_called_once_with(name='1.0.0', project='TEST')
157+
# Verify the version creation was attempted with normalized name
158+
mock_jira.create_version.assert_called_once_with(name='1.5', project='TEST')
122159

123160
# Verify project was fetched to get existing version
124161
mock_jira.project.assert_called_once_with('TEST')
125162

126163
# Verify output
127164
stdout_output = mock_stdout.getvalue()
128165
self.assertIn('new_version_id=67890', stdout_output)
129-
self.assertIn('new_version_name=1.0.0', stdout_output)
166+
self.assertIn('new_version_name=1.5', stdout_output)
130167

131168
stderr_output = mock_stderr.getvalue()
132-
self.assertIn("Warning: Version '1.0.0' already exists. Skipping creation.", stderr_output)
169+
self.assertIn("Warning: Version '1.5' already exists. Skipping creation.", stderr_output)
133170

134-
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.0.0', '--jira-url', 'https://test.jira.com'])
171+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.5.0', '--jira-url', 'https://test.jira.com'])
135172
@patch('create_jira_version.get_jira_instance')
136173
@patch('sys.stderr', new_callable=StringIO)
137174
def test_main_version_exists_but_not_found(self, mock_stderr, mock_get_jira):
@@ -159,9 +196,9 @@ def test_main_version_exists_but_not_found(self, mock_stderr, mock_get_jira):
159196
self.assertEqual(cm.exception.code, 1)
160197

161198
stderr_output = mock_stderr.getvalue()
162-
self.assertIn("Error: Could not find existing version '1.0.0' in project.", stderr_output)
199+
self.assertIn("Error: Could not find existing version '1.5' in project.", stderr_output)
163200

164-
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.0.0', '--jira-url', 'https://test.jira.com'])
201+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.5.1', '--jira-url', 'https://test.jira.com'])
165202
@patch('create_jira_version.get_jira_instance')
166203
@patch('sys.stderr', new_callable=StringIO)
167204
def test_main_other_jira_error(self, mock_stderr, mock_get_jira):
@@ -185,5 +222,61 @@ def test_main_other_jira_error(self, mock_stderr, mock_get_jira):
185222
self.assertIn("Error: Failed to create new version. Status: 500, Text: Internal server error", stderr_output)
186223

187224

225+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.2.0', '--jira-url', 'https://test.jira.com'])
226+
@patch('create_jira_version.get_jira_instance')
227+
@patch('sys.stdout', new_callable=StringIO)
228+
@patch('sys.stderr', new_callable=StringIO)
229+
def test_main_normalizes_version_with_zero_patch(self, mock_stderr, mock_stdout, mock_get_jira):
230+
"""Test that version names with .0 patch are normalized."""
231+
# Mock JIRA instance and version
232+
mock_jira = Mock()
233+
mock_version = Mock()
234+
mock_version.id = '12345'
235+
mock_version.name = '1.2'
236+
mock_jira.create_version.return_value = mock_version
237+
mock_get_jira.return_value = mock_jira
238+
239+
main()
240+
241+
# Verify the version was created with normalized name (1.2 instead of 1.2.0)
242+
mock_jira.create_version.assert_called_once_with(name='1.2', project='TEST')
243+
244+
# Verify output shows normalized version
245+
stdout_output = mock_stdout.getvalue()
246+
self.assertIn('new_version_id=12345', stdout_output)
247+
self.assertIn('new_version_name=1.2', stdout_output)
248+
249+
stderr_output = mock_stderr.getvalue()
250+
self.assertIn("Normalized version name from '1.2.0' to '1.2'", stderr_output)
251+
self.assertIn("Try to create new version '1.2'", stderr_output)
252+
253+
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.2.3', '--jira-url', 'https://test.jira.com'])
254+
@patch('create_jira_version.get_jira_instance')
255+
@patch('sys.stdout', new_callable=StringIO)
256+
@patch('sys.stderr', new_callable=StringIO)
257+
def test_main_keeps_non_zero_patch(self, mock_stderr, mock_stdout, mock_get_jira):
258+
"""Test that version names with non-zero patch are kept as-is."""
259+
# Mock JIRA instance and version
260+
mock_jira = Mock()
261+
mock_version = Mock()
262+
mock_version.id = '12345'
263+
mock_version.name = '1.2.3'
264+
mock_jira.create_version.return_value = mock_version
265+
mock_get_jira.return_value = mock_jira
266+
267+
main()
268+
269+
# Verify the version was created with original name
270+
mock_jira.create_version.assert_called_once_with(name='1.2.3', project='TEST')
271+
272+
# Verify output shows original version
273+
stdout_output = mock_stdout.getvalue()
274+
self.assertIn('new_version_name=1.2.3', stdout_output)
275+
276+
stderr_output = mock_stderr.getvalue()
277+
# Should NOT contain normalization message
278+
self.assertNotIn("Normalized version name", stderr_output)
279+
280+
188281
if __name__ == '__main__':
189282
unittest.main()

0 commit comments

Comments
 (0)