Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
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 only to $env:BUILD_SOURCESDIRECTORY and isn’t normalized/validated like the NuGet update script. When run outside Azure Pipelines (e.g., GitHub Actions or locally), this can be empty and cause Join-Path/Test-Path failures. Add the same fallback logic ($env:GITHUB_WORKSPACE / current directory) and validate the directory exists before use.

Suggested change
# Normalize and validate sources directory for use across environments
if ([string]::IsNullOrWhiteSpace($SourcesDirectory)) {
$SourcesDirectory = $env:BUILD_SOURCESDIRECTORY
}
if ([string]::IsNullOrWhiteSpace($SourcesDirectory)) {
$SourcesDirectory = $env:GITHUB_WORKSPACE
}
if ([string]::IsNullOrWhiteSpace($SourcesDirectory)) {
$SourcesDirectory = (Get-Location).Path
}
if (-not (Test-Path -LiteralPath $SourcesDirectory -PathType Container)) {
throw "Sources directory '$SourcesDirectory' does not exist or is not a directory."
}
$SourcesDirectory = (Resolve-Path -LiteralPath $SourcesDirectory).Path

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
}

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

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 script looks for NuGet.config, but this repo uses NuGet.Config (capital C). On case-sensitive filesystems this will fail to locate the config and may miss private feeds. Update the filename casing to match the repo root file (NuGet.Config).

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 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"

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

Comment on lines +179 to +188
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.

Using Invoke-Expression to execute the dotnet package search command is fragile and can be unsafe. Prefer invoking dotnet with & and a proper argument list to avoid quoting issues and reduce injection risk.

Copilot uses AI. Check for mistakes.
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