Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5a23446
Add GitHub Actions workflow for package updates
neilr81 Feb 7, 2026
b7f8e53
Add script to update NuGet package versions
neilr81 Feb 7, 2026
c0d9eb7
Update script paths in package-update.yml
neilr81 Feb 7, 2026
bfb26c8
Apply suggestion from @Copilot
neilr81 Feb 7, 2026
88eb82c
Update package-update workflow to use cron schedule
neilr81 Feb 7, 2026
671dd97
Update .github/scripts/Update-NuGetPackageVersions.ps1
neilr81 Feb 7, 2026
1c56edc
Gate PR creation on commit detection in package-update workflow (#796)
Copilot Feb 7, 2026
4b27d31
Update .github/scripts/Update-NuGetPackageVersions.ps1
neilr81 Feb 7, 2026
8c3190a
Apply suggestion from @Copilot
neilr81 Feb 7, 2026
287fbe0
Apply suggestion from @Copilot
neilr81 Feb 7, 2026
c56db37
Apply suggestion from @Copilot
neilr81 Feb 7, 2026
1a02929
Add pull_request_target trigger to workflow
neilr81 Feb 7, 2026
1cf02c9
Apply suggestion from @Copilot
neilr81 Feb 7, 2026
331b09d
Update package-update.yml
neilr81 Feb 7, 2026
07d2d47
Update package-update.yml
neilr81 Feb 7, 2026
0455764
Rename checkout step for clarity
neilr81 Feb 7, 2026
1cbd892
Remove GitHub CLI installation from package-update.yml
neilr81 Feb 7, 2026
3e92781
Update Update-NuGetPackageVersions.ps1
neilr81 Feb 7, 2026
fc9b6c7
Fix label assignment in package-update workflow
neilr81 Feb 7, 2026
527230e
Remove 'powershell' label from package update PR
neilr81 Feb 7, 2026
34493b9
Add label to automated package update PR
neilr81 Feb 7, 2026
f2afff8
Fix label formatting in package-update.yml
neilr81 Feb 7, 2026
f5431d8
Add token input for create pull request action
neilr81 Feb 7, 2026
cd5081d
Update GH_TOKEN to use secrets for PR creation
neilr81 Feb 7, 2026
852325b
Update package-update.yml
neilr81 Feb 8, 2026
0150f4e
Add script to update .NET SDK and MSBuild SDK versions
neilr81 Feb 8, 2026
6c0ff0c
Merge 0150f4e64a2db68558e638d09c26305f04fcef4d into 60e237869ccaee744…
neilr81 Feb 8, 2026
3d8c054
chore(automation): apply PowerShell updates
github-actions[bot] Feb 8, 2026
7d85dac
Merge 3d8c054d82cc1f82d3adf836afc4482aea916281 into 60e237869ccaee744…
neilr81 Feb 8, 2026
2e990e3
Merge 7d85daca75aec6e46661d2e0c4466332f75f9348 into 60e237869ccaee744…
neilr81 Feb 8, 2026
3188132
Merge 2e990e3985e3554fd67faf961fbe368316503964 into 60e237869ccaee744…
neilr81 Feb 8, 2026
d9a33c9
Merge 31881321e7054cab7ca538949152414f7e93983e into 60e237869ccaee744…
neilr81 Feb 8, 2026
de397b9
Merge d9a33c95527e71dbc7272e844d98fde78986835a into 60e237869ccaee744…
neilr81 Feb 8, 2026
483c42f
Merge de397b915675d810068ff54c84981b61b07d0ca3 into 60e237869ccaee744…
neilr81 Feb 8, 2026
46a3008
Merge 483c42feece9abfa252e24519096e68f528f10e0 into 60e237869ccaee744…
neilr81 Feb 8, 2026
ef098de
Merge 46a3008f615279358739aa7df95a6653ac074461 into 60e237869ccaee744…
neilr81 Feb 8, 2026
c062660
Merge ef098de7ef4abe1736ff957b1c752112e5737136 into 60e237869ccaee744…
neilr81 Feb 8, 2026
4ab4a26
Merge c062660701bd5813dfd9c36e49d5dd7917f3a093 into 60e237869ccaee744…
neilr81 Feb 8, 2026
2fdd9c4
Merge 4ab4a2615d8bb0cdf4bb2764f2baffde2c9350b8 into 60e237869ccaee744…
neilr81 Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 359 additions & 0 deletions .github/scripts/Update-DotNetSdkVersions.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
<#
.SYNOPSIS
Updates .NET SDK and MSBuild SDK versions in global.json file.

.DESCRIPTION
Updates the .NET SDK version by querying the latest release from aka.ms redirects,
and updates MSBuild SDK package versions using 'dotnet package search'.

.PARAMETER GlobalJsonPath
Relative path to the global.json file from the source directory.
Default: "global.json"

.PARAMETER SdkChannel
The .NET SDK release channel to track. Options: STS, LTS, 8.0, 9.0, etc.
Default: "STS"

.PARAMETER SourcesDirectory
The root source directory containing the global.json file.
Default: $env:BUILD_SOURCESDIRECTORY (Azure Pipelines variable)

.PARAMETER FailOnError
If $true, throws an exception on SDK lookup failures. If $false, logs warnings and continues.
Default: $false

.NOTES
Verbose logging is automatically enabled when Azure Pipelines System.Debug is set to 'true'.
To enable verbose logging, set the system.debug variable in your pipeline or run with:
variables:
system.debug: true

.EXAMPLE
.\Update-DotNetSdkVersions.ps1

.EXAMPLE
.\Update-DotNetSdkVersions.ps1 -SdkChannel "LTS" -FailOnError $true

.EXAMPLE
$env:SYSTEM_DEBUG = 'true'; .\Update-DotNetSdkVersions.ps1 -GlobalJsonPath "global.json"
#>

[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$GlobalJsonPath = "global.json",

[Parameter(Mandatory = $false)]
[string]$SdkChannel = "STS",

[Parameter(Mandatory = $false)]
[string]$SourcesDirectory = $env:BUILD_SOURCESDIRECTORY,

[Parameter(Mandatory = $false)]
[bool]$FailOnError = $false
)

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$SourcesDirectory defaults to $env:BUILD_SOURCESDIRECTORY but the script doesn’t normalize/validate it like the package update script does. If the env var is unset/empty, Join-Path will fail. Add the same fallback logic (e.g., $env:GITHUB_WORKSPACE then Get-Location) and validate the directory exists.

Suggested change
# Normalize and validate the sources directory to avoid Join-Path failures
if ([string]::IsNullOrWhiteSpace($SourcesDirectory)) {
if (-not [string]::IsNullOrWhiteSpace($env:BUILD_SOURCESDIRECTORY)) {
$SourcesDirectory = $env:BUILD_SOURCESDIRECTORY
} elseif (-not [string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) {
$SourcesDirectory = $env:GITHUB_WORKSPACE
} else {
$SourcesDirectory = (Get-Location).Path
}
}
try {
# Resolve to a full, normalized path and ensure it exists as a directory
$resolvedSourcesDirectory = Resolve-Path -Path $SourcesDirectory -ErrorAction Stop
if (-not (Test-Path -Path $resolvedSourcesDirectory -PathType Container)) {
throw "Sources directory '$SourcesDirectory' is not a valid directory."
}
$SourcesDirectory = $resolvedSourcesDirectory.ProviderPath
}
catch {
$message = "Failed to resolve sources directory from '$SourcesDirectory'."
if ($FailOnError) {
throw "$message Error: $($_.Exception.Message)"
} else {
Write-Warning "$message Falling back to current location."
$fallback = (Get-Location).Path
$SourcesDirectory = $fallback
}
}

Copilot uses AI. Check for mistakes.
# Auto-detect verbose logging from Azure Pipelines System.Debug variable
$EnableVerboseLogging = ($env:SYSTEM_DEBUG -eq 'true') -or ($env:SYSTEM_DEBUG -eq '1')

$ErrorActionPreference = if ($FailOnError) { "Stop" } else { "Continue" }

# Helper function for version comparison
function Get-LatestVersionFromString {
param (
[string]$First,
[string]$Second
)

if (-not $First) { return $Second }
if (-not $Second) { return $First }

function Get-VersionFromString {
param ([string]$Value)

$splitIndex = $Value.IndexOf('-')
if ($splitIndex -eq -1) {
$versionString = $Value
$suffix = ''
} else {
$versionString = $Value.Substring(0, $splitIndex)
$suffix = $Value.Substring($splitIndex)
}

$version = $null
if (-not [System.Version]::TryParse($versionString, [ref]$version)) {
$version = $versionString
}

return [PSCustomObject]@{ Version = $version; Suffix = $suffix }
}

$firstVersionObject = Get-VersionFromString $First
$secondVersionObject = Get-VersionFromString $Second

if ($firstVersionObject.Version -eq $secondVersionObject.Version) {
if (-not $firstVersionObject.Suffix) { return $First }
if (-not $secondVersionObject.Suffix) { return $Second }
if ($firstVersionObject.Suffix -lt $secondVersionObject.Suffix) { return $Second }
return $First
}
Comment on lines +94 to +99
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get-LatestVersionFromString compares pre-release suffixes lexicographically (string compare), which can order -preview.10 before -preview.2. This can select the wrong latest SDK/MSBuild SDK version when pre-releases are involved. Use a SemVer/NuGetVersion-aware comparison (or parse numeric suffix parts) instead.

Copilot uses AI. Check for mistakes.

if ($firstVersionObject.Version -lt $secondVersionObject.Version) { return $Second }
return $First
}

function Test-PreReleaseVersion {
param ([string]$Version)
return $Version.Contains('-')
}

function Get-LatestSdkVersion {
param ([string]$Channel = "STS")

$sdkRedirectUrl = "https://aka.ms/dotnet/$Channel/dotnet-sdk-win-x64.zip"

try {
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Querying SDK redirect URL: $sdkRedirectUrl"
}

# Follow redirects automatically and get final URL
$response = Invoke-WebRequest -Uri $sdkRedirectUrl -Method HEAD -MaximumRedirection 10 -UseBasicParsing

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Response status: $($response.StatusCode)"
Write-Host " [VERBOSE] Response headers: $($response.Headers | ConvertTo-Json -Compress)"
}

# PowerShell Core: Get final URL from response object
$finalUrl = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
Comment on lines +121 to +129
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoke-WebRequest result handling is not compatible across Windows PowerShell 5.1 vs PowerShell Core: -UseBasicParsing is legacy, and $response.BaseResponse.RequestMessage.RequestUri is not available on WinPS 5.1. If this script is intended for Azure Pipelines (often WinPS 5.1), use a response URI property that exists there (e.g., BaseResponse.ResponseUri) and avoid -UseBasicParsing.

Suggested change
$response = Invoke-WebRequest -Uri $sdkRedirectUrl -Method HEAD -MaximumRedirection 10 -UseBasicParsing
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Response status: $($response.StatusCode)"
Write-Host " [VERBOSE] Response headers: $($response.Headers | ConvertTo-Json -Compress)"
}
# PowerShell Core: Get final URL from response object
$finalUrl = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
$response = Invoke-WebRequest -Uri $sdkRedirectUrl -Method HEAD -MaximumRedirection 10
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Response status: $($response.StatusCode)"
Write-Host " [VERBOSE] Response headers: $($response.Headers | ConvertTo-Json -Compress)"
}
# Get final URL from response object (compatible with Windows PowerShell 5.1 and PowerShell Core)
$finalUrl = $response.BaseResponse.ResponseUri.AbsoluteUri

Copilot uses AI. Check for mistakes.

if (-not $finalUrl) {
throw "Could not determine final redirect URL from response"
}

Write-Host "Resolved SDK URL: $finalUrl"

# URL format: https://dotnetcli.azureedge.net/dotnet/Sdk/<version>/dotnet-sdk-<version>-win-x64.zip
# Pattern: \d+ for major/minor to support multi-digit versions (e.g., 10.0.101)
$version = ($finalUrl | Select-String -Pattern "\d+\.\d+\.\d{3}").Matches.Value
if (-not $version) {
throw "Failed to extract version from URL: $finalUrl"
}

Write-Host "Latest .NET SDK version: $version"
return $version
}
catch {
$errorMsg = "Failed to retrieve SDK version from ${sdkRedirectUrl}: $($_.Exception.Message)"
if ($FailOnError) {
throw $errorMsg
} else {
Write-Warning $errorMsg
return $null
}
}
}

function Get-LatestPackageVersion {
param (
[string]$PackageId,
[bool]$IncludePrerelease
)

try {
$prereleaseFlag = if ($IncludePrerelease) { "--prerelease" } else { "" }

# Check if NuGet.config exists in the sources directory
$nugetConfigPath = Join-Path $SourcesDirectory "NuGet.config"
$configSourceFlag = ""
if (Test-Path $nugetConfigPath) {
$configSourceFlag = "--configfile `"$nugetConfigPath`""
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Using NuGet.config from: $nugetConfigPath"
}
} else {
Write-Warning "NuGet.config not found at: $nugetConfigPath - search may not find private feeds"
Comment on lines +167 to +176
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo root uses NuGet.Config (capital C), but this script looks for NuGet.config. On case-sensitive filesystems the config won’t be found, so package searches may miss required feeds. Update the filename (or check both casings).

Suggested change
# Check if NuGet.config exists in the sources directory
$nugetConfigPath = Join-Path $SourcesDirectory "NuGet.config"
$configSourceFlag = ""
if (Test-Path $nugetConfigPath) {
$configSourceFlag = "--configfile `"$nugetConfigPath`""
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Using NuGet.config from: $nugetConfigPath"
}
} else {
Write-Warning "NuGet.config not found at: $nugetConfigPath - search may not find private feeds"
# Check if NuGet.config / NuGet.Config exists in the sources directory
$nugetConfigLower = Join-Path $SourcesDirectory "NuGet.config"
$nugetConfigUpper = Join-Path $SourcesDirectory "NuGet.Config"
$configSourceFlag = ""
$nugetConfigPath = $null
if (Test-Path $nugetConfigLower) {
$nugetConfigPath = $nugetConfigLower
} elseif (Test-Path $nugetConfigUpper) {
$nugetConfigPath = $nugetConfigUpper
}
if ($nugetConfigPath) {
$configSourceFlag = "--configfile `"$nugetConfigPath`""
if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Using NuGet config from: $nugetConfigPath"
}
} else {
Write-Warning "NuGet config file not found (checked: $nugetConfigLower, $nugetConfigUpper) - search may not find private feeds"

Copilot uses AI. Check for mistakes.
}

$searchCmd = "dotnet package search `"$PackageId`" --exact-match --format json $prereleaseFlag $configSourceFlag"

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Executing: $searchCmd"
}

Write-Host "Searching for MSBuild SDK: $PackageId"

$output = Invoke-Expression $searchCmd 2>&1 | Out-String

if ($LASTEXITCODE -ne 0) {
$errorMsg = "Failed to search for package '$PackageId': $output"
if ($FailOnError) {
throw $errorMsg
} else {
Write-Warning $errorMsg
return $null
}
}

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Raw output: $output"
}

$result = $output | ConvertFrom-Json

if (-not $result.searchResult -or $result.searchResult.Count -eq 0) {
$errorMsg = "Package '$PackageId' not found in any configured feed"
if ($FailOnError) {
throw $errorMsg
} else {
Write-Warning $errorMsg
return $null
}
}

# Iterate through all sources and packages to find the latest version
# The JSON structure is: searchResult[].packages[].version
$latestVersion = $null
foreach ($source in $result.searchResult) {
if ($source.packages) {
foreach ($package in $source.packages) {
if ($package.id -eq $PackageId) {
$latestVersion = Get-LatestVersionFromString -First $latestVersion -Second $package.version
}
}
}
}

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Found latest version: $latestVersion"
}

return $latestVersion
}
catch {
$errorMsg = "Error searching for package '$PackageId': $_"
if ($FailOnError) {
throw $errorMsg
} else {
Write-Warning $errorMsg
return $null
}
}
}

# Main script execution
Write-Host "==============================================================================="
Write-Host ".NET SDK Version Update Script"
Write-Host "==============================================================================="
Write-Host "Global JSON Path: $GlobalJsonPath"
Write-Host "SDK Channel: $SdkChannel"
Write-Host "Sources Directory: $SourcesDirectory"
Write-Host "Verbose Logging: $EnableVerboseLogging"
Write-Host "Fail On Error: $FailOnError"
Write-Host "==============================================================================="

$globalJsonFile = Join-Path $SourcesDirectory $GlobalJsonPath
Write-Host "Full path to global.json: $globalJsonFile"

if (-not (Test-Path $globalJsonFile)) {
$errorMsg = "global.json file not found at: $globalJsonFile"
if ($FailOnError) {
throw $errorMsg
} else {
Write-Warning $errorMsg
exit 1
}
}

Write-Host "Updating SDK versions in: $globalJsonFile"
Write-Host "Using SDK channel: $SdkChannel"

# Load global.json
$globalJson = Get-Content $globalJsonFile -Raw | ConvertFrom-Json

if ($EnableVerboseLogging) {
Write-Host "[VERBOSE] Loaded global.json content:"
Write-Host ($globalJson | ConvertTo-Json -Depth 10)
}

$updateCount = 0

# Update .NET SDK version
$currentSdkVersion = $globalJson.sdk.version
Write-Host "Current .NET SDK version: $currentSdkVersion"

$latestSdkVersion = Get-LatestSdkVersion -Channel $SdkChannel

if ($latestSdkVersion) {
$selectedVersion = Get-LatestVersionFromString -First $currentSdkVersion -Second $latestSdkVersion

if ($EnableVerboseLogging) {
Write-Host "[VERBOSE] SDK version comparison: current=$currentSdkVersion, latest=$latestSdkVersion, selected=$selectedVersion"
}

if ($selectedVersion -ne $currentSdkVersion) {
Write-Host "##[section]Updating .NET SDK from '$currentSdkVersion' to '$latestSdkVersion'"
$globalJson.sdk.version = $latestSdkVersion
$updateCount++
} else {
Write-Host ".NET SDK already at latest version '$currentSdkVersion'"
}
}

# Update MSBuild SDKs
if ($globalJson.'msbuild-sdks') {
$msbuildSdks = $globalJson.'msbuild-sdks'

if ($EnableVerboseLogging) {
Write-Host "[VERBOSE] Found $($msbuildSdks.PSObject.Properties.Count) MSBuild SDK(s) to check"
}

foreach ($property in $msbuildSdks.PSObject.Properties) {
$packageName = $property.Name
$currentVersion = $property.Value

Write-Host "Checking MSBuild SDK: $packageName (current: $currentVersion)"

$includePrerelease = Test-PreReleaseVersion -Version $currentVersion

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Include prerelease: $includePrerelease"
}

$latestVersion = Get-LatestPackageVersion -PackageId $packageName -IncludePrerelease $includePrerelease

if (-not $latestVersion) {
Write-Host "No update available for '$packageName'"
continue
}

$selectedVersion = Get-LatestVersionFromString -First $currentVersion -Second $latestVersion

if ($EnableVerboseLogging) {
Write-Host " [VERBOSE] Version comparison: current=$currentVersion, latest=$latestVersion, selected=$selectedVersion"
}

if ($selectedVersion -ne $currentVersion) {
Write-Host "##[section]Updating MSBuild SDK '$packageName' from '$currentVersion' to '$latestVersion'"
$msbuildSdks.$packageName = $latestVersion
$updateCount++
} else {
Write-Host "MSBuild SDK '$packageName' already at latest version '$currentVersion'"
}
}
}

if ($updateCount -gt 0) {
Write-Host "##[section]Saving $updateCount SDK updates to $globalJsonFile"

# Save with consistent JSON formatting (2-space indent)
$globalJson | ConvertTo-Json -Depth 10 | Set-Content $globalJsonFile -NoNewline
Write-Host "Successfully updated $updateCount SDK(s)"
} else {
Write-Host "No SDK updates needed"
}

Write-Host "==============================================================================="
Write-Host ".NET SDK Update Complete"
Write-Host "==============================================================================="
Loading
Loading