diff --git a/eng/tools/azure-sdk-tools/packaging_tools/sdk_update_version.py b/eng/tools/azure-sdk-tools/packaging_tools/sdk_update_version.py index be4de4badf1e..506b38d97b8c 100644 --- a/eng/tools/azure-sdk-tools/packaging_tools/sdk_update_version.py +++ b/eng/tools/azure-sdk-tools/packaging_tools/sdk_update_version.py @@ -175,13 +175,43 @@ def edit_version_file(content: list[str]): def edit_changelog_file(content: list[str]): nonlocal unchanged version_line = f"## {version} ({release_date})\n" + first_version_index = -1 for i in range(0, len(content)): if re.match(r"^## \d+\.\d+\.\d+(b\d+)?", content[i]): content[i] = version_line unchanged = False + first_version_index = i _LOGGER.info(f"Updated version line in CHANGELOG.md to: {version_line.strip()}") break + # Remove duplicate version section: if a later section has the same version, + # delete it (from its header up to the next version header or end of file). + # This handles re-generation where the same changelog content is inserted again. + if first_version_index >= 0: + # Capture the version token from each version header (e.g., "1.2.3" or "1.2.3b1"). + version_pattern = re.compile(r"^## (?P\d+\.\d+\.\d+(b\d+)?)") + duplicate_start = -1 + for j in range(first_version_index + 1, len(content)): + match = version_pattern.match(content[j]) + if match: + # Compare only the version token, ignoring the release date or other suffixes. + if match.group("ver") == version: + duplicate_start = j + break # stop at the next version header regardless + + if duplicate_start >= 0: + # Find end of the duplicate section (next version header or EOF) + duplicate_end = len(content) + for k in range(duplicate_start + 1, len(content)): + if version_pattern.match(content[k]): + duplicate_end = k + break + _LOGGER.info( + f"Removing duplicate version section for {version} " + f"(lines {duplicate_start + 1}-{duplicate_end})" + ) + del content[duplicate_start:duplicate_end] + modify_file(str(changelog_path), edit_changelog_file) if unchanged: _LOGGER.warning(f"No version line found in {changelog_path} to update.") diff --git a/eng/tools/azure-sdk-tools/tests/test_sdk_update_version.py b/eng/tools/azure-sdk-tools/tests/test_sdk_update_version.py index 3953afa7928f..f55611594876 100644 --- a/eng/tools/azure-sdk-tools/tests/test_sdk_update_version.py +++ b/eng/tools/azure-sdk-tools/tests/test_sdk_update_version.py @@ -265,3 +265,95 @@ def mock_log_failed_message(message: str, enable_log_error: bool): assert ( log_level is None ), "Expected no error log for invalid changelog content in ARM SDK when version is explicitly provided" + + +def test_duplicate_version_section_removed(temp_package): + """When re-generating an SDK, the changelog may end up with duplicate version sections. + The update_version_main should detect and remove the duplicate. (GitHub issue #12425)""" + pkg = temp_package + (pkg / "_version.py").write_text('VERSION = "1.1.0b2"\n') + + # Simulate re-generation: changelog has a new section (with placeholder version) + # followed by an existing section with the same content and version. + changelog_content = ( + "# Release History\n" + "\n" + "## 0.0.0 (UnReleased)\n" + "\n" + "### Features Added\n" + "\n" + " - Model `Client` added parameter `setting` in method `__init__`\n" + "\n" + "### Breaking Changes\n" + "\n" + " - Model `StorageProperties` deleted property `iops`\n" + "\n" + "## 1.1.0b2 (2025-10-08)\n" + "\n" + "### Features Added\n" + "\n" + " - Model `Client` added parameter `setting` in method `__init__`\n" + "\n" + "### Breaking Changes\n" + "\n" + " - Model `StorageProperties` deleted property `iops`\n" + "\n" + "## 1.1.0b1 (2025-09-01)\n" + "\n" + "### Other Changes\n" + "\n" + " - Initial version\n" + ) + (pkg / "CHANGELOG.md").write_text(changelog_content) + + package_result = { + "version": "1.1.0b2", + "tagIsStable": False, + "targetReleaseDate": "2025-10-08", + "changelog": {"content": "### Features Added\n\n - Model `Client` added parameter `setting`"}, + } + update_version_main(pkg, package_result=package_result) + + result = (pkg / "CHANGELOG.md").read_text() + # The duplicate '## 1.1.0b2' section should be removed + assert result.count("## 1.1.0b2") == 1, f"Expected exactly one '## 1.1.0b2' section, got:\n{result}" + # The older version section should still be present + assert "## 1.1.0b1 (2025-09-01)" in result + assert " - Initial version" in result + + +def test_no_duplicate_version_left_intact(temp_package): + """When there is no duplicate, the changelog should remain unchanged aside from the version line update.""" + pkg = temp_package + (pkg / "_version.py").write_text('VERSION = "1.0.0b1"\n') + + changelog_content = ( + "# Release History\n" + "\n" + "## 0.0.0 (UnReleased)\n" + "\n" + "### Features Added\n" + "\n" + " - New feature\n" + "\n" + "## 1.0.0b1 (2025-09-01)\n" + "\n" + "### Other Changes\n" + "\n" + " - Initial version\n" + ) + (pkg / "CHANGELOG.md").write_text(changelog_content) + + package_result = { + "version": "1.0.0b1", + "tagIsStable": False, + "targetReleaseDate": "2025-10-08", + "changelog": {"content": "### Features Added\n\n - New feature"}, + } + update_version_main(pkg, package_result=package_result) + + result = (pkg / "CHANGELOG.md").read_text() + # Version should be updated to b2 + assert "## 1.0.0b2" in result + # Old b1 section should still exist (it's a different version, not a duplicate) + assert "## 1.0.0b1 (2025-09-01)" in result