From a045f9c25be22949d4420c20821ef22f35a92d63 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 30 Nov 2023 13:18:54 -0500 Subject: [PATCH] Handle major versions and non-rc prereleases This commit adds support to the qiskit-bot release automation to handle major version releases and non-rc pre-releases. The major version handling is primarily for building the log command, as we need to get the log from 'n.0.0..n-1.m.0' where m is the most recent non-prerelease. To handle non-rc prereleases the only change is we don't create a new branch for the pre-release. Previously all pre-releases would trigger branch creation (given the appropriate configuration variable). But the desired behavior is to actually only create a stable branch from a release candidate. --- qiskit_bot/git.py | 14 ++++ qiskit_bot/release_process.py | 24 ++++-- tests/test_release_process.py | 146 ++++++++++++++++++++++++++++++++-- 3 files changed, 172 insertions(+), 12 deletions(-) diff --git a/qiskit_bot/git.py b/qiskit_bot/git.py index fd211ca..acef4b7 100644 --- a/qiskit_bot/git.py +++ b/qiskit_bot/git.py @@ -84,6 +84,20 @@ def get_git_log(repo, sha1): return '' +def get_tags(repo): + """Get a list of tags in creation order separated by newlines.""" + LOG.info('Querying git tags for %s' % repo.local_path) + try: + res = subprocess.run(['git', 'tag', '--sort=-creatordate'], + capture_output=True, check=True, encoding="UTF8", + cwd=repo.local_path) + return res.stdout + except subprocess.CalledProcessError as e: + LOG.exception('Failed to get git log\nstdout:\n%s\nstderr:\n%s' + % (e.stdout, e.stderr)) + return '' + + def clean_repo(repo): cmd = ['git', 'clean', '-fdX'] try: diff --git a/qiskit_bot/release_process.py b/qiskit_bot/release_process.py index 38838de..6f92f07 100644 --- a/qiskit_bot/release_process.py +++ b/qiskit_bot/release_process.py @@ -216,7 +216,7 @@ def create_github_release(repo, log_string, version_number, categories, prerelease=prerelease) -def _get_log_string(version_obj): +def _get_log_string(version_obj, version_number, repo): # If a second prerelease show log from first if version_obj.is_prerelease and version_obj.pre[1] > 1: old_version = ( @@ -230,9 +230,19 @@ def _get_log_string(version_obj): f"{version_obj.micro - 1}" ) # If a minor release log between 0.X.0..0.X-1.0 + elif version_obj.major >= 1 and version_obj.minor == 0: + tags = git.get_tags(repo) + previous_major = version_obj.major - 1 + for tag in tags.splitlines(): + tag_version = parse(tag) + if tag_version.is_prerelease: + continue + if tag_version.major == previous_major: + old_version = tag + break else: old_version = f"{version_obj.epoch}.{version_obj.minor - 1}.0" - return f"{version_obj}...{old_version}" + return f"{version_number}...{old_version}" # This helper function must be a top-level function to be pickable for @@ -242,7 +252,7 @@ def _finish_release__changelog_process( ): with fasteners.InterProcessLock(os.path.join(lock_dir, repo.name)): git.checkout_default_branch(repo, pull=True) - log_string = _get_log_string(version_obj) + log_string = _get_log_string(version_obj, version_number, repo) categories = repo.get_local_config().get( 'categories', config.default_changelog_categories) create_github_release(repo, log_string, version_number, @@ -278,8 +288,12 @@ def finish_release(version_number, repo, conf, meta_repo): repo_branches = [x.name for x in repo.gh_repo.get_branches()] if int(version_number_pieces[2]) == 0 and \ branch_name not in repo_branches: - git.checkout_default_branch(repo, pull=True) - git.create_branch(branch_name, version_number, repo, push=True) + if is_prerelease and "rc" not in version_number: + pass + else: + git.checkout_default_branch(repo, pull=True) + git.create_branch(branch_name, version_number, repo, + push=True) if not is_postrelease: _changelog_process = partial( diff --git a/tests/test_release_process.py b/tests/test_release_process.py index 2b1a601..b5e1c8c 100644 --- a/tests/test_release_process.py +++ b/tests/test_release_process.py @@ -840,27 +840,49 @@ def test_bump_minor_release_from_pending_patch_release_pr(self, def test_get_log_string(self): version_obj = parse("0.10.2") self.assertEqual('0.10.2...0.10.1', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.10.2", + unittest.mock.MagicMock() + )) version_obj = parse("0.3.0") self.assertEqual('0.3.0...0.2.0', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.3.0", + unittest.mock.MagicMock())) version_obj = parse("0.3.25") self.assertEqual('0.3.25...0.3.24', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.3.25", + unittest.mock.MagicMock())) version_obj = parse("0.25.0") self.assertEqual('0.25.0...0.24.0', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.25.0", + unittest.mock.MagicMock())) def test_get_log_string_prerelease(self): version_obj = parse("0.25.0rc1") self.assertEqual('0.25.0rc1...0.24.0', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.25.0rc1", + unittest.mock.MagicMock())) version_obj = parse("0.25.0rc2") self.assertEqual('0.25.0rc2...0.25.0rc1', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.25.0rc2", + unittest.mock.MagicMock())) version_obj = parse("0.25.0b1") self.assertEqual('0.25.0b1...0.24.0', - release_process._get_log_string(version_obj)) + release_process._get_log_string( + version_obj, + "0.25.0b1", + unittest.mock.MagicMock())) @unittest.mock.patch.object(release_process, 'git') @unittest.mock.patch.object(release_process, 'create_github_release') @@ -1755,6 +1777,116 @@ def test_finish_prerelease_with_pre_existing_branch(self, bump_meta_mock, config.default_changelog_categories, True) git_mock.create_branch.assert_not_called() + @unittest.mock.patch.object(release_process, 'git') + @unittest.mock.patch.object(release_process, 'create_github_release') + @unittest.mock.patch.object(release_process, 'bump_meta') + def test_finish_prerelease_non_rc(self, bump_meta_mock, + github_release_mock, git_mock): + meta_repo = unittest.mock.MagicMock() + meta_repo.repo_config = {} + meta_repo.name = 'qiskit' + repo = unittest.mock.MagicMock() + repo.name = 'qiskit-terra' + repo.repo_config = {'branch_on_release': True} + repo.get_local_config = lambda: {} + conf = {'working_dir': self.temp_dir.path} + with unittest.mock.patch.object( + release_process, 'multiprocessing' + ) as mp_mock: + mp_mock.Process = ProcessMock + release_process.finish_release('0.12.0pre1', repo, conf, meta_repo) + git_mock.create_branch.assert_not_called() + github_release_mock.assert_called_once_with( + repo, '0.12.0pre1...0.11.0', '0.12.0pre1', + config.default_changelog_categories, True) + bump_meta_mock.assert_not_called() + + @unittest.mock.patch.object(release_process, 'git') + @unittest.mock.patch.object(release_process, 'create_github_release') + @unittest.mock.patch.object(release_process, 'bump_meta') + def test_finish_major_version_prerelease_non_rc(self, bump_meta_mock, + github_release_mock, + git_mock): + meta_repo = unittest.mock.MagicMock() + meta_repo.repo_config = {} + meta_repo.name = 'qiskit' + repo = unittest.mock.MagicMock() + repo.name = 'qiskit-terra' + repo.repo_config = {'branch_on_release': True} + repo.get_local_config = lambda: {} + conf = {'working_dir': self.temp_dir.path} + + def tag_history(*args, **kwargs): + return """1.0.0pre1 +0.45.1 +0.45.0 +0.25.3 +0.45.0rc1 +0.25.2.1 +0.25.2 +0.25.1 +0.25.0 +0.25.0rc1 +0.24.2 +0.24.1 +""" + git_mock.get_tags = tag_history + with unittest.mock.patch.object( + release_process, 'multiprocessing' + ) as mp_mock: + mp_mock.Process = ProcessMock + release_process.finish_release('1.0.0pre1', repo, conf, meta_repo) + git_mock.create_branch.assert_not_called() + github_release_mock.assert_called_once_with( + repo, '1.0.0pre1...0.45.1', '1.0.0pre1', + config.default_changelog_categories, True) + bump_meta_mock.assert_not_called() + + @unittest.mock.patch.object(release_process, 'git') + @unittest.mock.patch.object(release_process, 'create_github_release') + @unittest.mock.patch.object(release_process, 'bump_meta') + def test_finish_major_version_prerelease_rc(self, bump_meta_mock, + github_release_mock, + git_mock): + meta_repo = unittest.mock.MagicMock() + meta_repo.repo_config = {} + meta_repo.name = 'qiskit' + repo = unittest.mock.MagicMock() + repo.name = 'qiskit-terra' + repo.repo_config = {'branch_on_release': True} + repo.get_local_config = lambda: {} + conf = {'working_dir': self.temp_dir.path} + + def tag_history(*args, **kwargs): + return """1.0.0rc1 +1.0.0pre1 +0.45.1 +0.45.0 +0.25.3 +0.45.0rc1 +0.25.2.1 +0.25.2 +0.25.1 +0.25.0 +0.25.0rc1 +0.24.2 +0.24.1 +""" + + git_mock.get_tags = tag_history + with unittest.mock.patch.object( + release_process, 'multiprocessing' + ) as mp_mock: + mp_mock.Process = ProcessMock + release_process.finish_release('1.0.0rc1', repo, conf, meta_repo) + git_mock.create_branch.assert_called_once_with( + "stable/1.0", "1.0.0rc1", repo, push=True + ) + github_release_mock.assert_called_once_with( + repo, '1.0.0rc1...0.45.1', '1.0.0rc1', + config.default_changelog_categories, True) + bump_meta_mock.assert_not_called() + class ProcessMock: