Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions create-jira-version/create_jira_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@

import argparse
import os
import re
import sys
from jira import JIRA
from jira.exceptions import JIRAError


def normalize_version_name(version_name):
"""
Normalizes a version name by removing .0 patch suffix.
For example: 1.2.0 -> 1.2, but 1.2.4 stays as 1.2.4
"""
# Match versions ending in .0 (e.g., 1.2.0, 10.20.0)
match = re.match(r'^(.+)\.0$', version_name)
if match:
return match.group(1)
return version_name


# noinspection DuplicatedCode
def eprint(*args, **kwargs):
"""Prints messages to the standard error stream (stderr) for logging."""
Expand Down Expand Up @@ -47,6 +60,27 @@ def get_jira_instance(jira_url):
sys.exit(1)


def find_existing_version(jira, project_key, version_name):
"""Finds and returns an existing version by name in a project."""
project = jira.project(project_key)
for version in project.versions:
if version.name == version_name:
return version
return None


def handle_existing_version(jira, project_key, version_name):
"""Handles the case when a version already exists."""
eprint(f"Warning: Version '{version_name}' already exists. Skipping creation.")
existing_version = find_existing_version(jira, project_key, version_name)
if existing_version:
print(f"new_version_id={existing_version.id}")
print(f"new_version_name={existing_version.name}")
else:
eprint(f"Error: Could not find existing version '{version_name}' in project.")
sys.exit(1)


def main():
"""Main function to orchestrate the creation process."""
parser = argparse.ArgumentParser(
Expand All @@ -60,32 +94,21 @@ def main():

jira = get_jira_instance(args.jira_url)

eprint(f"Try to create new version '{args.version_name}'")
version_name = normalize_version_name(args.version_name)
if version_name != args.version_name:
eprint(f"Normalized version name from '{args.version_name}' to '{version_name}'")

eprint(f"Try to create new version '{version_name}'")
try:
new_version = jira.create_version(name=args.version_name, project=args.project_key)
new_version = jira.create_version(name=version_name, project=args.project_key)
eprint(f"✅ Successfully created new version '{new_version.name}'")

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

except JIRAError as e:
if "A version with this name already exists" in e.text:
eprint(f"Warning: Version '{args.version_name}' already exists. Skipping creation.")

# Fetch the existing version details
project = jira.project(args.project_key)
existing_version = None
for version in project.versions:
if version.name == args.version_name:
existing_version = version
break

if existing_version:
print(f"new_version_id={existing_version.id}")
print(f"new_version_name={existing_version.name}")
else:
eprint(f"Error: Could not find existing version '{args.version_name}' in project.")
sys.exit(1)
handle_existing_version(jira, args.project_key, version_name)
else:
eprint(f"Error: Failed to create new version. Status: {e.status_code}, Text: {e.text}")
sys.exit(1)
Expand Down
129 changes: 111 additions & 18 deletions create-jira-version/test_create_jira_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,46 @@
# Add the current directory to the path to import our module
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from create_jira_version import get_jira_instance, main
from create_jira_version import get_jira_instance, main, normalize_version_name
from jira.exceptions import JIRAError


class TestNormalizeVersionName(unittest.TestCase):
"""Tests for the normalize_version_name function."""

def test_removes_zero_patch(self):
"""Test that .0 patch is removed from version names."""
self.assertEqual(normalize_version_name('1.2.0'), '1.2')

def test_keeps_non_zero_patch(self):
"""Test that non-zero patches are kept."""
self.assertEqual(normalize_version_name('1.2.4'), '1.2.4')
self.assertEqual(normalize_version_name('1.2.1'), '1.2.1')
self.assertEqual(normalize_version_name('10.20.30'), '10.20.30')

def test_handles_two_part_versions(self):
"""Test that two-part versions are unchanged."""
self.assertEqual(normalize_version_name('1.2'), '1.2')

def test_handles_single_zero(self):
"""Test edge case with just 0."""
self.assertEqual(normalize_version_name('0'), '0')

def test_handles_version_with_prefix(self):
"""Test versions with prefixes like 'v'."""
self.assertEqual(normalize_version_name('v1.2.0'), 'v1.2')
self.assertEqual(normalize_version_name('v1.2.4'), 'v1.2.4')

def test_handles_four_part_version_with_zero(self):
"""Test four-part versions ending in .0."""
self.assertEqual(normalize_version_name('1.2.3.0'), '1.2.3')

def test_handles_multiple_zeros(self):
"""Test versions with multiple zeros."""
self.assertEqual(normalize_version_name('1.0.0'), '1.0')
self.assertEqual(normalize_version_name('0.0.0'), '0.0')


class TestCreateJiraVersion(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -59,7 +95,7 @@ def test_get_jira_instance_auth_failure(self, mock_jira_class):
get_jira_instance('https://prod.com')
self.assertEqual(cm.exception.code, 1)

@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.0.0', '--jira-url', 'https://test.jira.com'])
@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.5.0', '--jira-url', 'https://test.jira.com'])
@patch('create_jira_version.get_jira_instance')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.stderr', new_callable=StringIO)
Expand All @@ -69,7 +105,7 @@ def test_main_successful_version_creation(self, mock_stderr, mock_stdout, mock_g
mock_jira = Mock()
mock_version = Mock()
mock_version.id = '12345'
mock_version.name = '1.0.0'
mock_version.name = '1.5'
mock_jira.create_version.return_value = mock_version
mock_get_jira.return_value = mock_jira

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

# Verify the version was created with correct parameters
mock_jira.create_version.assert_called_once_with(name='1.0.0', project='TEST')
# Verify the version was created with normalized name (1.5 instead of 1.5.0)
mock_jira.create_version.assert_called_once_with(name='1.5', project='TEST')

# Verify output
stdout_output = mock_stdout.getvalue()
self.assertIn('new_version_id=12345', stdout_output)
self.assertIn('new_version_name=1.0.0', stdout_output)
self.assertIn('new_version_name=1.5', stdout_output)

stderr_output = mock_stderr.getvalue()
self.assertIn("Try to create new version '1.0.0'", stderr_output)
self.assertIn("✅ Successfully created new version '1.0.0'", stderr_output)
self.assertIn("Normalized version name from '1.5.0' to '1.5'", stderr_output)
self.assertIn("Try to create new version '1.5'", stderr_output)
self.assertIn("✅ Successfully created new version '1.5'", stderr_output)

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

# Mock project and existing version
# Mock project and existing version (normalized to 1.5)
mock_project = Mock()
mock_existing_version = Mock()
mock_existing_version.id = '67890'
mock_existing_version.name = '1.0.0'
mock_existing_version.name = '1.5'
mock_project.versions = [mock_existing_version]
mock_jira.project.return_value = mock_project

mock_get_jira.return_value = mock_jira

main()

# Verify the version creation was attempted
mock_jira.create_version.assert_called_once_with(name='1.0.0', project='TEST')
# Verify the version creation was attempted with normalized name
mock_jira.create_version.assert_called_once_with(name='1.5', project='TEST')

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

# Verify output
stdout_output = mock_stdout.getvalue()
self.assertIn('new_version_id=67890', stdout_output)
self.assertIn('new_version_name=1.0.0', stdout_output)
self.assertIn('new_version_name=1.5', stdout_output)

stderr_output = mock_stderr.getvalue()
self.assertIn("Warning: Version '1.0.0' already exists. Skipping creation.", stderr_output)
self.assertIn("Warning: Version '1.5' already exists. Skipping creation.", stderr_output)

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

stderr_output = mock_stderr.getvalue()
self.assertIn("Error: Could not find existing version '1.0.0' in project.", stderr_output)
self.assertIn("Error: Could not find existing version '1.5' in project.", stderr_output)

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


@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.2.0', '--jira-url', 'https://test.jira.com'])
@patch('create_jira_version.get_jira_instance')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.stderr', new_callable=StringIO)
def test_main_normalizes_version_with_zero_patch(self, mock_stderr, mock_stdout, mock_get_jira):
"""Test that version names with .0 patch are normalized."""
# Mock JIRA instance and version
mock_jira = Mock()
mock_version = Mock()
mock_version.id = '12345'
mock_version.name = '1.2'
mock_jira.create_version.return_value = mock_version
mock_get_jira.return_value = mock_jira

main()

# Verify the version was created with normalized name (1.2 instead of 1.2.0)
mock_jira.create_version.assert_called_once_with(name='1.2', project='TEST')

# Verify output shows normalized version
stdout_output = mock_stdout.getvalue()
self.assertIn('new_version_id=12345', stdout_output)
self.assertIn('new_version_name=1.2', stdout_output)

stderr_output = mock_stderr.getvalue()
self.assertIn("Normalized version name from '1.2.0' to '1.2'", stderr_output)
self.assertIn("Try to create new version '1.2'", stderr_output)

@patch('sys.argv', ['create_jira_version.py', '--project-key', 'TEST', '--version-name', '1.2.3', '--jira-url', 'https://test.jira.com'])
@patch('create_jira_version.get_jira_instance')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.stderr', new_callable=StringIO)
def test_main_keeps_non_zero_patch(self, mock_stderr, mock_stdout, mock_get_jira):
"""Test that version names with non-zero patch are kept as-is."""
# Mock JIRA instance and version
mock_jira = Mock()
mock_version = Mock()
mock_version.id = '12345'
mock_version.name = '1.2.3'
mock_jira.create_version.return_value = mock_version
mock_get_jira.return_value = mock_jira

main()

# Verify the version was created with original name
mock_jira.create_version.assert_called_once_with(name='1.2.3', project='TEST')

# Verify output shows original version
stdout_output = mock_stdout.getvalue()
self.assertIn('new_version_name=1.2.3', stdout_output)

stderr_output = mock_stderr.getvalue()
# Should NOT contain normalization message
self.assertNotIn("Normalized version name", stderr_output)


if __name__ == '__main__':
unittest.main()
Loading