From ca1023afc60ae69d8ee4ec60660eb163c04afe66 Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Tue, 20 Aug 2024 00:15:51 +0200 Subject: [PATCH 1/5] Tag release branch --- ddev/src/ddev/cli/release/branch/__init__.py | 2 + ddev/src/ddev/cli/release/branch/tag.py | 63 ++++++ ddev/src/ddev/utils/git.py | 17 +- ddev/tests/cli/release/branch/test_tag.py | 191 +++++++++++++++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 ddev/src/ddev/cli/release/branch/tag.py create mode 100644 ddev/tests/cli/release/branch/test_tag.py diff --git a/ddev/src/ddev/cli/release/branch/__init__.py b/ddev/src/ddev/cli/release/branch/__init__.py index d0f219a54f8f0..784d9805ebb4b 100644 --- a/ddev/src/ddev/cli/release/branch/__init__.py +++ b/ddev/src/ddev/cli/release/branch/__init__.py @@ -4,6 +4,7 @@ import click from ddev.cli.release.branch.create import create +from ddev.cli.release.branch.tag import tag @click.group(short_help='Manage release branches') @@ -14,3 +15,4 @@ def branch(): branch.add_command(create) +branch.add_command(tag) diff --git a/ddev/src/ddev/cli/release/branch/tag.py b/ddev/src/ddev/cli/release/branch/tag.py new file mode 100644 index 0000000000000..358fbdedfa0a3 --- /dev/null +++ b/ddev/src/ddev/cli/release/branch/tag.py @@ -0,0 +1,63 @@ +import re + +import click +from packaging.version import Version + +from .create import BRANCH_NAME_REGEX + + +@click.command +@click.option( + '--final/--rc', + default=False, + show_default=True, + help="Whether we're tagging the final release or a release candidate (rc).", +) +@click.pass_obj +def tag(app, final): + """ + Tag the release branch either as release candidate or final release. + """ + branch_name = app.repo.git.current_branch() + release_branch = BRANCH_NAME_REGEX.match(branch_name) + if release_branch is None: + app.abort( + f'Invalid branch name: {branch_name}. Branch name must match the pattern {BRANCH_NAME_REGEX.pattern}.' + ) + click.echo(app.repo.git.pull(branch_name)) + click.echo(app.repo.git.fetch_tags()) + major_minor_version = branch_name.replace('.x', '') + this_release_tags = sorted( + ( + Version(t) + for t in set(app.repo.git.tags(glob_pattern=major_minor_version + '.*')) + # We take 'major.minor.x' as the branch name pattern and replace 'x' with 'patch-rc.number'. + if re.match(BRANCH_NAME_REGEX.pattern.replace('x', r'\d+\-rc\.\d+'), t) + ), + reverse=True, + ) + patch_version, next_rc_guess = ( + # The first item in this_release_tags is the latest tag parsed as a Version object. + (this_release_tags[0].micro, this_release_tags[0].pre[1] + 1) + if this_release_tags + else (0, 1) + ) + if final: + new_tag = f'{major_minor_version}.{patch_version}' + else: + next_rc = click.prompt( + 'Which RC number are we tagging? (hit ENTER to accept suggestion)', type=int, default=next_rc_guess + ) + new_tag = f'{major_minor_version}.{patch_version}-rc.{next_rc}' + if Version(new_tag) in this_release_tags: + app.abort(f'Tag {new_tag} already exists. Switch to git to overwrite it.') + if next_rc < next_rc_guess: + click.secho('!!! WARNING !!!') + if not click.confirm( + 'You are about to create an RC with a number less than the latest RC number (12). Are you sure?' + ): + app.abort('Did not get confirmation, aborting. Did not create or push the tag.') + if not click.confirm(f'Create and push this tag: {new_tag}?'): + app.abort('Did not get confirmation, aborting. Did not create or push the tag.') + click.echo(app.repo.git.tag(new_tag, message=new_tag)) + click.echo(app.repo.git.push(new_tag)) diff --git a/ddev/src/ddev/utils/git.py b/ddev/src/ddev/utils/git.py index 560e1d7585650..8a80351e59b80 100644 --- a/ddev/src/ddev/utils/git.py +++ b/ddev/src/ddev/utils/git.py @@ -40,11 +40,26 @@ def latest_commit(self) -> GitCommit: sha, subject = self.capture('log', '-1', '--format=%H%n%s').splitlines() return GitCommit(sha, subject=subject) + def pull(self, ref): + return self.capture('pull', 'origin', ref) + + def push(self, ref): + return self.capture('push', 'origin', ref) + + def tag(self, value, message=None): + """ + Create a tag with an optional message. + """ + cmd = ['tag', value] + if message is not None: + cmd.extend(['--message', value]) + return self.capture(*cmd) + def tags(self, glob_pattern=None) -> list[str]: """ List the repo's tags and sort them. - If provided, `glob_pattern` filters tags just like the pattern argument to `git tag --list`. + If not None, we pass `glob_pattern` as the pattern argument to `git tag --list`. """ cmd = ['tag', '--list'] diff --git a/ddev/tests/cli/release/branch/test_tag.py b/ddev/tests/cli/release/branch/test_tag.py new file mode 100644 index 0000000000000..28da1f70c8947 --- /dev/null +++ b/ddev/tests/cli/release/branch/test_tag.py @@ -0,0 +1,191 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from unittest.mock import call as c + +import pytest + +from ddev.utils.git import GitRepository + +NO_CONFIRMATION_SO_ABORT = 'Did not get confirmation, aborting. Did not create or push the tag.' + + +@pytest.fixture +def basic_git(mocker): + mock_git = mocker.create_autospec(GitRepository) + # We're patching the constructor (__new__) method of GitRepository class. + # That's why we need a function that returns the mock. + mocker.patch('ddev.repo.core.GitRepository', lambda _: mock_git) + return mock_git + + +@pytest.fixture +def git(basic_git): + basic_git.current_branch.return_value = '7.56.x' + basic_git.tags.return_value = [ + # interesting phenomena: + # - skipping RCs + # - rc 11 naively would be sorted as less than rc 2 + # - an rc tag from dbm + # '7.56.0', + '7.56.0-rc.1', + '7.56.0-rc.1-dbm-agent-jobs', + '7.56.0-rc.11', + '7.56.0-rc.2', + '7.56.0-rc.5', + '7.56.0-rc.6', + '7.56.0-rc.7', + '7.56.0-rc.8', + ] + yield basic_git + assert basic_git.method_calls[:4] == [ + c.current_branch(), + c.pull('7.56.x'), + c.fetch_tags(), + c.tags(glob_pattern='7.56.*'), + ] + + +def _assert_tag_pushed(git, result, tag): + assert result.exit_code == 0, result.output + assert git.method_calls[-2:] == [ + c.tag(tag, message=tag), + c.push(tag), + ] + assert f'Create and push this tag: {tag}?' in result.output + + +def test_wrong_branch(ddev, basic_git): + """ + Given a branch that doesn't match the release branch pattern we should abort. + """ + name = 'foo' + basic_git.current_branch.return_value = name + + result = ddev('release', 'branch', 'tag') + + assert result.exit_code == 1, result.output + assert rf'Invalid branch name: {name}. Branch name must match the pattern ^\d+\.\d+\.x$' in result.output + assert basic_git.method_calls == [c.current_branch()] + + +def test_middle_of_release_next_rc(ddev, git): + """ + We're in the middle of a release, some RCs are already done. We want to create the next RC. + """ + result = ddev('release', 'branch', 'tag', input='\ny\n') + + _assert_tag_pushed(git, result, '7.56.0-rc.12') + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + + +@pytest.mark.parametrize( + 'no_confirm', + [ + pytest.param('n', id='explicit abort'), + pytest.param('', id='abort by default'), + pytest.param('x', id='abort on any other input'), + ], +) +@pytest.mark.parametrize('rc_num', ['3', '10']) +def test_do_not_confirm_non_sequential_rc(ddev, git, rc_num, no_confirm): + """ + We're in the middle of a release, some RCs are already done. User wants to create the next RC. + + However the user asks to create an RC that's less than the latest RC number. + This is unusual, so we give a warning and ask user to confirm. If they don't, we abort. + + Important: we are not overwriting an existing RC tag. + """ + + result = ddev('release', 'branch', 'tag', input=f'{rc_num}\n{no_confirm}\n') + + assert result.exit_code == 1, result.output + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert ( + '!!! WARNING !!!\n' + 'You are about to create an RC with a number less than the latest RC number (12). Are you sure? [y/N]' + ) in result.output + assert NO_CONFIRMATION_SO_ABORT in result.output + + +@pytest.mark.parametrize('rc_num', ['3', '10']) +def test_confirm_non_sequential_rc(ddev, git, rc_num): + """ + We're in the middle of a release, some RCs are already done. User wants to create the next RC. + + However the user asks to create an RC that's less than the latest RC number. + This is unusual, so we give a warning and ask user to confirm. If they do, we create the tag. + + Important: we are not overwriting an existing RC tag. + """ + result = ddev('release', 'branch', 'tag', input=f'{rc_num}\ny\ny\n') + + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert ( + '!!! WARNING !!!\n' + 'You are about to create an RC with a number less than the latest RC number (12). Are you sure? [y/N]' + ) in result.output + _assert_tag_pushed(git, result, f'7.56.0-rc.{rc_num}') + + +@pytest.mark.parametrize('rc_num', ['1', '5', '11']) +def test_abort_if_rc_tag_exists(ddev, git, rc_num): + """ + We're in the middle of a release, some RCs are already done. User wants to create the next RC. + + However the user asks to create an RC for which we already have a tag. This requires special git flags to clobber + the local and remote tags. To keep our logic simple we give up here and leave a hint how the user can proceed. + """ + + result = ddev('release', 'branch', 'tag', input=f'{rc_num}\ny\n') + + assert result.exit_code == 1, result.output + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert (f'Tag 7.56.0-rc.{rc_num} already exists. Switch to git to overwrite it.') in result.output + + +@pytest.mark.parametrize( + 'no_confirm', + [ + pytest.param('n', id='explicit abort'), + pytest.param('', id='abort by default'), + pytest.param('x', id='abort on any other input'), + ], +) +def test_abort_valid_rc(ddev, git, no_confirm): + """ + The RC is fine but we don't confirm in the end, so we abort. + """ + git.tags.return_value = [] + + result = ddev('release', 'branch', 'tag', input='\n{no_confirm}\n') + + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [1]' in result.output + assert result.exit_code == 1, result.output + assert NO_CONFIRMATION_SO_ABORT in result.output + + +def test_first_rc(ddev, git): + """ + First RC for a new release. + add some more examples? + should be ok to specify a number other than 1 + """ + git.tags.return_value = [] + + result = ddev('release', 'branch', 'tag', input='\ny\n') + + _assert_tag_pushed(git, result, '7.56.0-rc.1') + assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [1]' in result.output + + +def test_final(ddev, git): + """ + Create final release tag. + + We should handle the case of bugfix releases here too + """ + result = ddev('release', 'branch', 'tag', '--final', input='y\n') + + _assert_tag_pushed(git, result, '7.56.0') From 161a41559ebd3b38cdbe135d1b19cd4635a175c7 Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Mon, 26 Aug 2024 12:00:27 +0200 Subject: [PATCH 2/5] add changelog --- ddev/changelog.d/18413.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 ddev/changelog.d/18413.added diff --git a/ddev/changelog.d/18413.added b/ddev/changelog.d/18413.added new file mode 100644 index 0000000000000..462be04dff5f6 --- /dev/null +++ b/ddev/changelog.d/18413.added @@ -0,0 +1 @@ +Add command to tag the Agent release branch. It supports both RC and final tags, see `ddev release branch tag --help` for more details. From d298cfea29ff03ea95fcf7b9d111f4f1dbacb834 Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Wed, 28 Aug 2024 10:31:36 +0200 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: dkirov-dd <166512750+dkirov-dd@users.noreply.github.com> --- ddev/src/ddev/cli/release/branch/tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddev/src/ddev/cli/release/branch/tag.py b/ddev/src/ddev/cli/release/branch/tag.py index 358fbdedfa0a3..7398f49ad5977 100644 --- a/ddev/src/ddev/cli/release/branch/tag.py +++ b/ddev/src/ddev/cli/release/branch/tag.py @@ -46,7 +46,7 @@ def tag(app, final): new_tag = f'{major_minor_version}.{patch_version}' else: next_rc = click.prompt( - 'Which RC number are we tagging? (hit ENTER to accept suggestion)', type=int, default=next_rc_guess + 'What RC number should be tagged? (hit ENTER to accept suggestion)', type=int, default=next_rc_guess ) new_tag = f'{major_minor_version}.{patch_version}-rc.{next_rc}' if Version(new_tag) in this_release_tags: From 037ef1aab744065e851e488f4d4373e670657752 Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Wed, 4 Sep 2024 21:54:11 +0200 Subject: [PATCH 4/5] Some more feedback, fixes, features --- ddev/src/ddev/cli/release/branch/tag.py | 36 ++++--- ddev/tests/cli/release/branch/test_tag.py | 109 ++++++++++++++-------- 2 files changed, 96 insertions(+), 49 deletions(-) diff --git a/ddev/src/ddev/cli/release/branch/tag.py b/ddev/src/ddev/cli/release/branch/tag.py index 7398f49ad5977..70be246f1cd03 100644 --- a/ddev/src/ddev/cli/release/branch/tag.py +++ b/ddev/src/ddev/cli/release/branch/tag.py @@ -32,32 +32,44 @@ def tag(app, final): Version(t) for t in set(app.repo.git.tags(glob_pattern=major_minor_version + '.*')) # We take 'major.minor.x' as the branch name pattern and replace 'x' with 'patch-rc.number'. - if re.match(BRANCH_NAME_REGEX.pattern.replace('x', r'\d+\-rc\.\d+'), t) + # We make the RC component optional so that final tags also match our filter. + if re.match(BRANCH_NAME_REGEX.pattern.replace('x', r'\d+(\-rc\.\d+)?'), t) ), reverse=True, ) - patch_version, next_rc_guess = ( - # The first item in this_release_tags is the latest tag parsed as a Version object. - (this_release_tags[0].micro, this_release_tags[0].pre[1] + 1) - if this_release_tags - else (0, 1) - ) + last_patch, last_rc = _extract_patch_and_rc(this_release_tags) + last_tag_was_final = last_rc is None + new_patch = last_patch + 1 if last_tag_was_final else last_patch if final: - new_tag = f'{major_minor_version}.{patch_version}' + new_tag = f'{major_minor_version}.{new_patch}' else: + new_rc_guess = 1 if last_tag_was_final else last_rc + 1 next_rc = click.prompt( - 'What RC number should be tagged? (hit ENTER to accept suggestion)', type=int, default=next_rc_guess + 'What RC number are we tagging? (hit ENTER to accept suggestion)', type=int, default=new_rc_guess ) - new_tag = f'{major_minor_version}.{patch_version}-rc.{next_rc}' + if next_rc < 1: + app.abort('RC number must be at least 1.') + new_tag = f'{major_minor_version}.{new_patch}-rc.{next_rc}' if Version(new_tag) in this_release_tags: app.abort(f'Tag {new_tag} already exists. Switch to git to overwrite it.') - if next_rc < next_rc_guess: + if not last_tag_was_final and next_rc < last_rc: click.secho('!!! WARNING !!!') if not click.confirm( - 'You are about to create an RC with a number less than the latest RC number (12). Are you sure?' + f'The latest RC is {last_rc}. ' + 'You are about to go back in time by creating an RC with a number less than that. Are you sure? [y/N]' ): app.abort('Did not get confirmation, aborting. Did not create or push the tag.') if not click.confirm(f'Create and push this tag: {new_tag}?'): app.abort('Did not get confirmation, aborting. Did not create or push the tag.') click.echo(app.repo.git.tag(new_tag, message=new_tag)) click.echo(app.repo.git.push(new_tag)) + + +def _extract_patch_and_rc(version_tags): + if not version_tags: + return 0, 0 + latest = version_tags[0] + patch = latest.micro + # latest.pre is None for final releases and a tuple ('rc', ) for RC. + rc = latest.pre if latest.pre is None else latest.pre[1] + return patch, rc diff --git a/ddev/tests/cli/release/branch/test_tag.py b/ddev/tests/cli/release/branch/test_tag.py index 28da1f70c8947..593bfa1e6cb51 100644 --- a/ddev/tests/cli/release/branch/test_tag.py +++ b/ddev/tests/cli/release/branch/test_tag.py @@ -8,12 +8,28 @@ from ddev.utils.git import GitRepository NO_CONFIRMATION_SO_ABORT = 'Did not get confirmation, aborting. Did not create or push the tag.' +RC_NUMBER_PROMPT = 'What RC number are we tagging? (hit ENTER to accept suggestion) [{}]' + +EXAMPLE_TAGS = [ + '7.56.0-rc.1', + # Random RC tag from DBM. We should make sure we ignore it. + '7.56.0-rc.1-dbm-agent-jobs', + # Icluding RC 11 is interesting because it makes sure we we parse the versions before we sort them. + # The naive sort will think RC 11 is earlier than RC 2. + '7.56.0-rc.11', + '7.56.0-rc.2', + # Skipping RCs, we go from 2 to 5. + '7.56.0-rc.5', + '7.56.0-rc.6', + '7.56.0-rc.7', + '7.56.0-rc.8', +] @pytest.fixture def basic_git(mocker): mock_git = mocker.create_autospec(GitRepository) - # We're patching the constructor (__new__) method of GitRepository class. + # We're patching the creation of the GitRepository class. # That's why we need a function that returns the mock. mocker.patch('ddev.repo.core.GitRepository', lambda _: mock_git) return mock_git @@ -22,21 +38,7 @@ def basic_git(mocker): @pytest.fixture def git(basic_git): basic_git.current_branch.return_value = '7.56.x' - basic_git.tags.return_value = [ - # interesting phenomena: - # - skipping RCs - # - rc 11 naively would be sorted as less than rc 2 - # - an rc tag from dbm - # '7.56.0', - '7.56.0-rc.1', - '7.56.0-rc.1-dbm-agent-jobs', - '7.56.0-rc.11', - '7.56.0-rc.2', - '7.56.0-rc.5', - '7.56.0-rc.6', - '7.56.0-rc.7', - '7.56.0-rc.8', - ] + basic_git.tags.return_value = EXAMPLE_TAGS[:] yield basic_git assert basic_git.method_calls[:4] == [ c.current_branch(), @@ -76,7 +78,7 @@ def test_middle_of_release_next_rc(ddev, git): result = ddev('release', 'branch', 'tag', input='\ny\n') _assert_tag_pushed(git, result, '7.56.0-rc.12') - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert RC_NUMBER_PROMPT.format('12') in result.output @pytest.mark.parametrize( @@ -88,7 +90,8 @@ def test_middle_of_release_next_rc(ddev, git): ], ) @pytest.mark.parametrize('rc_num', ['3', '10']) -def test_do_not_confirm_non_sequential_rc(ddev, git, rc_num, no_confirm): +@pytest.mark.parametrize('last_rc', [11, 12]) +def test_do_not_confirm_non_sequential_rc(ddev, git, rc_num, no_confirm, last_rc): """ We're in the middle of a release, some RCs are already done. User wants to create the next RC. @@ -98,14 +101,16 @@ def test_do_not_confirm_non_sequential_rc(ddev, git, rc_num, no_confirm): Important: we are not overwriting an existing RC tag. """ + git.tags.return_value.append(f'7.56.0-rc.{last_rc}') result = ddev('release', 'branch', 'tag', input=f'{rc_num}\n{no_confirm}\n') - assert result.exit_code == 1, result.output - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert RC_NUMBER_PROMPT.format(str(last_rc + 1)) in result.output assert ( '!!! WARNING !!!\n' - 'You are about to create an RC with a number less than the latest RC number (12). Are you sure? [y/N]' + f'The latest RC is {last_rc}. You are about to go back in time by creating an RC with a number less than that. ' + 'Are you sure? [y/N]' ) in result.output + assert result.exit_code == 1, result.output assert NO_CONFIRMATION_SO_ABORT in result.output @@ -121,10 +126,11 @@ def test_confirm_non_sequential_rc(ddev, git, rc_num): """ result = ddev('release', 'branch', 'tag', input=f'{rc_num}\ny\ny\n') - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output + assert RC_NUMBER_PROMPT.format('12') in result.output assert ( '!!! WARNING !!!\n' - 'You are about to create an RC with a number less than the latest RC number (12). Are you sure? [y/N]' + 'The latest RC is 11. You are about to go back in time by creating an RC with a number less than that. ' + 'Are you sure? [y/N]' ) in result.output _assert_tag_pushed(git, result, f'7.56.0-rc.{rc_num}') @@ -141,8 +147,19 @@ def test_abort_if_rc_tag_exists(ddev, git, rc_num): result = ddev('release', 'branch', 'tag', input=f'{rc_num}\ny\n') assert result.exit_code == 1, result.output - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [12]' in result.output - assert (f'Tag 7.56.0-rc.{rc_num} already exists. Switch to git to overwrite it.') in result.output + assert RC_NUMBER_PROMPT.format('12') in result.output + assert f'Tag 7.56.0-rc.{rc_num} already exists. Switch to git to overwrite it.' in result.output + + +def test_abort_if_tag_less_than_one(ddev, git): + """ + RC numbers less than 1 don't make any sense, so we abort if we get one. + """ + result = ddev('release', 'branch', 'tag', input='0\ny\n') + + assert RC_NUMBER_PROMPT.format('12') in result.output + assert result.exit_code == 1, result.output + assert 'RC number must be at least 1.' in result.output @pytest.mark.parametrize( @@ -161,31 +178,49 @@ def test_abort_valid_rc(ddev, git, no_confirm): result = ddev('release', 'branch', 'tag', input='\n{no_confirm}\n') - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [1]' in result.output + assert RC_NUMBER_PROMPT.format('1') in result.output assert result.exit_code == 1, result.output assert NO_CONFIRMATION_SO_ABORT in result.output -def test_first_rc(ddev, git): +@pytest.mark.parametrize( + 'rc_num_input, rc_num', + [ + pytest.param('', '1', id='implicit sequential'), + pytest.param('', '1', id='explicit sequential'), + pytest.param('2', '2', id='explicit non-sequential'), + ], +) +@pytest.mark.parametrize('tags, patch', [([], '0'), (EXAMPLE_TAGS + ['7.56.0'], '1')]) +def test_first_rc(ddev, git, rc_num_input, rc_num, tags, patch): """ First RC for a new release. - add some more examples? - should be ok to specify a number other than 1 + + We support starting with a number other than 1, though that's very unlikely to happen in practice. """ - git.tags.return_value = [] + git.tags.return_value = tags - result = ddev('release', 'branch', 'tag', input='\ny\n') + result = ddev('release', 'branch', 'tag', input=f'{rc_num_input}\ny\n') - _assert_tag_pushed(git, result, '7.56.0-rc.1') - assert 'Which RC number are we tagging? (hit ENTER to accept suggestion) [1]' in result.output + _assert_tag_pushed(git, result, f'7.56.{patch}-rc.{rc_num}') + assert RC_NUMBER_PROMPT.format('1') in result.output -def test_final(ddev, git): +@pytest.mark.parametrize( + 'latest_final_tag, expected_new_final_tag', + [ + pytest.param('', '7.56.0', id='no final tag yet'), + pytest.param('7.56.0', '7.56.1', id='final tag present, so we are making a bugfix release'), + ], +) +def test_final(ddev, git, latest_final_tag, expected_new_final_tag): """ Create final release tag. - - We should handle the case of bugfix releases here too """ + git.tags.return_value.append(latest_final_tag) result = ddev('release', 'branch', 'tag', '--final', input='y\n') - _assert_tag_pushed(git, result, '7.56.0') + _assert_tag_pushed(git, result, expected_new_final_tag) + + +# TODO: test for adding RCs for a bugfix release From f12f40ac7ecba072c5192bb55ac0185d491cef7e Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Thu, 5 Sep 2024 06:40:10 +0200 Subject: [PATCH 5/5] reduce duplication and complexity --- ddev/src/ddev/cli/release/branch/tag.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ddev/src/ddev/cli/release/branch/tag.py b/ddev/src/ddev/cli/release/branch/tag.py index 70be246f1cd03..1dcf2c0e2dc22 100644 --- a/ddev/src/ddev/cli/release/branch/tag.py +++ b/ddev/src/ddev/cli/release/branch/tag.py @@ -40,16 +40,15 @@ def tag(app, final): last_patch, last_rc = _extract_patch_and_rc(this_release_tags) last_tag_was_final = last_rc is None new_patch = last_patch + 1 if last_tag_was_final else last_patch - if final: - new_tag = f'{major_minor_version}.{new_patch}' - else: + new_tag = f'{major_minor_version}.{new_patch}' + if not final: new_rc_guess = 1 if last_tag_was_final else last_rc + 1 next_rc = click.prompt( 'What RC number are we tagging? (hit ENTER to accept suggestion)', type=int, default=new_rc_guess ) if next_rc < 1: app.abort('RC number must be at least 1.') - new_tag = f'{major_minor_version}.{new_patch}-rc.{next_rc}' + new_tag += f'-rc.{next_rc}' if Version(new_tag) in this_release_tags: app.abort(f'Tag {new_tag} already exists. Switch to git to overwrite it.') if not last_tag_was_final and next_rc < last_rc: