From b3148a64cdd2e6b2d1f286468364a39a80ceff59 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Thu, 12 Mar 2026 10:50:58 +0800 Subject: [PATCH 1/9] fix dependent version --- eng/scripts/patchhelpers.ps1 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/eng/scripts/patchhelpers.ps1 b/eng/scripts/patchhelpers.ps1 index 4682298462de..d76793485592 100644 --- a/eng/scripts/patchhelpers.ps1 +++ b/eng/scripts/patchhelpers.ps1 @@ -126,6 +126,26 @@ function CreateForwardLookingVersions($ArtifactInfos) { } } + # Seed the map with each artifact's own LatestGAOrPatchVersion so that dependents + # detect when an artifact in the patch list was independently released (not via this + # patch system) and has a newer version than what their POMs reference. + foreach ($arId in $ArtifactInfos.Keys) { + $latestVersion = $ArtifactInfos[$arId].LatestGAOrPatchVersion + if ($null -ne $latestVersion) { + $currentVersion = $allDependenciesWithVersion[$arId] + if ($null -eq $currentVersion) { + $allDependenciesWithVersion[$arId] = $latestVersion + } + else { + $orderedVersions = @($latestVersion, $currentVersion) | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } + $sortedVersions = [AzureEngSemanticVersion]::SortVersions($orderedVersions) + if ($null -ne $sortedVersions) { + $allDependenciesWithVersion[$arId] = $sortedVersions[0].RawVersion + } + } + } + } + return $allDependenciesWithVersion } From 23024e7a8b4fbf15b182a4e13f0d604b7ee9449b Mon Sep 17 00:00:00 2001 From: azure-sdk Date: Thu, 12 Mar 2026 14:20:29 +0800 Subject: [PATCH 2/9] Add regression test for seeding loop fix in CreateForwardLookingVersions Verifies that when an artifact is independently released to a newer GA version, dependents are correctly flagged for patching. Without the seeding loop (commit b3148a64), the forward-looking map would only contain the old version from the dependent's POM, causing the mismatch to go undetected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/tests/patchhelpers.tests.ps1 | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 eng/scripts/tests/patchhelpers.tests.ps1 diff --git a/eng/scripts/tests/patchhelpers.tests.ps1 b/eng/scripts/tests/patchhelpers.tests.ps1 new file mode 100644 index 000000000000..bcb959b81b9c --- /dev/null +++ b/eng/scripts/tests/patchhelpers.tests.ps1 @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Pester tests for patchhelpers.ps1 + +.DESCRIPTION + This file contains unit tests for the patch release helper functions + using the Pester testing framework. + + Tests cover the pure-logic functions that operate on in-memory data + structures and require no network, git, or file-system access. + +.NOTES + How to run: + 1. Install Pester if not already installed: + Install-Module Pester -Force -MinimumVersion 5.3.3 + + 2. Run the tests: + Invoke-Pester ./patchhelpers.tests.ps1 +#> + +BeforeAll { + # Import common scripts (loads AzureEngSemanticVersion via SemVer.ps1) + $commonScriptPath = Join-Path $PSScriptRoot ".." ".." "common" "scripts" "common.ps1" + . $commonScriptPath + + # Import patchhelpers (loads bomhelpers.ps1 transitively) + $patchHelpersPath = Join-Path $PSScriptRoot ".." "patchhelpers.ps1" + . $patchHelpersPath + + # Helper: build an ArtifactInfo with dependencies pre-populated. + function New-TestArtifactInfo { + param( + [string]$ArtifactId, + [string]$LatestGAOrPatchVersion, + [hashtable]$Dependencies = @{}, + [string]$GroupId = "com.azure" + ) + $info = [ArtifactInfo]::new($ArtifactId, $GroupId, $LatestGAOrPatchVersion) + $info.Dependencies = $Dependencies + return $info + } +} + +# --------------------------------------------------------------------------- +# Regression test for commit b3148a64 ("fix dependent version"). +# +# Before the fix, CreateForwardLookingVersions only populated the map from +# POM sections. When an artifact was independently released to +# a newer GA version, the map only had the OLD version (from the dependent's +# POM) and FindAllArtifactsThatNeedPatching saw no mismatch. +# The fix seeds each artifact's own LatestGAOrPatchVersion into the map. +# --------------------------------------------------------------------------- +Describe "Regression: seeding loop in CreateForwardLookingVersions" { + It "Detects stale dependency when only the seeding loop upgrades the map entry" { + # azure-core was independently released: 1.40.0 → 1.41.0 on Maven. + # azure-identity's published POM still references azure-core 1.40.0. + # + # Without the seeding loop the first loop puts azure-core=1.40.0 + # (from azure-identity's POM), so FindAll sees 1.40.0==1.40.0 → no patch. + # With the seeding loop, azure-core gets upgraded to 1.41.0 → mismatch → patch. + $infos = [ordered]@{ + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{}) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.40.0" + }) + } + + $forwardVersions = CreateForwardLookingVersions -ArtifactInfos $infos + + # Verify the seeding loop upgraded azure-core in the map + $forwardVersions["azure-core"] | Should -Be "1.41.0" + + FindAllArtifactsThatNeedPatching -ArtifactInfos $infos -AllDependenciesWithVersion $forwardVersions + + # azure-core was already released — no patch for itself + $infos["azure-core"].FutureReleasePatchVersion | Should -BeNullOrEmpty + # azure-identity must be flagged: its POM has azure-core 1.40.0, map has 1.41.0 + $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" + } +} From 3da0be1b01dfb326a79091c90cb00af024433d03 Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Thu, 12 Mar 2026 15:29:37 +0800 Subject: [PATCH 3/9] Simplify patch detection: replace CreateForwardLookingVersions + FindAllArtifactsThatNeedPatching Replace the two-function dance (CreateForwardLookingVersions building a complex version map from POM deps, then FindAllArtifactsThatNeedPatching comparing against it) with a single FindArtifactsThatNeedPatching function that: 1. Seeds a version map directly from each artifact's LatestGAOrPatchVersion 2. Iterates in dependency order (guaranteed by patch_release_client.txt) 3. Only checks dependencies that are in the patch list (ignores external deps) This also fixes a latent bug where external dependency version inconsistencies (e.g., different reactor-core versions across artifacts) could trigger false positive patch detections. Deletes unused ArtifactsToPatchUtil which had bugs (undefined $depId variable, infinite recursion passing wrong parameter). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...pdate-Artifacts-List-For-Patch-Release.ps1 | 6 +- eng/scripts/patchhelpers.ps1 | 92 ++------- eng/scripts/patchreleases.ps1 | 3 +- eng/scripts/tests/patchhelpers.tests.ps1 | 185 ++++++++++++++++-- 4 files changed, 183 insertions(+), 103 deletions(-) diff --git a/eng/scripts/Update-Artifacts-List-For-Patch-Release.ps1 b/eng/scripts/Update-Artifacts-List-For-Patch-Release.ps1 index 75d402da3976..9a2de59bf5a5 100644 --- a/eng/scripts/Update-Artifacts-List-For-Patch-Release.ps1 +++ b/eng/scripts/Update-Artifacts-List-For-Patch-Release.ps1 @@ -72,7 +72,7 @@ foreach ($ymlFile in $ymlFiles) { } } -# Use OrderedDictionary here for later "FindAllArtifactsThatNeedPatching" +# Use OrderedDictionary here for later "FindArtifactsThatNeedPatching" $ArtifactInfos = New-Object System.Collections.Specialized.OrderedDictionary Write-Host "Loading libraries from text file." @@ -87,9 +87,7 @@ foreach ($line in Get-Content "${PSScriptRoot}/../pipelines/patch_release_client UpdateDependencies -ArtifactInfos $ArtifactInfos -$AllDependenciesWithVersion = CreateForwardLookingVersions -ArtifactInfos $ArtifactInfos - -FindAllArtifactsThatNeedPatching -ArtifactInfos $ArtifactInfos -AllDependenciesWithVersion $AllDependenciesWithVersion +FindArtifactsThatNeedPatching -ArtifactInfos $ArtifactInfos $ArtifactsToPatch = $ArtifactInfos.Keys | Where-Object { $null -ne $ArtifactInfos[$_].FutureReleasePatchVersion } | ForEach-Object { $ArtifactInfos[$_].ArtifactId } diff --git a/eng/scripts/patchhelpers.ps1 b/eng/scripts/patchhelpers.ps1 index d76793485592..b8a38d021acf 100644 --- a/eng/scripts/patchhelpers.ps1 +++ b/eng/scripts/patchhelpers.ps1 @@ -101,92 +101,32 @@ function UpdateCIInformation($ArtifactInfos) { } } -# Create the forward looking graph for once the artifacts have been patched. -function CreateForwardLookingVersions($ArtifactInfos) { - $allDependenciesWithVersion = @{} +# Find all the artifacts that will need to be patched based on dependency analysis. +# Artifacts are processed in the same order as defined in patch_release_client.txt +# (dependencies appear before dependents). This guarantees cascading: if a dependency +# is patched, all its dependents will be included as well. +# Only dependencies that are themselves in the patch list are checked — external +# dependencies (reactor-core, jackson, etc.) are ignored. +function FindArtifactsThatNeedPatching($ArtifactInfos) { + $latestVersions = @{} foreach ($arId in $ArtifactInfos.Keys) { - foreach ($depId in $ArtifactInfos[$arId].Dependencies.Keys) { - $depVersion = $ArtifactInfos[$arId].Dependencies[$depId] - $currentVersion = $allDependenciesWithVersion[$depId] - if ($null -eq $currentVersion) { - $latestVersion = $depVersion - } - else { - $orderedVersions = @($depVersion, $currentVersion) | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } - $sortedVersions = [AzureEngSemanticVersion]::SortVersions($orderedVersions) - if($null -eq $sortedVersions) { - # We currently have a bug where semantic version may have 4 values just like jackson-databind. - $latestVersion = $depVersion - } else { - $latestVersion = $sortedVersions[0].RawVersion - } - } - - $allDependenciesWithVersion[$depId] = $latestVersion - } + $latestVersions[$arId] = $ArtifactInfos[$arId].LatestGAOrPatchVersion } - # Seed the map with each artifact's own LatestGAOrPatchVersion so that dependents - # detect when an artifact in the patch list was independently released (not via this - # patch system) and has a newer version than what their POMs reference. foreach ($arId in $ArtifactInfos.Keys) { - $latestVersion = $ArtifactInfos[$arId].LatestGAOrPatchVersion - if ($null -ne $latestVersion) { - $currentVersion = $allDependenciesWithVersion[$arId] - if ($null -eq $currentVersion) { - $allDependenciesWithVersion[$arId] = $latestVersion - } - else { - $orderedVersions = @($latestVersion, $currentVersion) | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } - $sortedVersions = [AzureEngSemanticVersion]::SortVersions($orderedVersions) - if ($null -ne $sortedVersions) { - $allDependenciesWithVersion[$arId] = $sortedVersions[0].RawVersion - } - } - } - } - - return $allDependenciesWithVersion -} - -# Find all the artifacts that will need to be patched based on the dependency analysis. -# Artifacts will be processed in the same order as defined in patch_release_client.txt (libraries that depend on other -# libraries will appear later in the file). -# This guarantees that if dependency libraries are going to be patched, dependent ones will be included as well. -function FindAllArtifactsThatNeedPatching($ArtifactInfos, $AllDependenciesWithVersion) { - foreach($arId in $ArtifactInfos.Keys) { $arInfo = $ArtifactInfos[$arId] - - foreach($depId in $arInfo.Dependencies.Keys) { - $depVersion = $arInfo.Dependencies[$depId] - - if($depVersion -ne $AllDependenciesWithVersion[$depId]) { - $currentGAOrPatchVersion = $arInfo.LatestGAOrPatchVersion - $newPatchVersion = GetPatchVersion -ReleaseVersion $currentGAOrPatchVersion - $arInfo.FutureReleasePatchVersion = $newPatchVersion - $AllDependenciesWithVersion[$arId] = $newPatchVersion + foreach ($depId in $arInfo.Dependencies.Keys) { + if (-not $latestVersions.ContainsKey($depId)) { continue } + if ($arInfo.Dependencies[$depId] -ne $latestVersions[$depId]) { + $patchVersion = GetPatchVersion -ReleaseVersion $arInfo.LatestGAOrPatchVersion + $arInfo.FutureReleasePatchVersion = $patchVersion + $latestVersions[$arId] = $patchVersion + break } } } } -# Helper class that analyzes all the artifacts that need to be patched if a given artifact is patched. -function ArtifactsToPatchUtil([String] $DependencyId, [hashtable]$ArtifactInfos, $AllDependenciesWithVersion) { - $arInfo = $ArtifactInfos[$DependencyId] - $currentGAOrPatchVersion = $arInfo.LatestGAOrPatchVersion - $newPatchVersion = GetPatchVersion -ReleaseVersion $currentGAOrPatchVersion - $arInfo.FutureReleasePatchVersion = $newPatchVersion - $AllDependenciesWithVersion[$depId] = $newPatchVersion - - foreach($arId in $ArtifactInfos.Keys) { - $arInfo = $ArtifactInfos[$arId] - $depVersion = $arInfo.Dependencies[$DependencyId] - if($depVersion -and $depVersion -ne $newPatchVersion) { - ArtifactsToPatchUtil -DependencyId $DependencyId -ArtifactInfos $ArtifactInfos -AllDependenciesWithVersion $AllDependenciesWithVersion - } - } -} - # Update dependencies in the version client file. function UpdateDependenciesInVersionClient([hashtable]$ArtifactInfos) { ## We need to update the version_client.txt to have the correct versions in place. diff --git a/eng/scripts/patchreleases.ps1 b/eng/scripts/patchreleases.ps1 index 39397727cbb6..f25b57cea01e 100644 --- a/eng/scripts/patchreleases.ps1 +++ b/eng/scripts/patchreleases.ps1 @@ -57,8 +57,7 @@ UpdateDependencies -ArtifactInfos $ArtifactInfos # $AzCoreNettyArtifactId = "azure-core-http-netty" # $ArtifactInfos[$AzCoreNettyArtifactId].Dependencies[$AzCoreArtifactId] = $AzCoreVersion -$AllDependenciesWithVersion = CreateForwardLookingVersions -ArtifactInfos $ArtifactInfos -FindAllArtifactsThatNeedPatching -ArtifactInfos $ArtifactInfos -AllDependenciesWithVersion $AllDependenciesWithVersion +FindArtifactsThatNeedPatching -ArtifactInfos $ArtifactInfos $ArtifactsToPatch = $ArtifactInfos.Keys | Where-Object { $null -ne $ArtifactInfos[$_].FutureReleasePatchVersion } | ForEach-Object {$ArtifactInfos[$_].ArtifactId} $RemoteName = GetRemoteName diff --git a/eng/scripts/tests/patchhelpers.tests.ps1 b/eng/scripts/tests/patchhelpers.tests.ps1 index bcb959b81b9c..c346fd0c952e 100644 --- a/eng/scripts/tests/patchhelpers.tests.ps1 +++ b/eng/scripts/tests/patchhelpers.tests.ps1 @@ -45,39 +45,182 @@ BeforeAll { } # --------------------------------------------------------------------------- -# Regression test for commit b3148a64 ("fix dependent version"). -# -# Before the fix, CreateForwardLookingVersions only populated the map from -# POM sections. When an artifact was independently released to -# a newer GA version, the map only had the OLD version (from the dependent's -# POM) and FindAllArtifactsThatNeedPatching saw no mismatch. -# The fix seeds each artifact's own LatestGAOrPatchVersion into the map. +# FindArtifactsThatNeedPatching # --------------------------------------------------------------------------- -Describe "Regression: seeding loop in CreateForwardLookingVersions" { - It "Detects stale dependency when only the seeding loop upgrades the map entry" { - # azure-core was independently released: 1.40.0 → 1.41.0 on Maven. - # azure-identity's published POM still references azure-core 1.40.0. - # - # Without the seeding loop the first loop puts azure-core=1.40.0 - # (from azure-identity's POM), so FindAll sees 1.40.0==1.40.0 → no patch. - # With the seeding loop, azure-core gets upgraded to 1.41.0 → mismatch → patch. +Describe "FindArtifactsThatNeedPatching" { + It "Single artifact with no dependencies — no patch" { + $infos = [ordered]@{ + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{}) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-core"].FutureReleasePatchVersion | Should -BeNullOrEmpty + } + + It "Outdated dependency triggers patch" { $infos = [ordered]@{ "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{}) "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ "azure-core" = "1.40.0" }) } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-core"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" + } - $forwardVersions = CreateForwardLookingVersions -ArtifactInfos $infos + It "Up-to-date dependency — no patch" { + $infos = [ordered]@{ + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{}) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.41.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-identity"].FutureReleasePatchVersion | Should -BeNullOrEmpty + } - # Verify the seeding loop upgraded azure-core in the map - $forwardVersions["azure-core"] | Should -Be "1.41.0" + It "Cascading patches through dependency chain" { + $infos = [ordered]@{ + "azure-json" = (New-TestArtifactInfo -ArtifactId "azure-json" -LatestGAOrPatchVersion "1.4.0" -Dependencies @{}) + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{ + "azure-json" = "1.3.0" + }) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.41.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-json"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-core"].FutureReleasePatchVersion | Should -Be "1.41.1" + # azure-identity sees azure-core 1.41.0 != 1.41.1 → cascade + $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" + } - FindAllArtifactsThatNeedPatching -ArtifactInfos $infos -AllDependenciesWithVersion $forwardVersions + It "External dependency version difference is ignored (no false positive)" { + # azure-core uses reactor-core 3.5.0, azure-identity uses 3.6.0. + # reactor-core is NOT in the patch list → should NOT trigger patching. + $infos = [ordered]@{ + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{ + "reactor-core" = "3.5.0" + }) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.41.0" + "reactor-core" = "3.6.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-core"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-identity"].FutureReleasePatchVersion | Should -BeNullOrEmpty + } - # azure-core was already released — no patch for itself + It "Independent release of a dependency triggers dependent patch (seeding loop regression)" { + # azure-core was independently released: 1.40.0 → 1.41.0 on Maven. + # azure-identity's published POM still references azure-core 1.40.0. + $infos = [ordered]@{ + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{}) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.40.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos $infos["azure-core"].FutureReleasePatchVersion | Should -BeNullOrEmpty - # azure-identity must be flagged: its POM has azure-core 1.40.0, map has 1.41.0 $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" } + + It "Dependency with no sibling dependents still triggers patch (containerinstance scenario)" { + # azure-storage-file-share was independently released (12.24.0 → 12.24.1). + # azure-resourcemanager-containerinstance depends on it but no OTHER artifact + # in the patch list does. Before the algorithm fix, containerinstance was missed. + $infos = [ordered]@{ + "azure-storage-file-share" = (New-TestArtifactInfo -ArtifactId "azure-storage-file-share" -LatestGAOrPatchVersion "12.24.1" -Dependencies @{}) + "azure-resourcemanager-containerinstance" = (New-TestArtifactInfo -ArtifactId "azure-resourcemanager-containerinstance" -LatestGAOrPatchVersion "2.44.0" -GroupId "com.azure.resourcemanager" -Dependencies @{ + "azure-storage-file-share" = "12.24.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-storage-file-share"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-resourcemanager-containerinstance"].FutureReleasePatchVersion | Should -Be "2.44.1" + } + + It "Diamond dependency — all paths detected" { + $infos = [ordered]@{ + "azure-json" = (New-TestArtifactInfo -ArtifactId "azure-json" -LatestGAOrPatchVersion "1.4.0" -Dependencies @{}) + "azure-xml" = (New-TestArtifactInfo -ArtifactId "azure-xml" -LatestGAOrPatchVersion "1.2.0" -Dependencies @{ + "azure-json" = "1.3.0" + }) + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{ + "azure-json" = "1.3.0" + }) + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-xml" = "1.2.0" + "azure-core" = "1.41.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-json"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-xml"].FutureReleasePatchVersion | Should -Be "1.2.1" + $infos["azure-core"].FutureReleasePatchVersion | Should -Be "1.41.1" + # azure-identity sees azure-xml 1.2.0 != 1.2.1 → cascade + $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" + } + + It "Multiple outdated deps — patch version computed once" { + $infos = [ordered]@{ + "azure-json" = (New-TestArtifactInfo -ArtifactId "azure-json" -LatestGAOrPatchVersion "1.4.0" -Dependencies @{}) + "azure-xml" = (New-TestArtifactInfo -ArtifactId "azure-xml" -LatestGAOrPatchVersion "1.2.0" -Dependencies @{}) + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{ + "azure-json" = "1.3.0" + "azure-xml" = "1.1.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + # azure-core is flagged once (patch version = 1.41.1, not 1.41.2) + $infos["azure-core"].FutureReleasePatchVersion | Should -Be "1.41.1" + } +} + +# --------------------------------------------------------------------------- +# GetPatchVersion +# --------------------------------------------------------------------------- +Describe "GetPatchVersion" { + It "Bumps the patch version (1.0.0 → 1.0.1)" { + GetPatchVersion -ReleaseVersion "1.0.0" | Should -Be "1.0.1" + } + + It "Bumps an existing patch version (12.24.1 → 12.24.2)" { + GetPatchVersion -ReleaseVersion "12.24.1" | Should -Be "12.24.2" + } +} + +# --------------------------------------------------------------------------- +# GetTopologicalSort +# --------------------------------------------------------------------------- +Describe "GetTopologicalSort" { + It "Linear chain returns correct order" { + $infos = [ordered]@{ + "A" = (New-TestArtifactInfo -ArtifactId "A" -LatestGAOrPatchVersion "1.0.0" -Dependencies @{ "B" = "1.0.0" }) + "B" = (New-TestArtifactInfo -ArtifactId "B" -LatestGAOrPatchVersion "1.0.0" -Dependencies @{ "C" = "1.0.0" }) + "C" = (New-TestArtifactInfo -ArtifactId "C" -LatestGAOrPatchVersion "1.0.0" -Dependencies @{}) + } + $infos["A"].PipelineName = "pipeline-a" + $infos["B"].PipelineName = "pipeline-b" + $infos["C"].PipelineName = "pipeline-c" + + $result = GetTopologicalSort -ArtifactIds @("A", "B", "C") -ArtifactInfos $infos + $flatOrder = $result | ForEach-Object { $_.ArtifactId } + $flatOrder.IndexOf("C") | Should -BeLessThan $flatOrder.IndexOf("B") + $flatOrder.IndexOf("B") | Should -BeLessThan $flatOrder.IndexOf("A") + } + + It "Independent artifacts are each returned" { + $infos = [ordered]@{ + "X" = (New-TestArtifactInfo -ArtifactId "X" -LatestGAOrPatchVersion "1.0.0" -Dependencies @{}) + "Y" = (New-TestArtifactInfo -ArtifactId "Y" -LatestGAOrPatchVersion "1.0.0" -Dependencies @{}) + } + $infos["X"].PipelineName = "pipeline-x" + $infos["Y"].PipelineName = "pipeline-y" + + $result = GetTopologicalSort -ArtifactIds @("X", "Y") -ArtifactInfos $infos + $result.Count | Should -Be 2 + } } From 65f7581c2d3d71e5f116ca160d0522e35894df14 Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Thu, 12 Mar 2026 16:04:36 +0800 Subject: [PATCH 4/9] Add -UseCurrentBranch switch to all patch scripts for local testing All patch scripts hardcoded $remoteName/main as the base for creating branches. This makes end-to-end local testing impossible without being on main. Add a -UseCurrentBranch switch that, when set, creates patch branches from HEAD instead of $remoteName/main. Threaded through all entry points and internal functions: - Generate-Patches-For-Automatic-Releases.ps1 - Generate-Patch.ps1 - generatepatch.ps1 - patchreleases.ps1 - GeneratePatch() / GeneratePatches() in bomhelpers.ps1 - GenerateBOMFile() in patchhelpers.ps1 Production behavior is unchanged (default creates from main). For local testing, add the switch: ./Generate-Patch.ps1 ... -UseCurrentBranch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/Generate-Patch.ps1 | 7 +++++-- .../Generate-Patches-For-Automatic-Releases.ps1 | 13 ++++++++----- eng/scripts/bomhelpers.ps1 | 11 ++++++----- eng/scripts/generatepatch.ps1 | 6 ++++-- eng/scripts/patchhelpers.ps1 | 7 ++++--- eng/scripts/patchreleases.ps1 | 13 ++++++++----- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/eng/scripts/Generate-Patch.ps1 b/eng/scripts/Generate-Patch.ps1 index f14d4cc28dc0..5b4e73457286 100644 --- a/eng/scripts/Generate-Patch.ps1 +++ b/eng/scripts/Generate-Patch.ps1 @@ -29,7 +29,9 @@ param( [Parameter(Mandatory=$false)][string]$PatchVersion, [Parameter(Mandatory=$false)][string]$BranchName, [Parameter(Mandatory=$false)][boolean]$PushToRemote, - [Parameter(Mandatory=$false)][boolean]$CreateNewBranch = $true + [Parameter(Mandatory=$false)][boolean]$CreateNewBranch = $true, + # When set, creates patch branches from the current branch instead of remote main. + [Parameter(Mandatory=$false)][switch]$UseCurrentBranch ) function TestPathThrow($Path, $PathName) { @@ -268,7 +270,8 @@ if(!$BranchName) { try { ## Creating a new branch if($CreateNewBranch) { - $cmdOutput = git checkout -b $BranchName $RemoteName/main + $base = if ($UseCurrentBranch) { "HEAD" } else { "$RemoteName/main" } + $cmdOutput = git checkout -b $BranchName $base } else { $cmdOutput = git checkout $BranchName diff --git a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 index ca455189f22c..c44887d18297 100644 --- a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 +++ b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 @@ -5,7 +5,9 @@ param( # $(Build.SourcesDirectory) - root of the repository [Parameter(Mandatory = $true)][string]$SourcesDirectory, # The yml file whose artifacts and additionalModules lists will be updated - [Parameter(Mandatory = $true)][string]$PackagesYmlPath + [Parameter(Mandatory = $true)][string]$PackagesYmlPath, + # When set, creates patch branches from the current branch instead of remote main. + [switch]$UseCurrentBranch ) $StartTime = $( get-date ) @@ -26,10 +28,11 @@ try { Write-Host "git fetch --all --prune" git fetch --all --prune - # Checkout a branch to work on based off of main in upstream. + # Checkout a branch to work on based off of main (or current branch if -UseCurrentBranch). if ($currentBranchName -ne $branchName) { - Write-Host "git checkout -b $branchName $remoteName/main" - git checkout -b $branchName $remoteName/main + $base = if ($UseCurrentBranch) { "HEAD" } else { "$remoteName/main" } + Write-Host "git checkout -b $branchName $base" + git checkout -b $branchName $base if ($LASTEXITCODE -ne 0) { LogError "Could not checkout branch $branchName, please check if it already exists and delete as necessary. Exiting..." @@ -54,7 +57,7 @@ try { # Reset each package to the latest stable release and update CHANGELOG, POM and README for patch release. foreach ($packageData in $packagesData) { - . "${PSScriptRoot}/generatepatch.ps1" -ArtifactIds $packageData["name"] -ServiceDirectoryName $packageData["ServiceDirectory"] -BranchName $branchName -GroupId $packageData["groupId"] + . "${PSScriptRoot}/generatepatch.ps1" -ArtifactIds $packageData["name"] -ServiceDirectoryName $packageData["ServiceDirectory"] -BranchName $branchName -GroupId $packageData["groupId"] -UseCurrentBranch:$UseCurrentBranch $libraryList += $packageData["groupId"] + ":" + $packageData["name"] + "," } diff --git a/eng/scripts/bomhelpers.ps1 b/eng/scripts/bomhelpers.ps1 index ea96485513e1..84f4cd56641f 100644 --- a/eng/scripts/bomhelpers.ps1 +++ b/eng/scripts/bomhelpers.ps1 @@ -231,9 +231,9 @@ function GitCommit($Message) { } # Generate patches for given artifact patch infos. -function GeneratePatches($ArtifactPatchInfos, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure") { +function GeneratePatches($ArtifactPatchInfos, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false) { foreach ($patchInfo in $ArtifactPatchInfos) { - GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId + GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch } #TriggerPipeline -PatchInfos $ArtifactPatchInfos -BranchName $BranchName @@ -251,7 +251,7 @@ function GetCurrentBranchName() { 3. Updating the changelog and readme's to update the dependency information. 4. Committing these changes. #> -function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure") { +function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false) { $artifactId = $PatchInfo.ArtifactId $releaseVersion = $PatchInfo.LatestGAOrPatchVersion $serviceDirectoryName = $PatchInfo.ServiceDirectoryName @@ -278,8 +278,9 @@ function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [st $currentBranchName = GetCurrentBranchName if ($currentBranchName -ne $BranchName) { - Write-Host "git checkout -b $BranchName $RemoteName/main" - $cmdOutput = git checkout -b $BranchName $RemoteName/main + $base = if ($UseCurrentBranch) { "HEAD" } else { "$RemoteName/main" } + Write-Host "git checkout -b $BranchName $base" + $cmdOutput = git checkout -b $BranchName $base if ($LASTEXITCODE -ne 0) { LogError "Could not checkout branch $BranchName, please check if it already exists and delete as necessary. Exiting..." exit $LASTEXITCODE diff --git a/eng/scripts/generatepatch.ps1 b/eng/scripts/generatepatch.ps1 index 8c69ee788115..a34ec5825925 100644 --- a/eng/scripts/generatepatch.ps1 +++ b/eng/scripts/generatepatch.ps1 @@ -44,7 +44,9 @@ param( [string[]]$ArtifactIds, [string]$ServiceDirectoryName, [string]$BranchName, - [string]$GroupId = 'com.azure' + [string]$GroupId = 'com.azure', + # When set, creates patch branches from the current branch instead of remote main. + [switch]$UseCurrentBranch ) $RepoRoot = Resolve-Path "${PSScriptRoot}..\..\.." @@ -87,7 +89,7 @@ foreach ($artifactId in $ArtifactIds) { $patchInfo = [ArtifactPatchInfo]::new() $patchInfo.ArtifactId = $artifactId $patchInfo.ServiceDirectoryName = $ServiceDirectoryName - GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId + GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch #TriggerPipeline -PatchInfos $patchInfo -BranchName $BranchName } diff --git a/eng/scripts/patchhelpers.ps1 b/eng/scripts/patchhelpers.ps1 index b8a38d021acf..ffe9430e5b55 100644 --- a/eng/scripts/patchhelpers.ps1 +++ b/eng/scripts/patchhelpers.ps1 @@ -233,7 +233,7 @@ function CreateDependencyXmlElement($Artifact, [xml]$Doc) { } # Generate BOM file for the given artifacts. -function GenerateBOMFile($ArtifactInfos, $BomFileBranchName) { +function GenerateBOMFile($ArtifactInfos, $BomFileBranchName, [bool]$UseCurrentBranch = $false) { $gaArtifacts = @() foreach ($artifact in $ArtifactInfos.Values) { @@ -270,8 +270,9 @@ function GenerateBOMFile($ArtifactInfos, $BomFileBranchName) { $releaseVersion = $bomFileContent.project.version $patchVersion = GetPatchVersion -ReleaseVersion $releaseVersion $remoteName = GetRemoteName - Write-Host "git checkout -b $BomFileBranchName $remoteName/main" - $cmdOutput = git checkout -b $BomFileBranchName $remoteName/main + $base = if ($UseCurrentBranch) { "HEAD" } else { "$remoteName/main" } + Write-Host "git checkout -b $BomFileBranchName $base" + $cmdOutput = git checkout -b $BomFileBranchName $base $bomFileContent.Save($BomFilePath) Write-Host "git add $BomFilePath" git add $BomFilePath diff --git a/eng/scripts/patchreleases.ps1 b/eng/scripts/patchreleases.ps1 index f25b57cea01e..4162dace0076 100644 --- a/eng/scripts/patchreleases.ps1 +++ b/eng/scripts/patchreleases.ps1 @@ -9,7 +9,9 @@ 4. Generate the forward looking BOM file and create a branch for the BOM release. #> param( - [string]$GroupId = "com.azure" + [string]$GroupId = "com.azure", + # When set, creates patch branches from the current branch instead of remote main. + [switch]$UseCurrentBranch ) Write-Information "PS Script Root is: $PSScriptRoot" @@ -74,8 +76,9 @@ $ArtifactPatchInfos = @() Write-Output "Preparing patch releases for BOM updates." try { $patchBranchName = "PatchSet_$bomPatchVersion" - Write-Host "git checkout -b $patchBranchName $RemoteName/main" - git checkout -b $patchBranchName $RemoteName/main + $base = if ($UseCurrentBranch) { "HEAD" } else { "$RemoteName/main" } + Write-Host "git checkout -b $patchBranchName $base" + git checkout -b $patchBranchName $base UpdateDependenciesInVersionClient -ArtifactInfos $ArtifactInfos foreach ($artifactId in $ArtifactsToPatch) { @@ -83,7 +86,7 @@ try { $patchInfo = [ArtifactPatchInfo]::new() $patchInfo = ConvertToPatchInfo -ArInfo $arInfo $ArtifactPatchInfos += $patchInfo - GeneratePatches -ArtifactPatchInfos $patchInfo -BranchName $patchBranchName -RemoteName $RemoteName -GroupId $GroupId + GeneratePatches -ArtifactPatchInfos $patchInfo -BranchName $patchBranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch } Write-Host "git -c user.name=`"azure-sdk`" -c user.email=`"azuresdk@microsoft.com`" push $RemoteName $patchBranchName" @@ -99,7 +102,7 @@ finally { $cmdOutput = git checkout $CurrentBranchName } -GenerateBOMFile -ArtifactInfos $ArtifactInfos -BomFileBranchName $bomBranchName +GenerateBOMFile -ArtifactInfos $ArtifactInfos -BomFileBranchName $bomBranchName -UseCurrentBranch $UseCurrentBranch GenerateJsonReport -ArtifactPatchInfos $ArtifactPatchInfos -PatchBranchName $patchBranchName -BomFileBranchName $bomBranchName #$orderedArtifacts = GetTopologicalSort -ArtifactIds $ArtifactsToPatch.Keys -ArtifactInfos $ArtifactInfos #GenerateHtmlReport -Artifacts $orderedArtifacts -PatchBranchName $patchBranchName -BomFileBranchName $bomBranchName From 5dd762a133f886ef560c0918f3a5ffd3f16fddf0 Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Thu, 12 Mar 2026 17:16:01 +0800 Subject: [PATCH 5/9] Fix ordering regression: use fixed-point loop for patch detection The simplified single-pass algorithm assumed patch_release_client.txt was ordered by dependency (dependencies before dependents). In reality it's alphabetical. This caused dependents like azure-messaging-eventhubs- checkpointstore-blob to be missed when they appeared before their dependency (azure-storage-blob) in the iteration order. Replace the single foreach with a do/while fixed-point loop that iterates until no new patches are found, making the algorithm correct regardless of artifact ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/Generate-Patch.ps1 | 3 ++ eng/scripts/patchhelpers.ps1 | 30 +++++++++++-------- eng/scripts/tests/patchhelpers.tests.ps1 | 38 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/eng/scripts/Generate-Patch.ps1 b/eng/scripts/Generate-Patch.ps1 index 5b4e73457286..23eb2aa569e2 100644 --- a/eng/scripts/Generate-Patch.ps1 +++ b/eng/scripts/Generate-Patch.ps1 @@ -19,6 +19,9 @@ # # 7. CreateNewBranch - Whether to create a new branch or use an existing branch. You would want to use an existing branch if you are using the same release tag for multiple libraries. # +# 8. UseCurrentBranch - When set, creates patch branches from the current branch instead of remote main. +# Useful for local testing and dry-runs without requiring changes to be merged first. This is not a required parameter. +# # Example: .\eng\scripts\Generate-Patch.ps1 -ArtifactName azure-mixedreality-remoterendering -ServiceDirectory remoterendering -ReleaseVersion 1.0.0 -PatchVersion 1.0.1 # This creates a remote branch "release/azure-mixedreality-remoterendering" with all the necessary changes. diff --git a/eng/scripts/patchhelpers.ps1 b/eng/scripts/patchhelpers.ps1 index ffe9430e5b55..e4e7b966f566 100644 --- a/eng/scripts/patchhelpers.ps1 +++ b/eng/scripts/patchhelpers.ps1 @@ -102,9 +102,8 @@ function UpdateCIInformation($ArtifactInfos) { } # Find all the artifacts that will need to be patched based on dependency analysis. -# Artifacts are processed in the same order as defined in patch_release_client.txt -# (dependencies appear before dependents). This guarantees cascading: if a dependency -# is patched, all its dependents will be included as well. +# Iterates until no more patches are found (fixed-point), so the result is correct +# regardless of artifact ordering in patch_release_client.txt. # Only dependencies that are themselves in the patch list are checked — external # dependencies (reactor-core, jackson, etc.) are ignored. function FindArtifactsThatNeedPatching($ArtifactInfos) { @@ -113,18 +112,23 @@ function FindArtifactsThatNeedPatching($ArtifactInfos) { $latestVersions[$arId] = $ArtifactInfos[$arId].LatestGAOrPatchVersion } - foreach ($arId in $ArtifactInfos.Keys) { - $arInfo = $ArtifactInfos[$arId] - foreach ($depId in $arInfo.Dependencies.Keys) { - if (-not $latestVersions.ContainsKey($depId)) { continue } - if ($arInfo.Dependencies[$depId] -ne $latestVersions[$depId]) { - $patchVersion = GetPatchVersion -ReleaseVersion $arInfo.LatestGAOrPatchVersion - $arInfo.FutureReleasePatchVersion = $patchVersion - $latestVersions[$arId] = $patchVersion - break + do { + $changed = $false + foreach ($arId in $ArtifactInfos.Keys) { + $arInfo = $ArtifactInfos[$arId] + if ($arInfo.FutureReleasePatchVersion) { continue } + foreach ($depId in $arInfo.Dependencies.Keys) { + if (-not $latestVersions.ContainsKey($depId)) { continue } + if ($arInfo.Dependencies[$depId] -ne $latestVersions[$depId]) { + $patchVersion = GetPatchVersion -ReleaseVersion $arInfo.LatestGAOrPatchVersion + $arInfo.FutureReleasePatchVersion = $patchVersion + $latestVersions[$arId] = $patchVersion + $changed = $true + break + } } } - } + } while ($changed) } # Update dependencies in the version client file. diff --git a/eng/scripts/tests/patchhelpers.tests.ps1 b/eng/scripts/tests/patchhelpers.tests.ps1 index c346fd0c952e..19aa0484e67f 100644 --- a/eng/scripts/tests/patchhelpers.tests.ps1 +++ b/eng/scripts/tests/patchhelpers.tests.ps1 @@ -177,6 +177,44 @@ Describe "FindArtifactsThatNeedPatching" { # azure-core is flagged once (patch version = 1.41.1, not 1.41.2) $infos["azure-core"].FutureReleasePatchVersion | Should -Be "1.41.1" } + + It "Unordered hashtable: cascading still works when iteration order is non-deterministic" { + # Production callers (patchreleases.ps1) pass plain @{} not [ordered]@{}. + # With an unordered hashtable the iteration order is undefined, so the + # fixed-point loop must converge regardless. + $infos = @{ + "azure-identity" = (New-TestArtifactInfo -ArtifactId "azure-identity" -LatestGAOrPatchVersion "1.10.0" -Dependencies @{ + "azure-core" = "1.41.0" + }) + "azure-json" = (New-TestArtifactInfo -ArtifactId "azure-json" -LatestGAOrPatchVersion "1.4.0" -Dependencies @{}) + "azure-core" = (New-TestArtifactInfo -ArtifactId "azure-core" -LatestGAOrPatchVersion "1.41.0" -Dependencies @{ + "azure-json" = "1.3.0" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-json"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-core"].FutureReleasePatchVersion | Should -Be "1.41.1" + $infos["azure-identity"].FutureReleasePatchVersion | Should -Be "1.10.1" + } + + It "Handles out-of-order: dependent listed before its dependency (checkpointstore scenario)" { + # checkpointstore appears BEFORE blob in the iteration order (alphabetical in patch_release_client.txt). + # blob gets patched because its own dep (storage-common) was independently released. + # checkpointstore must still be patched even though it's iterated first. + $infos = [ordered]@{ + "azure-storage-common" = (New-TestArtifactInfo -ArtifactId "azure-storage-common" -LatestGAOrPatchVersion "12.32.2" -Dependencies @{}) + "azure-messaging-eventhubs-checkpointstore-blob" = (New-TestArtifactInfo -ArtifactId "azure-messaging-eventhubs-checkpointstore-blob" -LatestGAOrPatchVersion "1.21.4" -Dependencies @{ + "azure-storage-blob" = "12.33.2" + }) + "azure-storage-blob" = (New-TestArtifactInfo -ArtifactId "azure-storage-blob" -LatestGAOrPatchVersion "12.33.2" -Dependencies @{ + "azure-storage-common" = "12.32.1" + }) + } + FindArtifactsThatNeedPatching -ArtifactInfos $infos + $infos["azure-storage-common"].FutureReleasePatchVersion | Should -BeNullOrEmpty + $infos["azure-storage-blob"].FutureReleasePatchVersion | Should -Be "12.33.3" + $infos["azure-messaging-eventhubs-checkpointstore-blob"].FutureReleasePatchVersion | Should -Be "1.21.5" + } } # --------------------------------------------------------------------------- From cd0d46be1b24a2bb0769500c576ee393f0338b19 Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Fri, 13 Mar 2026 12:19:47 +0800 Subject: [PATCH 6/9] Fix patch changelog to use GA versions instead of beta for dependencies The changelog generation was reading dependency versions from the pom.xml after update_versions.py ran, which writes beta/preview versions for {x-version-update;...;current} markers. For patch releases, the changelog should show GA/released versions (column 2 from version_client.txt). Added GetResolvedDependencyVersions function that resolves dependency versions from version_client.txt column 2 instead of the pom.xml. Applied the fix to both GeneratePatch() in bomhelpers.ps1 and CreatePatchRelease() in Generate-Patch.ps1. Fixes incorrect changelog entries like: 'Upgraded azure-messaging-eventhubs from 5.21.3 to 5.22.0-beta.1' which should instead show the GA version from version_client.txt col 2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/Generate-Patch.ps1 | 30 +++- eng/scripts/bomhelpers.ps1 | 47 +++++- eng/scripts/tests/patchhelpers.tests.ps1 | 196 +++++++++++++++++++++++ 3 files changed, 271 insertions(+), 2 deletions(-) diff --git a/eng/scripts/Generate-Patch.ps1 b/eng/scripts/Generate-Patch.ps1 index 23eb2aa569e2..90c114a1d943 100644 --- a/eng/scripts/Generate-Patch.ps1 +++ b/eng/scripts/Generate-Patch.ps1 @@ -176,6 +176,7 @@ function CreatePatchRelease($ArtifactName, $ServiceDirectoryName, $PatchVersion, $EngVersioningDir = Join-Path $EngDir "versioning" $SetVersionFilePath = Join-Path $EngVersioningDir "set_versions.py" $UpdateVersionFilePath = Join-Path $EngVersioningDir "update_versions.py" + $VersionClientPath = Join-Path $EngVersioningDir "version_client.txt" $pkgProperties = Get-PkgProperties -PackageName $ArtifactName -ServiceDirectory $ServiceDirectoryName -GroupId $GroupId $ChangelogPath = $pkgProperties.ChangeLogPath $PomFilePath = Join-Path $pkgProperties.DirectoryPath "pom.xml" @@ -201,8 +202,35 @@ function CreatePatchRelease($ArtifactName, $ServiceDirectoryName, $PatchVersion, exit 1 } + # Resolve new dependency versions from version_client.txt column 2 (GA/released version) + # instead of reading from the pom.xml, to avoid picking up beta versions written by + # update_versions.py for {x-version-update;...;current} markers. + # Key by groupId:artifactId to avoid collisions (e.g., com.azure vs com.azure.v2). + $versionClientLookup = @{} + foreach ($line in Get-Content -Path $VersionClientPath) { + $trimmed = $line.Trim() + if ($trimmed.StartsWith('#') -or [string]::IsNullOrWhiteSpace($trimmed)) { continue } + $parts = $trimmed.Split(';') + if ($parts.Length -ge 2) { + $versionClientLookup[$parts[0].Trim()] = $parts[1].Trim() + } + } + $newDependenciesToVersion = New-Object "System.Collections.Generic.Dictionary``2[System.String,System.String]" - parsePomFileDependencies -PomFilePath $PomFilePath -DependencyToVersion $newDependenciesToVersion + $pomFileContent = [xml](Get-Content -Path $PomFilePath) + foreach ($dependency in $pomFileContent.project.dependencies.dependency) { + $scope = $dependency.scope + if ($scope -ne 'test') { + $artifactId = $dependency.artifactId + $groupId = $dependency.groupId + $key = "${groupId}:${artifactId}" + if ($versionClientLookup.ContainsKey($key)) { + $newDependenciesToVersion[$artifactId] = $versionClientLookup[$key] + } else { + $newDependenciesToVersion[$artifactId] = $dependency.version + } + } + } $releaseStatus = "$(Get-Date -Format $CHANGELOG_DATE_FORMAT)" diff --git a/eng/scripts/bomhelpers.ps1 b/eng/scripts/bomhelpers.ps1 index 84f4cd56641f..a05ea5e0b840 100644 --- a/eng/scripts/bomhelpers.ps1 +++ b/eng/scripts/bomhelpers.ps1 @@ -155,6 +155,51 @@ function GetDependencyToVersion($PomFilePath) { return $dependencyNameToVersion } +# Resolve dependency versions using version_client.txt column 2 (dependency/released version) +# instead of reading from the pom.xml. This avoids picking up beta/preview versions written by +# update_versions.py for {x-version-update;...;current} markers. Falls back to the pom.xml +# value for dependencies not found in version_client.txt (e.g., external dependencies). +function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath) { + if (-not $VersionClientPath) { + $repoRoot = Resolve-Path "${PSScriptRoot}../../.." + $VersionClientPath = Join-Path $repoRoot "eng" "versioning" "version_client.txt" + } + + # Build lookup: groupId:artifactId → dependency version (column 2) from version_client.txt + # Key must include groupId because the same artifactId can appear under different groups + # (e.g., com.azure:azure-storage-blob vs com.azure.v2:azure-storage-blob). + $versionClientLookup = @{} + foreach ($line in Get-Content -Path $VersionClientPath) { + $trimmed = $line.Trim() + if ($trimmed.StartsWith('#') -or [string]::IsNullOrWhiteSpace($trimmed)) { continue } + $parts = $trimmed.Split(';') + if ($parts.Length -ge 2) { + $key = $parts[0].Trim() + $dependencyVersion = $parts[1].Trim() + $versionClientLookup[$key] = $dependencyVersion + } + } + + # Read non-test dependencies from pom.xml, resolve each against version_client.txt + $resolvedVersions = @{} + $pomFileContent = [xml](Get-Content -Path $PomFilePath) + foreach ($dependency in $pomFileContent.project.dependencies.dependency) { + $scope = $dependency.scope + if ($scope -ne 'test') { + $artifactId = $dependency.artifactId + $groupId = $dependency.groupId + $key = "${groupId}:${artifactId}" + if ($versionClientLookup.ContainsKey($key)) { + $resolvedVersions[$artifactId] = $versionClientLookup[$key] + } else { + $resolvedVersions[$artifactId] = $dependency.version + } + } + } + + return $resolvedVersions +} + # Create the changelog content from a message. function GetChangeLogContentFromMessage($ContentMessage) { $content = @() @@ -370,7 +415,7 @@ function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [st exit $LASTEXITCODE } - $newDependenciesToVersion = GetDependencyToVersion -PomFilePath $pomFilePath + $newDependenciesToVersion = GetResolvedDependencyVersions -PomFilePath $pomFilePath $releaseStatus = "$(Get-Date -Format $CHANGELOG_DATE_FORMAT)" $releaseStatus = "($releaseStatus)" $changeLogEntries = Get-ChangeLogEntries -ChangeLogLocation $changelogPath diff --git a/eng/scripts/tests/patchhelpers.tests.ps1 b/eng/scripts/tests/patchhelpers.tests.ps1 index 19aa0484e67f..b0872d6376b1 100644 --- a/eng/scripts/tests/patchhelpers.tests.ps1 +++ b/eng/scripts/tests/patchhelpers.tests.ps1 @@ -230,6 +230,202 @@ Describe "GetPatchVersion" { } } +# --------------------------------------------------------------------------- +# GetResolvedDependencyVersions +# --------------------------------------------------------------------------- +Describe "GetResolvedDependencyVersions" { + BeforeAll { + $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "patchtest_$(Get-Random)" + New-Item -ItemType Directory -Path $script:testDir -Force | Out-Null + } + + AfterAll { + if (Test-Path $script:testDir) { + Remove-Item -Recurse -Force $script:testDir + } + } + + It "Uses version_client.txt column 2 (GA) instead of pom.xml beta version" { + $versionClientFile = Join-Path $script:testDir "version_client.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-messaging-eventhubs;5.21.3;5.22.0-beta.1" + "com.azure:azure-storage-blob;12.33.2;12.34.0-beta.2" + ) + + $pomFile = Join-Path $script:testDir "pom.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-messaging-eventhubs + 5.22.0-beta.1 + + + com.azure + azure-storage-blob + 12.34.0-beta.2 + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-messaging-eventhubs"] | Should -Be "5.21.3" + $result["azure-storage-blob"] | Should -Be "12.33.2" + } + + It "Falls back to pom.xml version for external dependencies not in version_client.txt" { + $versionClientFile = Join-Path $script:testDir "version_client2.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-core;1.57.1;1.58.0-beta.1" + ) + + $pomFile = Join-Path $script:testDir "pom2.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-core + 1.58.0-beta.1 + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-core"] | Should -Be "1.57.1" + $result["jsr305"] | Should -Be "3.0.2" + } + + It "Excludes test-scoped dependencies" { + $versionClientFile = Join-Path $script:testDir "version_client3.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-core;1.57.1;1.58.0-beta.1" + "com.azure:azure-core-test;1.27.0-beta.14;1.27.0-beta.15" + ) + + $pomFile = Join-Path $script:testDir "pom3.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-core + 1.58.0-beta.1 + + + com.azure + azure-core-test + 1.27.0-beta.15 + test + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result.ContainsKey("azure-core") | Should -BeTrue + $result.ContainsKey("azure-core-test") | Should -BeFalse + } + + It "Handles patched dependency with updated column 2" { + $versionClientFile = Join-Path $script:testDir "version_client4.txt" + Set-Content -Path $versionClientFile -Value @( + "# Patched artifact — column 2 updated by UpdateDependenciesInVersionClient" + "com.azure:azure-storage-blob;12.33.3;12.34.0-beta.2" + "com.azure:azure-storage-common;12.32.2;12.33.0-beta.2" + ) + + $pomFile = Join-Path $script:testDir "pom4.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-storage-blob + 12.34.0-beta.2 + + + com.azure + azure-storage-common + 12.33.0-beta.2 + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-storage-blob"] | Should -Be "12.33.3" + $result["azure-storage-common"] | Should -Be "12.32.2" + } + + It "Skips comment and empty lines in version_client.txt" { + $versionClientFile = Join-Path $script:testDir "version_client5.txt" + Set-Content -Path $versionClientFile -Value @( + "# This is a comment" + "" + "com.azure:azure-core;1.57.1;1.58.0-beta.1" + " " + "# Another comment" + ) + + $pomFile = Join-Path $script:testDir "pom5.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-core + 1.58.0-beta.1 + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-core"] | Should -Be "1.57.1" + } + + It "Handles duplicate artifactId with different groupIds (com.azure vs com.azure.v2)" { + $versionClientFile = Join-Path $script:testDir "version_client6.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-storage-blob;12.33.2;12.34.0-beta.2" + "com.azure.v2:azure-storage-blob;13.0.0-beta.1;13.0.0-beta.1" + ) + + $pomFile = Join-Path $script:testDir "pom6.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-storage-blob + 12.34.0-beta.2 + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + # Must pick com.azure entry (12.33.2), NOT com.azure.v2 entry (13.0.0-beta.1) + $result["azure-storage-blob"] | Should -Be "12.33.2" + } +} + # --------------------------------------------------------------------------- # GetTopologicalSort # --------------------------------------------------------------------------- From f4d20ee39c9d43a78c340e562e14e982b4a790eb Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Fri, 13 Mar 2026 14:02:49 +0800 Subject: [PATCH 7/9] Fix patch changelog to show specific dependency upgrades GetResolvedDependencyVersions now uses a layered resolution strategy: 1. PatchVersionOverrides (highest priority) - patch versions for sibling artifacts being patched in the same run 2. version_client.txt col 2 - GA version fallback for prerelease pom versions 3. pom.xml version - used as-is for GA versions and external deps The previous approach read only from version_client.txt col 2, which was never updated with patch versions (set_versions.py --new-version only changes col 3). This caused all deps to appear unchanged, producing the generic 'Upgraded core dependencies.' message. patchreleases.ps1 now builds a PatchVersionOverrides map from ArtifactInfos and passes it through GeneratePatches/GeneratePatch to the resolver. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/Generate-Patch.ps1 | 18 ++-- eng/scripts/bomhelpers.ps1 | 47 +++++++--- eng/scripts/patchreleases.ps1 | 16 +++- eng/scripts/tests/patchhelpers.tests.ps1 | 111 +++++++++++++++-------- 4 files changed, 134 insertions(+), 58 deletions(-) diff --git a/eng/scripts/Generate-Patch.ps1 b/eng/scripts/Generate-Patch.ps1 index 90c114a1d943..4cfdd04db642 100644 --- a/eng/scripts/Generate-Patch.ps1 +++ b/eng/scripts/Generate-Patch.ps1 @@ -202,9 +202,10 @@ function CreatePatchRelease($ArtifactName, $ServiceDirectoryName, $PatchVersion, exit 1 } - # Resolve new dependency versions from version_client.txt column 2 (GA/released version) - # instead of reading from the pom.xml, to avoid picking up beta versions written by - # update_versions.py for {x-version-update;...;current} markers. + # Resolve new dependency versions for the changelog. Uses pom.xml versions + # (post update_versions.py) but substitutes prerelease versions with + # version_client.txt column 2 (GA/released version) to avoid showing beta + # versions in the changelog. # Key by groupId:artifactId to avoid collisions (e.g., com.azure vs com.azure.v2). $versionClientLookup = @{} foreach ($line in Get-Content -Path $VersionClientPath) { @@ -223,11 +224,16 @@ function CreatePatchRelease($ArtifactName, $ServiceDirectoryName, $PatchVersion, if ($scope -ne 'test') { $artifactId = $dependency.artifactId $groupId = $dependency.groupId + $pomVersion = $dependency.version $key = "${groupId}:${artifactId}" - if ($versionClientLookup.ContainsKey($key)) { - $newDependenciesToVersion[$artifactId] = $versionClientLookup[$key] + if ($pomVersion -match '-beta\.|_beta\.|BETA|-alpha\.|_alpha\.|ALPHA|-preview\.|_preview\.|PREVIEW|-SNAPSHOT') { + if ($versionClientLookup.ContainsKey($key)) { + $newDependenciesToVersion[$artifactId] = $versionClientLookup[$key] + } else { + $newDependenciesToVersion[$artifactId] = $pomVersion + } } else { - $newDependenciesToVersion[$artifactId] = $dependency.version + $newDependenciesToVersion[$artifactId] = $pomVersion } } } diff --git a/eng/scripts/bomhelpers.ps1 b/eng/scripts/bomhelpers.ps1 index a05ea5e0b840..c1a315e886d0 100644 --- a/eng/scripts/bomhelpers.ps1 +++ b/eng/scripts/bomhelpers.ps1 @@ -155,15 +155,23 @@ function GetDependencyToVersion($PomFilePath) { return $dependencyNameToVersion } -# Resolve dependency versions using version_client.txt column 2 (dependency/released version) -# instead of reading from the pom.xml. This avoids picking up beta/preview versions written by -# update_versions.py for {x-version-update;...;current} markers. Falls back to the pom.xml -# value for dependencies not found in version_client.txt (e.g., external dependencies). -function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath) { +# Resolve dependency versions for patch changelog generation. +# Uses a layered resolution strategy: +# 1. PatchVersionOverrides (highest priority) — maps artifactId to the patch version +# for sibling artifacts being patched in the same run. +# 2. version_client.txt column 2 — the released/GA version. Used as a fallback for +# dependencies whose pom.xml version is a prerelease (beta/alpha), which happens +# when update_versions.py writes column 3 for {x-version-update;...;current} markers. +# 3. pom.xml version (lowest priority) — used for external dependencies not in +# version_client.txt and for GA versions that are already correct. +function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath, $PatchVersionOverrides) { if (-not $VersionClientPath) { $repoRoot = Resolve-Path "${PSScriptRoot}../../.." $VersionClientPath = Join-Path $repoRoot "eng" "versioning" "version_client.txt" } + if (-not $PatchVersionOverrides) { + $PatchVersionOverrides = @{} + } # Build lookup: groupId:artifactId → dependency version (column 2) from version_client.txt # Key must include groupId because the same artifactId can appear under different groups @@ -180,7 +188,7 @@ function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath) { } } - # Read non-test dependencies from pom.xml, resolve each against version_client.txt + # Read non-test dependencies from pom.xml, resolve each version using the layered strategy. $resolvedVersions = @{} $pomFileContent = [xml](Get-Content -Path $PomFilePath) foreach ($dependency in $pomFileContent.project.dependencies.dependency) { @@ -188,11 +196,24 @@ function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath) { if ($scope -ne 'test') { $artifactId = $dependency.artifactId $groupId = $dependency.groupId + $pomVersion = $dependency.version $key = "${groupId}:${artifactId}" - if ($versionClientLookup.ContainsKey($key)) { - $resolvedVersions[$artifactId] = $versionClientLookup[$key] + + if ($PatchVersionOverrides.ContainsKey($artifactId)) { + # Sibling artifact being patched in the same run — use its patch version. + $resolvedVersions[$artifactId] = $PatchVersionOverrides[$artifactId] + } elseif ($pomVersion -match '-beta\.|_beta\.|BETA|-alpha\.|_alpha\.|ALPHA|-preview\.|_preview\.|PREVIEW|-SNAPSHOT') { + # Pom has a prerelease version (from {;current} marker) — fall back to + # version_client.txt column 2 (GA/released version) to avoid showing + # beta versions in the changelog. + if ($versionClientLookup.ContainsKey($key)) { + $resolvedVersions[$artifactId] = $versionClientLookup[$key] + } else { + $resolvedVersions[$artifactId] = $pomVersion + } } else { - $resolvedVersions[$artifactId] = $dependency.version + # GA version from pom (e.g., {;dependency} marker or external dep) — use as-is. + $resolvedVersions[$artifactId] = $pomVersion } } } @@ -276,9 +297,9 @@ function GitCommit($Message) { } # Generate patches for given artifact patch infos. -function GeneratePatches($ArtifactPatchInfos, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false) { +function GeneratePatches($ArtifactPatchInfos, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false, $PatchVersionOverrides = @{}) { foreach ($patchInfo in $ArtifactPatchInfos) { - GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch + GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch -PatchVersionOverrides $PatchVersionOverrides } #TriggerPipeline -PatchInfos $ArtifactPatchInfos -BranchName $BranchName @@ -296,7 +317,7 @@ function GetCurrentBranchName() { 3. Updating the changelog and readme's to update the dependency information. 4. Committing these changes. #> -function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false) { +function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [string]$GroupId = "com.azure", [bool]$UseCurrentBranch = $false, $PatchVersionOverrides = @{}) { $artifactId = $PatchInfo.ArtifactId $releaseVersion = $PatchInfo.LatestGAOrPatchVersion $serviceDirectoryName = $PatchInfo.ServiceDirectoryName @@ -415,7 +436,7 @@ function GeneratePatch($PatchInfo, [string]$BranchName, [string]$RemoteName, [st exit $LASTEXITCODE } - $newDependenciesToVersion = GetResolvedDependencyVersions -PomFilePath $pomFilePath + $newDependenciesToVersion = GetResolvedDependencyVersions -PomFilePath $pomFilePath -PatchVersionOverrides $PatchVersionOverrides $releaseStatus = "$(Get-Date -Format $CHANGELOG_DATE_FORMAT)" $releaseStatus = "($releaseStatus)" $changeLogEntries = Get-ChangeLogEntries -ChangeLogLocation $changelogPath diff --git a/eng/scripts/patchreleases.ps1 b/eng/scripts/patchreleases.ps1 index 4162dace0076..8992eaa662bc 100644 --- a/eng/scripts/patchreleases.ps1 +++ b/eng/scripts/patchreleases.ps1 @@ -74,6 +74,20 @@ $bomPatchVersion = GetNextBomVersion $bomBranchName = "bom_$bomPatchVersion" $ArtifactPatchInfos = @() Write-Output "Preparing patch releases for BOM updates." + +# Build a map of artifactId → patch version for all artifacts being patched. +# This is passed to GeneratePatches so changelogs can show the correct version +# when a sibling dependency is being patched in the same run. +$PatchVersionOverrides = @{} +foreach ($artifactId in $ArtifactInfos.Keys) { + $patchVersion = $ArtifactInfos[$artifactId].FutureReleasePatchVersion + if ($patchVersion) { + $PatchVersionOverrides[$artifactId] = $patchVersion + } else { + $PatchVersionOverrides[$artifactId] = $ArtifactInfos[$artifactId].LatestGAOrPatchVersion + } +} + try { $patchBranchName = "PatchSet_$bomPatchVersion" $base = if ($UseCurrentBranch) { "HEAD" } else { "$RemoteName/main" } @@ -86,7 +100,7 @@ try { $patchInfo = [ArtifactPatchInfo]::new() $patchInfo = ConvertToPatchInfo -ArInfo $arInfo $ArtifactPatchInfos += $patchInfo - GeneratePatches -ArtifactPatchInfos $patchInfo -BranchName $patchBranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch + GeneratePatches -ArtifactPatchInfos $patchInfo -BranchName $patchBranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch -PatchVersionOverrides $PatchVersionOverrides } Write-Host "git -c user.name=`"azure-sdk`" -c user.email=`"azuresdk@microsoft.com`" push $RemoteName $patchBranchName" diff --git a/eng/scripts/tests/patchhelpers.tests.ps1 b/eng/scripts/tests/patchhelpers.tests.ps1 index b0872d6376b1..0a2baf9e0e45 100644 --- a/eng/scripts/tests/patchhelpers.tests.ps1 +++ b/eng/scripts/tests/patchhelpers.tests.ps1 @@ -245,16 +245,39 @@ Describe "GetResolvedDependencyVersions" { } } - It "Uses version_client.txt column 2 (GA) instead of pom.xml beta version" { + It "Uses pom.xml GA version for dependency marker deps" { $versionClientFile = Join-Path $script:testDir "version_client.txt" Set-Content -Path $versionClientFile -Value @( - "com.azure:azure-messaging-eventhubs;5.21.3;5.22.0-beta.1" "com.azure:azure-storage-blob;12.33.2;12.34.0-beta.2" ) $pomFile = Join-Path $script:testDir "pom.xml" Set-Content -Path $pomFile -Value @' + + + + com.azure + azure-storage-blob + 12.33.2 + + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-storage-blob"] | Should -Be "12.33.2" + } + + It "Substitutes beta pom version with version_client.txt column 2 (GA)" { + $versionClientFile = Join-Path $script:testDir "version_client_beta.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-messaging-eventhubs;5.21.3;5.22.0-beta.1" + ) + + $pomFile = Join-Path $script:testDir "pom_beta.xml" + Set-Content -Path $pomFile -Value @' + @@ -262,18 +285,62 @@ Describe "GetResolvedDependencyVersions" { azure-messaging-eventhubs 5.22.0-beta.1 + + +'@ + + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile + $result["azure-messaging-eventhubs"] | Should -Be "5.21.3" + } + + It "PatchVersionOverrides take precedence over pom and version_client.txt" { + $versionClientFile = Join-Path $script:testDir "version_client_ovr.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-storage-blob;12.33.2;12.34.0-beta.2" + ) + + $pomFile = Join-Path $script:testDir "pom_ovr.xml" + Set-Content -Path $pomFile -Value @' + + + com.azure azure-storage-blob - 12.34.0-beta.2 + 12.33.2 '@ - $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile - $result["azure-messaging-eventhubs"] | Should -Be "5.21.3" - $result["azure-storage-blob"] | Should -Be "12.33.2" + $overrides = @{ "azure-storage-blob" = "12.33.3" } + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile -PatchVersionOverrides $overrides + $result["azure-storage-blob"] | Should -Be "12.33.3" + } + + It "PatchVersionOverrides also override beta pom versions" { + $versionClientFile = Join-Path $script:testDir "version_client_ovr2.txt" + Set-Content -Path $versionClientFile -Value @( + "com.azure:azure-messaging-eventhubs;5.21.3;5.22.0-beta.1" + ) + + $pomFile = Join-Path $script:testDir "pom_ovr2.xml" + Set-Content -Path $pomFile -Value @' + + + + + com.azure + azure-messaging-eventhubs + 5.22.0-beta.1 + + + +'@ + + $overrides = @{ "azure-messaging-eventhubs" = "5.21.4" } + $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile -PatchVersionOverrides $overrides + $result["azure-messaging-eventhubs"] | Should -Be "5.21.4" } It "Falls back to pom.xml version for external dependencies not in version_client.txt" { @@ -339,38 +406,6 @@ Describe "GetResolvedDependencyVersions" { $result.ContainsKey("azure-core-test") | Should -BeFalse } - It "Handles patched dependency with updated column 2" { - $versionClientFile = Join-Path $script:testDir "version_client4.txt" - Set-Content -Path $versionClientFile -Value @( - "# Patched artifact — column 2 updated by UpdateDependenciesInVersionClient" - "com.azure:azure-storage-blob;12.33.3;12.34.0-beta.2" - "com.azure:azure-storage-common;12.32.2;12.33.0-beta.2" - ) - - $pomFile = Join-Path $script:testDir "pom4.xml" - Set-Content -Path $pomFile -Value @' - - - - - com.azure - azure-storage-blob - 12.34.0-beta.2 - - - com.azure - azure-storage-common - 12.33.0-beta.2 - - - -'@ - - $result = GetResolvedDependencyVersions -PomFilePath $pomFile -VersionClientPath $versionClientFile - $result["azure-storage-blob"] | Should -Be "12.33.3" - $result["azure-storage-common"] | Should -Be "12.32.2" - } - It "Skips comment and empty lines in version_client.txt" { $versionClientFile = Join-Path $script:testDir "version_client5.txt" Set-Content -Path $versionClientFile -Value @( From af4fa478534d3e9a3bd5f6d8dca96edf731e7839 Mon Sep 17 00:00:00 2001 From: xiaofeicao Date: Fri, 13 Mar 2026 15:55:49 +0800 Subject: [PATCH 8/9] Pass PatchVersionOverrides through auto-release pipeline path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-release pipeline uses Generate-Patches-For-Automatic-Releases.ps1 → generatepatch.ps1 → GeneratePatch. This path was not passing PatchVersionOverrides, so sibling patch versions were invisible during changelog generation, resulting in 'Upgraded core dependencies.' instead of listing specific dependency changes. Fix: - generatepatch.ps1: accept -PatchVersionOverrides parameter - Generate-Patches-For-Automatic-Releases.ps1: build override map from YAML package list by querying Maven for each artifact's latest GA version and computing the next patch version Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...Generate-Patches-For-Automatic-Releases.ps1 | 18 +++++++++++++++++- eng/scripts/bomhelpers.ps1 | 2 ++ eng/scripts/generatepatch.ps1 | 7 +++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 index c44887d18297..bd1342623147 100644 --- a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 +++ b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 @@ -55,9 +55,25 @@ try { $packagesData = $ymlObject["extends"]["parameters"]["artifacts"] $libraryList = $null + # Build PatchVersionOverrides: map of artifactId → patch version for all artifacts + # being patched. This is passed to generatepatch.ps1 so changelogs show the correct + # version when a sibling dependency is also being patched in the same run. + $PatchVersionOverrides = @{} + foreach ($packageData in $packagesData) { + $pkgArtifactId = $packageData["name"] + $pkgGroupId = $packageData["groupId"] + try { + $mavenInfo = GetVersionInfoForAnArtifactId -GroupId $pkgGroupId -ArtifactId $pkgArtifactId + $patchVersion = GetPatchVersion -ReleaseVersion $mavenInfo.LatestGAOrPatchVersion + $PatchVersionOverrides[$pkgArtifactId] = $patchVersion + } catch { + Write-Warning "Could not determine patch version for ${pkgArtifactId}: $_" + } + } + # Reset each package to the latest stable release and update CHANGELOG, POM and README for patch release. foreach ($packageData in $packagesData) { - . "${PSScriptRoot}/generatepatch.ps1" -ArtifactIds $packageData["name"] -ServiceDirectoryName $packageData["ServiceDirectory"] -BranchName $branchName -GroupId $packageData["groupId"] -UseCurrentBranch:$UseCurrentBranch + . "${PSScriptRoot}/generatepatch.ps1" -ArtifactIds $packageData["name"] -ServiceDirectoryName $packageData["ServiceDirectory"] -BranchName $branchName -GroupId $packageData["groupId"] -UseCurrentBranch:$UseCurrentBranch -PatchVersionOverrides $PatchVersionOverrides $libraryList += $packageData["groupId"] + ":" + $packageData["name"] + "," } diff --git a/eng/scripts/bomhelpers.ps1 b/eng/scripts/bomhelpers.ps1 index c1a315e886d0..e9e2b4cdb5ce 100644 --- a/eng/scripts/bomhelpers.ps1 +++ b/eng/scripts/bomhelpers.ps1 @@ -156,6 +156,8 @@ function GetDependencyToVersion($PomFilePath) { } # Resolve dependency versions for patch changelog generation. +# Parses version_client.txt following the same format as utils.load_version_map_from_file +# (eng/versioning/utils.py), extracting column 2 (CodeModule.dependency) per entry. # Uses a layered resolution strategy: # 1. PatchVersionOverrides (highest priority) — maps artifactId to the patch version # for sibling artifacts being patched in the same run. diff --git a/eng/scripts/generatepatch.ps1 b/eng/scripts/generatepatch.ps1 index a34ec5825925..d9b0b120f592 100644 --- a/eng/scripts/generatepatch.ps1 +++ b/eng/scripts/generatepatch.ps1 @@ -46,7 +46,10 @@ param( [string]$BranchName, [string]$GroupId = 'com.azure', # When set, creates patch branches from the current branch instead of remote main. - [switch]$UseCurrentBranch + [switch]$UseCurrentBranch, + # Optional map of artifactId → version for sibling artifacts being patched in the same run. + # Passed to GeneratePatch so changelogs show the correct version for sibling dependencies. + [hashtable]$PatchVersionOverrides = @{} ) $RepoRoot = Resolve-Path "${PSScriptRoot}..\..\.." @@ -89,7 +92,7 @@ foreach ($artifactId in $ArtifactIds) { $patchInfo = [ArtifactPatchInfo]::new() $patchInfo.ArtifactId = $artifactId $patchInfo.ServiceDirectoryName = $ServiceDirectoryName - GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch + GeneratePatch -PatchInfo $patchInfo -BranchName $BranchName -RemoteName $RemoteName -GroupId $GroupId -UseCurrentBranch $UseCurrentBranch -PatchVersionOverrides $PatchVersionOverrides #TriggerPipeline -PatchInfos $patchInfo -BranchName $BranchName } From b5a012e1c41a86ac9884228efb8ea98b2809c781 Mon Sep 17 00:00:00 2001 From: Xiaofei Cao <92354331+XiaofeiCao@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:22:26 +0800 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/scripts/Generate-Patch.ps1 | 6 +++--- .../Generate-Patches-For-Automatic-Releases.ps1 | 9 +++++---- eng/scripts/bomhelpers.ps1 | 11 ++++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/eng/scripts/Generate-Patch.ps1 b/eng/scripts/Generate-Patch.ps1 index 4cfdd04db642..637311734b63 100644 --- a/eng/scripts/Generate-Patch.ps1 +++ b/eng/scripts/Generate-Patch.ps1 @@ -228,12 +228,12 @@ function CreatePatchRelease($ArtifactName, $ServiceDirectoryName, $PatchVersion, $key = "${groupId}:${artifactId}" if ($pomVersion -match '-beta\.|_beta\.|BETA|-alpha\.|_alpha\.|ALPHA|-preview\.|_preview\.|PREVIEW|-SNAPSHOT') { if ($versionClientLookup.ContainsKey($key)) { - $newDependenciesToVersion[$artifactId] = $versionClientLookup[$key] + $newDependenciesToVersion[$key] = $versionClientLookup[$key] } else { - $newDependenciesToVersion[$artifactId] = $pomVersion + $newDependenciesToVersion[$key] = $pomVersion } } else { - $newDependenciesToVersion[$artifactId] = $pomVersion + $newDependenciesToVersion[$key] = $pomVersion } } } diff --git a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 index bd1342623147..c1aee76dcbea 100644 --- a/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 +++ b/eng/scripts/Generate-Patches-For-Automatic-Releases.ps1 @@ -55,17 +55,18 @@ try { $packagesData = $ymlObject["extends"]["parameters"]["artifacts"] $libraryList = $null - # Build PatchVersionOverrides: map of artifactId → patch version for all artifacts - # being patched. This is passed to generatepatch.ps1 so changelogs show the correct - # version when a sibling dependency is also being patched in the same run. + # Build PatchVersionOverrides: map of "${groupId}:${artifactId}" → patch version for all + # artifacts being patched. This is passed to generatepatch.ps1 so changelogs show the + # correct version when a sibling dependency is also being patched in the same run. $PatchVersionOverrides = @{} foreach ($packageData in $packagesData) { $pkgArtifactId = $packageData["name"] $pkgGroupId = $packageData["groupId"] + $pkgKey = "${pkgGroupId}:${pkgArtifactId}" try { $mavenInfo = GetVersionInfoForAnArtifactId -GroupId $pkgGroupId -ArtifactId $pkgArtifactId $patchVersion = GetPatchVersion -ReleaseVersion $mavenInfo.LatestGAOrPatchVersion - $PatchVersionOverrides[$pkgArtifactId] = $patchVersion + $PatchVersionOverrides[$pkgKey] = $patchVersion } catch { Write-Warning "Could not determine patch version for ${pkgArtifactId}: $_" } diff --git a/eng/scripts/bomhelpers.ps1 b/eng/scripts/bomhelpers.ps1 index e9e2b4cdb5ce..847a5b5711ef 100644 --- a/eng/scripts/bomhelpers.ps1 +++ b/eng/scripts/bomhelpers.ps1 @@ -200,9 +200,14 @@ function GetResolvedDependencyVersions($PomFilePath, $VersionClientPath, $PatchV $groupId = $dependency.groupId $pomVersion = $dependency.version $key = "${groupId}:${artifactId}" - - if ($PatchVersionOverrides.ContainsKey($artifactId)) { - # Sibling artifact being patched in the same run — use its patch version. + $patchOverrideKey = $key + + if ($PatchVersionOverrides.ContainsKey($patchOverrideKey)) { + # Sibling artifact being patched in the same run — use its fully qualified + # patch version override (groupId:artifactId) to avoid collisions. + $resolvedVersions[$artifactId] = $PatchVersionOverrides[$patchOverrideKey] + } elseif ($PatchVersionOverrides.ContainsKey($artifactId)) { + # Backward compatibility: fall back to artifactId-only override if present. $resolvedVersions[$artifactId] = $PatchVersionOverrides[$artifactId] } elseif ($pomVersion -match '-beta\.|_beta\.|BETA|-alpha\.|_alpha\.|ALPHA|-preview\.|_preview\.|PREVIEW|-SNAPSHOT') { # Pom has a prerelease version (from {;current} marker) — fall back to