diff --git a/documentation/release-checklist.md b/documentation/release-checklist.md
index 15d01fcc44f..55af1502033 100644
--- a/documentation/release-checklist.md
+++ b/documentation/release-checklist.md
@@ -88,7 +88,7 @@ if it is not, `darc add-default-channel --channel "VS {{THIS_RELEASE_VERSION}}"
### Final branding
- [ ] Prepare final branding PR for `vs{{THIS_RELEASE_VERSION}}`: {{URL_OF_FINAL_BRANDING_PR}} \
- Edit Version.props file - add `release` as a suffix (on same line! - to intentionaly make it merge conflict on flows to main) after the `VersionPrefix` \
+ Run `scripts/Stabilize-Release.ps1` to update `eng/Versions.props` (adds `release` on same line as `VersionPrefix` and changes `PreReleaseVersionLabel` to `servicing`). Use `-DryRun` to preview changes. \
e.g.: #11130, #10697
- [ ] Merge final branding to `vs{{THIS_RELEASE_VERSION}}` branch
- [ ] Update perfstar MSBuild insertions configuration: [example PR](https://dev.azure.com/devdiv/DevDiv/_git/dotnet-perfstar/pullrequest/522843): {{URL_OF_PERFSTAR_PR}}
diff --git a/documentation/release.md b/documentation/release.md
index febe2cc1317..c00984db6d3 100644
--- a/documentation/release.md
+++ b/documentation/release.md
@@ -6,6 +6,8 @@ This is a description of the steps required to release MSBuild. It is **incomple
To produce packages without a `-prerelease` suffix, we need to specify `release` (see the [Arcade versioning docs]). This is ideally done on the same line as the version specification so that it causes a Git merge conflict when merging to the next release's branch. See [#6902](https://github.com/dotnet/msbuild/pull/6902) for an example.
+Run `scripts/Stabilize-Release.ps1` to automate this process. The script modifies `eng/Versions.props` to add `DotNetFinalVersionKind` and change `PreReleaseVersionLabel` from `preview` to `servicing`. Use `-DryRun` to preview changes before applying them.
+
[Arcade versioning docs]: https://github.com/dotnet/arcade/blob/31cecde14e1512ecf60d2d8afb71fd240919f4a8/Documentation/CorePackages/Versioning.md
## Public API
diff --git a/scripts/Stabilize-Release.ps1 b/scripts/Stabilize-Release.ps1
new file mode 100644
index 00000000000..3053ff2ca09
--- /dev/null
+++ b/scripts/Stabilize-Release.ps1
@@ -0,0 +1,101 @@
+<#
+.SYNOPSIS
+ Performs the stabilization portion of the MSBuild release process.
+
+.DESCRIPTION
+ This script modifies eng/Versions.props to:
+ 1. Add release on the same line as VersionPrefix
+ (to create a merge conflict in forward-flow, as per release documentation)
+ 2. Change PreReleaseVersionLabel from 'preview' to 'servicing'
+
+ See documentation/release.md and documentation/release-checklist.md for details.
+
+.PARAMETER DryRun
+ If specified, shows what changes would be made without actually modifying the file.
+
+.EXAMPLE
+ .\Stabilize-Release.ps1
+ Performs the stabilization changes to eng/Versions.props.
+
+.EXAMPLE
+ .\Stabilize-Release.ps1 -DryRun
+ Shows what changes would be made without modifying any files.
+#>
+
+[CmdletBinding()]
+param(
+ [switch]$DryRun
+)
+
+Set-StrictMode -Version 'Latest'
+$ErrorActionPreference = 'Stop'
+
+# Find repo root by looking for eng/Versions.props
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$repoRoot = Split-Path -Parent $scriptDir
+
+$versionsPropsPath = Join-Path $repoRoot 'eng\Versions.props'
+
+if (-not (Test-Path $versionsPropsPath)) {
+ Write-Error "Could not find eng/Versions.props at expected location: '$versionsPropsPath'. Repository structure may have changed."
+ exit 1
+}
+
+Write-Host "Processing: $versionsPropsPath" -ForegroundColor Cyan
+
+$content = Get-Content $versionsPropsPath -Raw
+
+# Pattern 1: Add DotNetFinalVersionKind on the same line as VersionPrefix
+# Match: X.Y.Z
+# Replace with: X.Y.Zrelease
+$versionPrefixPattern = '([^<]+)(\s*\r?\n)'
+
+if ($content -match 'release') {
+ Write-Error "DotNetFinalVersionKind is already set to 'release'. Has stabilization already been done?"
+ exit 1
+} elseif ($content -match $versionPrefixPattern) {
+ $newVersionPrefixLine = '$1release$2'
+ $content = $content -replace $versionPrefixPattern, $newVersionPrefixLine
+ Write-Host " Added DotNetFinalVersionKind=release on VersionPrefix line." -ForegroundColor Green
+} else {
+ Write-Error "Could not find VersionPrefix element in expected format. Expected pattern like: X.Y.Z"
+ exit 1
+}
+
+# Pattern 2: Change PreReleaseVersionLabel from 'preview' to 'servicing'
+# Match: preview
+# Replace with: servicing
+$preReleasePattern = 'preview'
+
+if ($content -match 'servicing') {
+ Write-Error "PreReleaseVersionLabel is already 'servicing'. Has stabilization already been done?"
+ exit 1
+} elseif ($content -match $preReleasePattern) {
+ $content = $content -replace $preReleasePattern, 'servicing'
+ Write-Host " Changed PreReleaseVersionLabel from 'preview' to 'servicing'." -ForegroundColor Green
+} else {
+ Write-Error "Could not find PreReleaseVersionLabel with value 'preview'. Current value may not be 'preview'."
+ exit 1
+}
+
+# Extract version for commit message (e.g., "18.4" from "18.4.0")
+$versionForCommit = ""
+if ($content -match '(\d+\.\d+)\.\d+') {
+ $versionForCommit = $Matches[1]
+} else {
+ Write-Error "Could not extract VersionPrefix for commit message."
+ exit 1
+}
+
+if ($DryRun) {
+ Write-Host "`n=== DRY RUN - No changes written ===" -ForegroundColor Magenta
+ Write-Host "`nResulting content of eng/Versions.props (first 30 lines):" -ForegroundColor Cyan
+ $content -split "`n" | Select-Object -First 30 | ForEach-Object { Write-Host $_ }
+} else {
+ [System.IO.File]::WriteAllText($versionsPropsPath, $content, [System.Text.Encoding]::UTF8)
+ Write-Host "`nStabilization complete. Changes written to: $versionsPropsPath" -ForegroundColor Green
+ Write-Host "`nNext steps:" -ForegroundColor Cyan
+ Write-Host " 1. Review the changes: git diff eng/Versions.props"
+ Write-Host " 2. Commit: git commit -am 'Stable branding for $versionForCommit release'"
+ Write-Host " 3. Create PR to the release branch (e.g., vs$versionForCommit)"
+}