Skip to content

Commit 4b202d5

Browse files
jairmyreeCopilotXiaofeiCaoCopilotazure-sdk
authored
April 2026 Patch Release Megeback PR and mergeback automation script (#49091)
* Patch Release Megeback PR and mergeback automation script * Address review feedback on patch mergeback script Agent-Logs-Url: https://github.com/Azure/azure-sdk-for-java/sessions/b5b5d0a5-be66-4af3-913c-07bac6b7a93f Co-authored-by: jairmyree <67484440+jairmyree@users.noreply.github.com> * Updating out of date library * Bump version to 1.1.0-beta.3 for azure-security-confidentialledger * Remove empty section for "Other Changes" in CHANGELOG.md * mgmt, prepare release 2.62.0 (#49100) * Update azure-resourcemanager to stable version 2.62.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update pom versions for azure-resourcemanager 2.62.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update README and CHANGELOG for azure-resourcemanager 2.62.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Increment package versions for resourcemanager releases (#49107) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Xiaofei Cao <92354331+XiaofeiCao@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Azure SDK Bot <53356347+azure-sdk@users.noreply.github.com>
1 parent 89b8f6d commit 4b202d5

233 files changed

Lines changed: 1445 additions & 382 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
#Requires -Version 7.0
2+
#Requires -PSEdition Core
3+
4+
# Copyright (c) Microsoft Corporation. All rights reserved.
5+
# Licensed under the MIT License.
6+
7+
<#
8+
.SYNOPSIS
9+
[Experimental] Automates creation of a patch-release mergeback against the current branch.
10+
11+
.DESCRIPTION
12+
[Experimental] Given a release branch (e.g. release/patch/20260505) that contains a patch
13+
release, this script ports the patch's published-version bumps and changelog
14+
entries back into the current working branch. It performs three steps:
15+
16+
1. version_client.txt: For every line whose dependency-version (middle
17+
column) was bumped on the release branch, copy that bumped value into
18+
the current branch's version_client.txt. The current-version (right
19+
column) on the current branch is preserved as-is, because it usually
20+
reflects in-development beta versions that should not be reverted.
21+
22+
2. CHANGELOG.md: For each library whose dependency-version changed, take
23+
the topmost (newest) entry from the release branch's CHANGELOG and
24+
insert it into the current branch's CHANGELOG. When an existing
25+
"## ... (Unreleased)" section is present, the new entry is inserted
26+
after that entire section (before the next "##" heading). Otherwise,
27+
it is inserted before the first dated entry (immediately after the
28+
"# Release History" heading).
29+
30+
3. Runs `python eng/versioning/update_versions.py --skip-readme` to
31+
propagate the new dependency-versions into all pom.xml files.
32+
33+
This script does NOT commit, push, or open a pull request. Review the
34+
working tree afterwards, then commit and push manually.
35+
36+
.PARAMETER ReleaseBranch
37+
The name of the patch release branch to port edits from
38+
(for example release/patch/20260505). The branch can be local or remote;
39+
the script will use `git fetch` and resolve origin/<branch> if needed.
40+
41+
.PARAMETER RepoRoot
42+
Optional. Path to the azure-sdk-for-java clone. Defaults to the repo root
43+
inferred from the script's location.
44+
45+
.EXAMPLE
46+
.\eng\scripts\Create-Patch-Mergeback.ps1 -ReleaseBranch release/patch/20260505
47+
#>
48+
49+
[CmdletBinding()]
50+
param(
51+
[Parameter(Mandatory = $true)]
52+
[string]$ReleaseBranch,
53+
54+
[string]$RepoRoot
55+
)
56+
57+
$ErrorActionPreference = 'Stop'
58+
59+
# Resolve repo root.
60+
if (-not $RepoRoot) {
61+
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
62+
}
63+
$RepoRoot = (Resolve-Path $RepoRoot).Path
64+
Push-Location $RepoRoot
65+
try {
66+
Write-Host "Repo root: $RepoRoot"
67+
68+
$workingTreeStatus = git status --porcelain
69+
if ($LASTEXITCODE -ne 0) {
70+
throw "Failed to check working tree status."
71+
}
72+
if ($workingTreeStatus) {
73+
throw "Working tree is not clean. Commit, stash, or discard local changes before running this script."
74+
}
75+
76+
# ---------------------------------------------------------------------
77+
# Resolve the release branch ref.
78+
# ---------------------------------------------------------------------
79+
Write-Host "Fetching latest refs..."
80+
git fetch --quiet origin 2>$null | Out-Null
81+
82+
$branchRef = $null
83+
foreach ($candidate in @($ReleaseBranch, "origin/$ReleaseBranch")) {
84+
$null = git rev-parse --verify --quiet "$candidate" 2>$null
85+
if ($LASTEXITCODE -eq 0) { $branchRef = $candidate; break }
86+
}
87+
if (-not $branchRef) {
88+
throw "Could not resolve '$ReleaseBranch' (tried local and origin/)."
89+
}
90+
Write-Host "Using release branch ref: $branchRef"
91+
92+
# ---------------------------------------------------------------------
93+
# 1. version_client.txt: port dependency-version bumps.
94+
# ---------------------------------------------------------------------
95+
$vcRelPath = 'eng/versioning/version_client.txt'
96+
$vcPath = Join-Path $RepoRoot $vcRelPath
97+
98+
Write-Host "`n[1/3] Porting dependency-version bumps from $vcRelPath ..."
99+
100+
$releaseVcRaw = git show "${branchRef}:${vcRelPath}"
101+
if ($LASTEXITCODE -ne 0) { throw "Failed to read $vcRelPath from $branchRef." }
102+
$releaseLines = $releaseVcRaw -split "`r?`n"
103+
$localLines = Get-Content -LiteralPath $vcPath
104+
105+
function Parse-VcLine([string]$line) {
106+
# Returns @{ Key=...; Dep=...; Cur=...; Comment=... } or $null
107+
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
108+
$trim = $line.TrimStart()
109+
if ($trim.StartsWith('#')) { return $null }
110+
# Strip optional inline comment.
111+
$body = $line
112+
$note = ''
113+
$hash = $line.IndexOf(' #')
114+
if ($hash -ge 0) {
115+
$body = $line.Substring(0, $hash)
116+
$note = $line.Substring($hash)
117+
}
118+
$parts = $body.Split(';')
119+
if ($parts.Count -lt 3) { return $null }
120+
return [pscustomobject]@{
121+
Key = $parts[0].Trim()
122+
Dep = $parts[1].Trim()
123+
Cur = $parts[2].Trim()
124+
Comment = $note
125+
}
126+
}
127+
128+
$releaseMap = @{}
129+
foreach ($l in $releaseLines) {
130+
$p = Parse-VcLine $l
131+
if ($p) { $releaseMap[$p.Key] = $p }
132+
}
133+
134+
$changedArtifacts = New-Object System.Collections.Generic.List[string]
135+
$newLocal = New-Object System.Collections.Generic.List[string]
136+
137+
foreach ($line in $localLines) {
138+
$p = Parse-VcLine $line
139+
if (-not $p -or -not $releaseMap.ContainsKey($p.Key)) {
140+
$newLocal.Add($line); continue
141+
}
142+
$r = $releaseMap[$p.Key]
143+
if ($r.Dep -eq $p.Dep) {
144+
$newLocal.Add($line); continue
145+
}
146+
# Bump dep-version, keep local current-version.
147+
$newBody = "$($p.Key);$($r.Dep);$($p.Cur)"
148+
$newLocal.Add("$newBody$($p.Comment)")
149+
$changedArtifacts.Add($p.Key) | Out-Null
150+
}
151+
152+
if ($changedArtifacts.Count -eq 0) {
153+
Write-Host " No dependency-version changes found. Nothing to port."
154+
return
155+
}
156+
157+
# Preserve original line endings of the file.
158+
$origBytes = [System.IO.File]::ReadAllBytes($vcPath)
159+
$useCrlf = $false
160+
for ($i = 0; $i -lt [Math]::Min($origBytes.Length, 4096); $i++) {
161+
if ($origBytes[$i] -eq 13) { $useCrlf = $true; break }
162+
}
163+
$eol = if ($useCrlf) { "`r`n" } else { "`n" }
164+
[System.IO.File]::WriteAllText($vcPath, ($newLocal -join $eol) + $eol)
165+
166+
Write-Host " Bumped $($changedArtifacts.Count) artifact(s) in version_client.txt."
167+
168+
# ---------------------------------------------------------------------
169+
# 2. CHANGELOG.md: port the latest entry from the release branch for
170+
# each artifact whose dependency-version changed.
171+
# ---------------------------------------------------------------------
172+
Write-Host "`n[2/3] Porting CHANGELOG.md entries..."
173+
174+
# Build artifactId -> list of CHANGELOG.md paths once.
175+
$changelogIndex = @{}
176+
Get-ChildItem -Path (Join-Path $RepoRoot 'sdk') -Recurse -Filter 'CHANGELOG.md' -File `
177+
| ForEach-Object {
178+
$artifactId = $_.Directory.Name
179+
if (-not $changelogIndex.ContainsKey($artifactId)) {
180+
$changelogIndex[$artifactId] = New-Object System.Collections.Generic.List[string]
181+
}
182+
$changelogIndex[$artifactId].Add($_.FullName) | Out-Null
183+
}
184+
185+
function Resolve-ChangelogPath([string]$artifactId) {
186+
if (-not $changelogIndex.ContainsKey($artifactId)) { return $null }
187+
$paths = $changelogIndex[$artifactId]
188+
if ($paths.Count -eq 1) { return $paths[0] }
189+
# Prefer the v1 (non *-v2) directory, then shortest path as a tiebreaker.
190+
$nonV2 = $paths | Where-Object { $_ -notmatch '[\\/][^\\/]*-v2[\\/]' }
191+
if ($nonV2.Count -eq 1) { return $nonV2[0] }
192+
return ($paths | Sort-Object Length | Select-Object -First 1)
193+
}
194+
195+
function Get-LatestChangelogEntry([string]$relPath) {
196+
# Returns @{ Header=...; Body=... } for the topmost ## entry, or $null.
197+
$raw = git show "${branchRef}:${relPath}" 2>$null
198+
if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null }
199+
$lines = $raw -split "`r?`n"
200+
$startIdx = -1
201+
for ($i = 0; $i -lt $lines.Count; $i++) {
202+
if ($lines[$i] -match '^##\s+\S') { $startIdx = $i; break }
203+
}
204+
if ($startIdx -lt 0) { return $null }
205+
$endIdx = $lines.Count
206+
for ($i = $startIdx + 1; $i -lt $lines.Count; $i++) {
207+
if ($lines[$i] -match '^##\s+\S') { $endIdx = $i; break }
208+
}
209+
# Trim trailing blank lines.
210+
$block = $lines[$startIdx..($endIdx - 1)]
211+
while ($block.Count -gt 0 -and [string]::IsNullOrWhiteSpace($block[-1])) {
212+
$block = $block[0..($block.Count - 2)]
213+
}
214+
return [pscustomobject]@{
215+
Header = $lines[$startIdx]
216+
Body = ($block -join "`n")
217+
}
218+
}
219+
220+
$portedCount = 0
221+
$skippedNoFile = New-Object System.Collections.Generic.List[string]
222+
$skippedNoEntry = New-Object System.Collections.Generic.List[string]
223+
$alreadyPresent = New-Object System.Collections.Generic.List[string]
224+
225+
foreach ($key in $changedArtifacts) {
226+
$artifactId = $key.Split(':')[1]
227+
$localPath = Resolve-ChangelogPath $artifactId
228+
if (-not $localPath) { $skippedNoFile.Add($artifactId) | Out-Null; continue }
229+
230+
$relLocalPath = (Resolve-Path -LiteralPath $localPath -Relative).TrimStart('.', '\', '/').Replace('\', '/')
231+
232+
$entry = Get-LatestChangelogEntry $relLocalPath
233+
if (-not $entry) { $skippedNoEntry.Add($artifactId) | Out-Null; continue }
234+
235+
$existing = Get-Content -LiteralPath $localPath -Raw
236+
# Skip if the same released-version header is already present.
237+
$headerEscaped = [regex]::Escape($entry.Header)
238+
if ($existing -match "(?m)^$headerEscaped\s*$") {
239+
$alreadyPresent.Add($artifactId) | Out-Null; continue
240+
}
241+
242+
$existingLines = $existing -split "`r?`n"
243+
244+
# Find an Unreleased heading; if none, find the first dated heading.
245+
$unreleasedIdx = -1
246+
$firstDatedIdx = -1
247+
for ($i = 0; $i -lt $existingLines.Count; $i++) {
248+
if ($unreleasedIdx -lt 0 -and $existingLines[$i] -match '^##\s+.*\(Unreleased\)\s*$') {
249+
$unreleasedIdx = $i
250+
} elseif ($firstDatedIdx -lt 0 -and $existingLines[$i] -match '^##\s+\S') {
251+
$firstDatedIdx = $i
252+
}
253+
}
254+
255+
$insertText = $entry.Body.TrimEnd() + "`n"
256+
257+
if ($unreleasedIdx -ge 0) {
258+
# Find end of the Unreleased block (next ## heading or EOF).
259+
$blockEnd = $existingLines.Count
260+
for ($i = $unreleasedIdx + 1; $i -lt $existingLines.Count; $i++) {
261+
if ($existingLines[$i] -match '^##\s+\S') { $blockEnd = $i; break }
262+
}
263+
# Strip trailing blanks inside the unreleased block before inserting.
264+
$tail = $blockEnd
265+
while ($tail -gt $unreleasedIdx + 1 -and [string]::IsNullOrWhiteSpace($existingLines[$tail - 1])) {
266+
$tail--
267+
}
268+
$before = if ($tail -gt 0) { $existingLines[0..($tail - 1)] } else { @() }
269+
$after = if ($blockEnd -lt $existingLines.Count) { $existingLines[$blockEnd..($existingLines.Count - 1)] } else { @() }
270+
$merged = @()
271+
$merged += $before
272+
$merged += ''
273+
$merged += ($insertText -split "`n")
274+
if ($after.Count -gt 0) { $merged += $after }
275+
} elseif ($firstDatedIdx -ge 0) {
276+
$before = $existingLines[0..($firstDatedIdx - 1)]
277+
$after = $existingLines[$firstDatedIdx..($existingLines.Count - 1)]
278+
# Ensure exactly one blank between.
279+
while ($before.Count -gt 0 -and [string]::IsNullOrWhiteSpace($before[-1])) {
280+
$before = $before[0..($before.Count - 2)]
281+
}
282+
$merged = @()
283+
$merged += $before
284+
$merged += ''
285+
$merged += ($insertText -split "`n")
286+
$merged += ''
287+
$merged += $after
288+
} else {
289+
# No headings at all; append.
290+
$merged = $existingLines + @('') + ($insertText -split "`n")
291+
}
292+
293+
# Determine line ending of the existing file.
294+
$crlf = $existing.Contains("`r`n")
295+
$eol2 = if ($crlf) { "`r`n" } else { "`n" }
296+
$finalText = ($merged -join $eol2)
297+
if (-not $finalText.EndsWith($eol2)) { $finalText += $eol2 }
298+
[System.IO.File]::WriteAllText($localPath, $finalText)
299+
$portedCount++
300+
}
301+
302+
Write-Host " Ported $portedCount changelog entr$(if ($portedCount -eq 1){'y'}else{'ies'})."
303+
if ($alreadyPresent.Count -gt 0) {
304+
Write-Host " Already present (skipped): $($alreadyPresent -join ', ')"
305+
}
306+
if ($skippedNoFile.Count -gt 0) {
307+
Write-Warning " No CHANGELOG.md found for: $($skippedNoFile -join ', ')"
308+
}
309+
if ($skippedNoEntry.Count -gt 0) {
310+
Write-Warning " No release entry found on $branchRef for: $($skippedNoEntry -join ', ')"
311+
}
312+
313+
# ---------------------------------------------------------------------
314+
# 3. Propagate dependency-versions into pom.xml files.
315+
# ---------------------------------------------------------------------
316+
Write-Host "`n[3/3] Running update_versions.py --skip-readme ..."
317+
$py = (Get-Command python -ErrorAction SilentlyContinue) ?? (Get-Command python3 -ErrorAction SilentlyContinue)
318+
if (-not $py) { throw "python is not on PATH; cannot run update_versions.py." }
319+
& $py.Source 'eng/versioning/update_versions.py' '--skip-readme'
320+
if ($LASTEXITCODE -ne 0) {
321+
throw "update_versions.py failed with exit code $LASTEXITCODE."
322+
}
323+
324+
# ---------------------------------------------------------------------
325+
# Summary.
326+
# ---------------------------------------------------------------------
327+
$status = git status --porcelain
328+
$clCount = ($status | Where-Object { $_ -match 'CHANGELOG\.md' }).Count
329+
$pomCount = ($status | Where-Object { $_ -match 'pom\.xml' }).Count
330+
331+
Write-Host "`nDone."
332+
Write-Host " version_client.txt artifacts bumped : $($changedArtifacts.Count)"
333+
Write-Host " CHANGELOG.md files modified : $clCount"
334+
Write-Host " pom.xml files modified : $pomCount"
335+
Write-Host "`nReview with 'git diff', then commit and push when ready."
336+
}
337+
finally {
338+
Pop-Location
339+
}

0 commit comments

Comments
 (0)