From 7a141609c4bbf7fa6a27b6ed743354a5d8ff364a Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 13 Oct 2026 07:48:37 -0700 Subject: [PATCH 1/6] Parse HTML instead of using playwright for MCUs --- eng/update-dependencies/LcuVariableUpdater.cs | 111 +++++++---------- ...DotNet.Framework.UpdateDependencies.csproj | 1 - eng/update-dependencies/Program.cs | 12 +- .../UpdateCatalog/UpdateCatalogClient.cs | 115 ++++++++++++++++++ 4 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 eng/update-dependencies/UpdateCatalog/UpdateCatalogClient.cs diff --git a/eng/update-dependencies/LcuVariableUpdater.cs b/eng/update-dependencies/LcuVariableUpdater.cs index 5e775e90..507cbdde 100644 --- a/eng/update-dependencies/LcuVariableUpdater.cs +++ b/eng/update-dependencies/LcuVariableUpdater.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Text.RegularExpressions; -using Microsoft.Playwright; +using Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; namespace Microsoft.DotNet.Framework.UpdateDependencies; @@ -11,27 +11,8 @@ namespace Microsoft.DotNet.Framework.UpdateDependencies; /// Updates Latest Cumulative Update (LCU) download URLs variables by fetching /// the most recent update information from the Microsoft Update Catalog. /// -internal sealed partial class LcuVariableUpdater : IVariableUpdater, IAsyncDisposable +internal sealed partial class LcuVariableUpdater(UpdateCatalogClient catalog) : IVariableUpdater { - private static readonly BrowserNewContextOptions s_newBrowserOptions = new() { Locale = "en-US" }; - - private readonly Lazy> _playwright; - private readonly Lazy> _browser; - - public LcuVariableUpdater() - { - _playwright = new Lazy>(Playwright.Playwright.CreateAsync); - _browser = new( - async () => - { - var playwright = await _playwright.Value; - return await playwright.Chromium.LaunchAsync( - new BrowserTypeLaunchOptions { Headless = true } - ); - } - ); - } - /// /// Matches LCU variable names in the format "lcu|{version}|{framework}" /// where version contains digits and periods. Examples: @@ -77,72 +58,66 @@ public async Task GetNewValueAsync(string variableKey, IVariableContext /// KB article download URL private async Task GetKbDownloadUrlAsync(string kb, string windowsVersion) { - var browser = await _browser.Value; - var context = await browser.NewContextAsync(s_newBrowserOptions); - var page = await context.NewPageAsync(); - - await page.GotoAsync($"https://catalog.update.microsoft.com/Search.aspx?q={kb}"); - // Some windows versions require a more precise regex to match the // correct LCU in on the update catalog page. By convention, the // Windows version is the second part of the version name. - var tableRowRegex = windowsVersion switch + Regex titleRegex = windowsVersion switch { - "ltsc2022" => Server2022TableRowRegex, - "ltsc2025" => Server2025TableRowRegex, - _ => WindowsServerTableRowRegex + "ltsc2022" => Server2022TitleRegex, + "ltsc2025" => Server2025TitleRegex, + _ => WindowsServerTitleRegex }; - var downloadPopUpPage = await page.RunAndWaitForPopupAsync( - async () => - { - await page - .GetByRole(AriaRole.Row, new PageGetByRoleOptions() { NameRegex = tableRowRegex, Exact = true }) - .GetByRole(AriaRole.Button) - .ClickAsync(); - } - ); + // A KB has an update for each Windows version and architecture. + IReadOnlyList results = await catalog.SearchAsync(kb); + CatalogUpdate[] matchingUpdates = results + .Where(result => titleRegex.IsMatch(result.Title)) + .ToArray(); - var url = await downloadPopUpPage - .GetByRole(AriaRole.Link, s_getDownloadLinkOptions) - .GetAttributeAsync("href"); + CatalogUpdate update = matchingUpdates switch + { + [var match] => match, - await context.CloseAsync(); + [] => throw new InvalidOperationException( + $"No Update Catalog result for {kb} matches {windowsVersion}. " + + $"Results: [{string.Join("; ", results.Select(result => result.Title))}]"), - Console.WriteLine($"{kb} download URL: {url}"); + _ => throw new InvalidOperationException( + $"Multiple Update Catalog results for {kb} match {windowsVersion}: " + + $"[{string.Join("; ", matchingUpdates.Select(match => match.Title))}]"), + }; - return url ?? ""; - } + IReadOnlyList downloadUrls = await catalog.GetDownloadUrlsAsync(update); + string[] msuUrls = downloadUrls + .Where(url => MsuUrlRegex.IsMatch(url)) + .ToArray(); - public async ValueTask DisposeAsync() - { - if (_browser.IsValueCreated) + string msuUrl = msuUrls switch { - var browser = await _browser.Value; - await browser.DisposeAsync(); - } + [var url] => url, - if (_playwright.IsValueCreated) - { - var playwright = await _playwright.Value; - playwright.Dispose(); - } - } + [] => throw new InvalidOperationException( + $"No MSU download URL for '{update.Title}'. URLs: [{string.Join("; ", downloadUrls)}]"), - private static readonly PageGetByRoleOptions s_getDownloadLinkOptions = new() - { - NameRegex = DownloadLinkRegex, - }; + _ => throw new InvalidOperationException( + $"Multiple MSU download URLs for '{update.Title}': [{string.Join("; ", msuUrls)}]"), + }; + + Console.WriteLine($"{kb} download URL: {msuUrl}"); + + return msuUrl; + } - [GeneratedRegex(@"^windows.*\.msu$")] - private static partial Regex DownloadLinkRegex { get; } + // The file name at the end of the URL, e.g. ".../windows10.0-kb5126422-x64-ndp481_1e92b2.msu". + [GeneratedRegex(@"/windows[^/]*\.msu$")] + private static partial Regex MsuUrlRegex { get; } [GeneratedRegex(@"server.*x64", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex WindowsServerTableRowRegex { get; } + private static partial Regex WindowsServerTitleRegex { get; } [GeneratedRegex(@"server.*21H2.*x64", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex Server2022TableRowRegex { get; } + private static partial Regex Server2022TitleRegex { get; } [GeneratedRegex(@"24H2.*x64", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex Server2025TableRowRegex { get; } + private static partial Regex Server2025TitleRegex { get; } } diff --git a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj index 8ff60a5f..a6c8c672 100644 --- a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj +++ b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj @@ -11,7 +11,6 @@ - diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index dc083370..4d21d34f 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -3,14 +3,12 @@ // See the LICENSE file in the project root for more information. using Microsoft.DotNet.Framework.UpdateDependencies; +using Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; using System.CommandLine; // Command-line tool for updating dependency variables in .NET Framework Docker // manifest files. // -// Before running, you may need to build the app and then run: -// pwsh bin/Debug/net*/playwright.ps1 install -// // Usage: dotnet run -- --help var manifestFileOption = new Argument("manifest file path") @@ -36,10 +34,10 @@ var manifestVersionsContent = await File.ReadAllTextAsync(manifestFilePath); var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); - await using (var lcuUpdater = new LcuVariableUpdater()) - { - await manifestVersionsContext.ApplyAsync(lcuUpdater); - } + using var httpClient = new HttpClient(); + var catalog = new UpdateCatalogClient(httpClient); + var lcuUpdater = new LcuVariableUpdater(catalog); + await manifestVersionsContext.ApplyAsync(lcuUpdater); await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); } diff --git a/eng/update-dependencies/UpdateCatalog/UpdateCatalogClient.cs b/eng/update-dependencies/UpdateCatalog/UpdateCatalogClient.cs new file mode 100644 index 00000000..9f612f64 --- /dev/null +++ b/eng/update-dependencies/UpdateCatalog/UpdateCatalogClient.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; + +/// +/// Client for the Microsoft Update Catalog website, which has no API. It +/// requests the same pages a browser does and reads the results from them. +/// +internal sealed partial class UpdateCatalogClient(HttpClient httpClient) +{ + private const string CatalogUrl = "https://www.catalog.update.microsoft.com"; + + // Shown instead of the results table when a search finds nothing. + private const string NoResultsText = "We did not find any results for"; + + // camelCase names, as the download dialog expects. + private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web); + + /// + /// Searches the catalog, e.g. for a KB number. Returns an empty list if + /// nothing matches. + /// + public async Task> SearchAsync(string query) + { + using var request = new HttpRequestMessage( + HttpMethod.Get, + $"{CatalogUrl}/Search.aspx?q={Uri.EscapeDataString(query)}"); + + string html = await SendAsync(request); + + List updates = SearchResultPattern.Matches(html) + .Select(match => new CatalogUpdate(Id: match.Groups["id"].Value, Title: match.Groups["title"].Value)) + .ToList(); + + // The catalog reports errors as an ordinary page with a 200 status. + if (updates.Count == 0 && !html.Contains(NoResultsText)) + { + throw new InvalidOperationException( + $"The Update Catalog returned an unexpected page for search '{query}'."); + } + + return updates; + } + + /// + /// Gets the URLs of the files that can be downloaded for an update. + /// + public async Task> GetDownloadUrlsAsync(CatalogUpdate update) + { + // The dialog takes a list of updates to download. + UpdateToDownload[] updatesToDownload = [new UpdateToDownload(update.Id)]; + string updateIDsJson = JsonSerializer.Serialize(updatesToDownload, s_jsonOptions); + + var form = new Dictionary { ["updateIDs"] = updateIDsJson }; + + using var request = new HttpRequestMessage(HttpMethod.Post, $"{CatalogUrl}/DownloadDialog.aspx") + { + Content = new FormUrlEncodedContent(form), + }; + + string dialogHtml = await SendAsync(request); + + List urls = DownloadUrlPattern.Matches(dialogHtml) + .Select(match => match.Groups["url"].Value) + .ToList(); + + // An update that exists always has at least one file, so a page + // without URLs is an error page. + if (urls.Count == 0) + { + throw new InvalidOperationException( + $"The Update Catalog returned no download URLs for '{update.Title}' ({update.Id})."); + } + + return urls; + } + + private async Task SendAsync(HttpRequestMessage request) + { + // The titles we match on are in English. + request.Headers.AcceptLanguage.ParseAdd("en-US"); + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(); + } + + // Matches the link to an update's details in a search result row: + // {title} + [GeneratedRegex(@"goToDetails\(""(?[^""]+)""\);'[^>]*>\s*(?[^<]+?)\s*</a>")] + private static partial Regex SearchResultPattern { get; } + + // Matches a file URL in the script embedded in the download dialog's page: + // downloadInformation[0].files[0].url = '{url}'; + [GeneratedRegex(@"downloadInformation\[\d+\]\.files\[\d+\]\.url\s*=\s*'(?<url>[^']+)'")] + private static partial Regex DownloadUrlPattern { get; } + + // An update to download. The dialog expects its ID twice, and a size. + private sealed record UpdateToDownload(string UpdateID) + { + public string Uid => UpdateID; + public int Size => 0; + } +} + +/// <summary> +/// An update in the Update Catalog. +/// </summary> +internal sealed record CatalogUpdate(string Id, string Title); From fa75b96c1875249ad712fcd7d70eacb2d4d87c22 Mon Sep 17 00:00:00 2001 From: Logan Bussell <loganbussell@microsoft.com> Date: Tue, 13 Oct 2026 08:13:08 -0700 Subject: [PATCH 2/6] Add update-nuget command to set the latest recommended NuGet CLI version Add a small tools.json client and a NuGetVariableUpdater that rewrites the nuget|version variable to the first ReleasedAndBlessed nuget.exe. Wire it into update-dependencies as 'update-nuget [manifest]'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f408eb74-08a1-4f02-9d5b-30e0bd0d7c6f --- eng/update-dependencies/NuGet/NuGetClient.cs | 58 +++++++++++++++++++ .../NuGetVariableUpdater.cs | 28 +++++++++ eng/update-dependencies/Program.cs | 27 ++++++++- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 eng/update-dependencies/NuGet/NuGetClient.cs create mode 100644 eng/update-dependencies/NuGetVariableUpdater.cs diff --git a/eng/update-dependencies/NuGet/NuGetClient.cs b/eng/update-dependencies/NuGet/NuGetClient.cs new file mode 100644 index 00000000..3b19f9e8 --- /dev/null +++ b/eng/update-dependencies/NuGet/NuGetClient.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.DotNet.Framework.UpdateDependencies.NuGet; + +/// <summary> +/// Client for the NuGet tools.json endpoint, which lists every released +/// nuget.exe. +/// </summary> +internal sealed class NuGetClient(HttpClient httpClient) +{ + private const string ToolsUrl = "https://dist.nuget.org/tools.json"; + + // Recommended for consumption. See https://learn.microsoft.com/nuget/api/tools-json + private const string ReleasedAndBlessed = "ReleasedAndBlessed"; + + private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) + { + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = true, + }; + + /// <summary> + /// Gets the latest recommended nuget.exe version. + /// </summary> + /// <remarks> + /// tools.json is sorted by SemVer descending, so the latest recommended + /// version is the first entry with stage ReleasedAndBlessed. + /// </remarks> + public async Task<string> GetLatestVersionAsync() + { + using var request = new HttpRequestMessage(HttpMethod.Get, ToolsUrl); + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(); + NuGetTools tools = JsonSerializer.Deserialize<NuGetTools>(json, s_jsonOptions) ?? + throw new InvalidOperationException($"tools.json response from {ToolsUrl} was empty."); + + NuGetTool? latest = tools.NuGetExe.FirstOrDefault(tool => tool.Stage == ReleasedAndBlessed); + if (latest is null) + { + throw new InvalidOperationException( + $"tools.json from {ToolsUrl} has no ReleasedAndBlessed nuget.exe version."); + } + + return latest.Version; + } + + private sealed record NuGetTools( + [property: JsonPropertyName("nuget.exe")] IReadOnlyList<NuGetTool> NuGetExe); + + private sealed record NuGetTool(string Version, string Stage); +} diff --git a/eng/update-dependencies/NuGetVariableUpdater.cs b/eng/update-dependencies/NuGetVariableUpdater.cs new file mode 100644 index 00000000..0255fa8c --- /dev/null +++ b/eng/update-dependencies/NuGetVariableUpdater.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; + +namespace Microsoft.DotNet.Framework.UpdateDependencies; + +/// <summary> +/// Updates the NuGet CLI version variable to the latest recommended nuget.exe. +/// </summary> +internal sealed class NuGetVariableUpdater(NuGetClient nuget) : IVariableUpdater +{ + /// <inheritdoc/> + public bool ShouldUpdate(string variableKey, IVariableContext variables) + { + // The manifest has a single NuGet variable, "nuget|version". + return variableKey == "nuget|version"; + } + + /// <inheritdoc/> + public async Task<string> GetNewValueAsync(string variableKey, IVariableContext variables) + { + string version = await nuget.GetLatestVersionAsync(); + Console.WriteLine($"NuGet version: {version}"); + return version; + } +} diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index 4d21d34f..c5c06918 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.DotNet.Framework.UpdateDependencies; +using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; using Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; using System.CommandLine; @@ -23,7 +24,14 @@ manifestFileOption }; -var rootCommand = new RootCommand() { updateLcusCommand }; +var updateNuGetCommand = new Command( + name: "update-nuget", + description: "Update the NuGet version variable in the specified manifest file.") +{ + manifestFileOption +}; + +var rootCommand = new RootCommand() { updateLcusCommand, updateNuGetCommand }; updateLcusCommand.SetAction( async parseResult => @@ -43,4 +51,21 @@ } ); +updateNuGetCommand.SetAction( + async parseResult => + { + string manifestFilePath = parseResult.GetRequiredValue(manifestFileOption); + + var manifestVersionsContent = await File.ReadAllTextAsync(manifestFilePath); + var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); + + using var httpClient = new HttpClient(); + var nuget = new NuGetClient(httpClient); + var nugetUpdater = new NuGetVariableUpdater(nuget); + await manifestVersionsContext.ApplyAsync(nugetUpdater); + + await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); + } +); + return rootCommand.Parse(args).Invoke(); From 74bd2370174b14360cd223d2f44784c776a621d9 Mon Sep 17 00:00:00 2001 From: Logan Bussell <loganbussell@microsoft.com> Date: Tue, 13 Oct 2026 09:00:49 -0700 Subject: [PATCH 3/6] Add update-referenceassemblies command for the latest reference assemblies version Add a NuGet v3 feed client and a ReferenceAssembliesVariableUpdater that rewrites referenceassemblies|version to the latest stable Microsoft.NETFramework.ReferenceAssemblies package on the dotnet-public feed. Wire it into update-dependencies as 'update-referenceassemblies [manifest]'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f408eb74-08a1-4f02-9d5b-30e0bd0d7c6f --- ...DotNet.Framework.UpdateDependencies.csproj | 1 + .../NuGet/NuGetFeedClient.cs | 114 ++++++++++++++++++ eng/update-dependencies/Program.cs | 31 ++++- .../ReferenceAssembliesVariableUpdater.cs | 38 ++++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 eng/update-dependencies/NuGet/NuGetFeedClient.cs create mode 100644 eng/update-dependencies/ReferenceAssembliesVariableUpdater.cs diff --git a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj index a6c8c672..9a1c34da 100644 --- a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj +++ b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj @@ -11,6 +11,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="NuGet.Versioning" Version="7.9.0" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" /> </ItemGroup> diff --git a/eng/update-dependencies/NuGet/NuGetFeedClient.cs b/eng/update-dependencies/NuGet/NuGetFeedClient.cs new file mode 100644 index 00000000..63187c48 --- /dev/null +++ b/eng/update-dependencies/NuGet/NuGetFeedClient.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Framework.UpdateDependencies.NuGet; + +/// <summary> +/// Client for a NuGet v3 feed. Reads the service index, then the package's +/// version list. +/// </summary> +internal sealed class NuGetFeedClient(HttpClient httpClient) +{ + private const string PackageBaseAddress = "PackageBaseAddress/3.0.0"; + + private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) + { + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = true, + }; + + /// <summary> + /// Gets the highest stable version of a package. Prerelease versions are + /// ignored. The version list is not assumed to be sorted. + /// </summary> + public async Task<string> GetLatestStableVersionAsync(string serviceIndexUrl, string packageId) + { + string packageBaseAddress = await GetPackageBaseAddressAsync(serviceIndexUrl); + string versionsUrl = + $"{packageBaseAddress.TrimEnd('/')}/{packageId.ToLowerInvariant()}/index.json"; + + PackageVersions packageVersions = await GetJsonAsync<PackageVersions>(versionsUrl); + + var stable = new List<NuGetVersion>(); + foreach (string versionText in packageVersions.Versions) + { + if (!NuGetVersion.TryParse(versionText, out NuGetVersion? version)) + { + throw new InvalidOperationException( + $"Package '{packageId}' has an unparseable version '{versionText}'."); + } + + if (!version.IsPrerelease) + { + stable.Add(version); + } + } + + if (stable.Count == 0) + { + throw new InvalidOperationException( + $"Package '{packageId}' has no stable version at {versionsUrl}."); + } + + // OriginalVersion is the string the feed listed, not a normalized rewrite. + NuGetVersion latest = stable.Max()!; + return latest.OriginalVersion ?? latest.ToNormalizedString(); + } + + private async Task<string> GetPackageBaseAddressAsync(string serviceIndexUrl) + { + ServiceIndex index = await GetJsonAsync<ServiceIndex>(serviceIndexUrl); + + string? packageBaseAddress = index.Resources + .FirstOrDefault(resource => IsPackageBaseAddress(resource.Type)) + ?.Id; + + if (packageBaseAddress is null) + { + throw new InvalidOperationException( + $"NuGet service index {serviceIndexUrl} has no {PackageBaseAddress} resource."); + } + + return packageBaseAddress; + } + + private async Task<T> GetJsonAsync<T>(string url) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + using HttpResponseMessage response = await httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + throw new InvalidOperationException($"NuGet feed returned 404 for {url}."); + } + + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize<T>(json, s_jsonOptions) ?? + throw new InvalidOperationException($"NuGet feed response from {url} was empty."); + } + + private static bool IsPackageBaseAddress(JsonElement type) + { + return type.ValueKind switch + { + JsonValueKind.String => type.GetString() == PackageBaseAddress, + JsonValueKind.Array => type.EnumerateArray().Any(item => item.GetString() == PackageBaseAddress), + _ => false, + }; + } + + private sealed record ServiceIndex(IReadOnlyList<ServiceResource> Resources); + + private sealed record ServiceResource( + [property: JsonPropertyName("@id")] string Id, + [property: JsonPropertyName("@type")] JsonElement Type); + + private sealed record PackageVersions(IReadOnlyList<string> Versions); +} diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index c5c06918..1a2b5bb4 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -31,7 +31,19 @@ manifestFileOption }; -var rootCommand = new RootCommand() { updateLcusCommand, updateNuGetCommand }; +var updateReferenceAssembliesCommand = new Command( + name: "update-referenceassemblies", + description: "Update the reference assemblies version variable in the specified manifest file.") +{ + manifestFileOption +}; + +var rootCommand = new RootCommand() +{ + updateLcusCommand, + updateNuGetCommand, + updateReferenceAssembliesCommand +}; updateLcusCommand.SetAction( async parseResult => @@ -68,4 +80,21 @@ } ); +updateReferenceAssembliesCommand.SetAction( + async parseResult => + { + string manifestFilePath = parseResult.GetRequiredValue(manifestFileOption); + + var manifestVersionsContent = await File.ReadAllTextAsync(manifestFilePath); + var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); + + using var httpClient = new HttpClient(); + var feed = new NuGetFeedClient(httpClient); + var updater = new ReferenceAssembliesVariableUpdater(feed); + await manifestVersionsContext.ApplyAsync(updater); + + await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); + } +); + return rootCommand.Parse(args).Invoke(); diff --git a/eng/update-dependencies/ReferenceAssembliesVariableUpdater.cs b/eng/update-dependencies/ReferenceAssembliesVariableUpdater.cs new file mode 100644 index 00000000..9051ba13 --- /dev/null +++ b/eng/update-dependencies/ReferenceAssembliesVariableUpdater.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; + +namespace Microsoft.DotNet.Framework.UpdateDependencies; + +/// <summary> +/// Updates the reference assemblies version to the latest stable +/// Microsoft.NETFramework.ReferenceAssemblies package. The images install the +/// per-framework packages at this version from the dotnet-public feed, so that +/// feed is the source of the version rather than nuget.org. +/// </summary> +internal sealed class ReferenceAssembliesVariableUpdater(NuGetFeedClient feed) : IVariableUpdater +{ + private const string PackageId = "Microsoft.NETFramework.ReferenceAssemblies"; + + /// <inheritdoc/> + public bool ShouldUpdate(string variableKey, IVariableContext variables) + { + return variableKey == "referenceassemblies|version"; + } + + /// <inheritdoc/> + public async Task<string> GetNewValueAsync(string variableKey, IVariableContext variables) + { + string source = variables["dotnet-public|source"]; + if (string.IsNullOrEmpty(source)) + { + throw new InvalidOperationException("Manifest has no dotnet-public|source variable."); + } + + string version = await feed.GetLatestStableVersionAsync(source, PackageId); + Console.WriteLine($"Reference assemblies version: {version}"); + return version; + } +} From d947fab32c80147e3e772cf3735cb098094aea84 Mon Sep 17 00:00:00 2001 From: Logan Bussell <loganbussell@microsoft.com> Date: Tue, 13 Oct 2026 09:01:48 -0700 Subject: [PATCH 4/6] Add update-servicemonitor command for the latest ServiceMonitor release Add an Octokit client and a ServiceMonitorVariableUpdater that rewrites servicemonitor|version to the latest stable IIS.ServiceMonitor release that publishes ServiceMonitor.exe. Wire it into update-dependencies as 'update-servicemonitor [manifest]'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f408eb74-08a1-4f02-9d5b-30e0bd0d7c6f --- ...DotNet.Framework.UpdateDependencies.csproj | 1 + eng/update-dependencies/Program.cs | 27 +++++++- .../ServiceMonitor/ServiceMonitorClient.cs | 64 +++++++++++++++++++ .../ServiceMonitorVariableUpdater.cs | 29 +++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs create mode 100644 eng/update-dependencies/ServiceMonitorVariableUpdater.cs diff --git a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj index 9a1c34da..09139f69 100644 --- a/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj +++ b/eng/update-dependencies/Microsoft.DotNet.Framework.UpdateDependencies.csproj @@ -12,6 +12,7 @@ <ItemGroup> <PackageReference Include="NuGet.Versioning" Version="7.9.0" /> + <PackageReference Include="Octokit" Version="14.0.0" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" /> </ItemGroup> diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index 1a2b5bb4..3a7d64b1 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -4,6 +4,7 @@ using Microsoft.DotNet.Framework.UpdateDependencies; using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; +using Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; using Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; using System.CommandLine; @@ -38,11 +39,19 @@ manifestFileOption }; +var updateServiceMonitorCommand = new Command( + name: "update-servicemonitor", + description: "Update the ServiceMonitor version variable in the specified manifest file.") +{ + manifestFileOption +}; + var rootCommand = new RootCommand() { updateLcusCommand, updateNuGetCommand, - updateReferenceAssembliesCommand + updateReferenceAssembliesCommand, + updateServiceMonitorCommand }; updateLcusCommand.SetAction( @@ -97,4 +106,20 @@ } ); +updateServiceMonitorCommand.SetAction( + async parseResult => + { + string manifestFilePath = parseResult.GetRequiredValue(manifestFileOption); + + var manifestVersionsContent = await File.ReadAllTextAsync(manifestFilePath); + var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); + + var serviceMonitor = new ServiceMonitorClient(); + var updater = new ServiceMonitorVariableUpdater(serviceMonitor); + await manifestVersionsContext.ApplyAsync(updater); + + await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); + } +); + return rootCommand.Parse(args).Invoke(); diff --git a/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs b/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs new file mode 100644 index 00000000..bcde6e82 --- /dev/null +++ b/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using NuGet.Versioning; +using Octokit; + +namespace Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; + +/// <summary> +/// Client for the IIS.ServiceMonitor GitHub releases. +/// </summary> +internal sealed class ServiceMonitorClient +{ + private const string Owner = "microsoft"; + private const string Repository = "IIS.ServiceMonitor"; + + // The images download this exact file name from the release. + private const string AssetName = "ServiceMonitor.exe"; + + private readonly GitHubClient _github = new(new ProductHeaderValue("dotnet-framework-docker-update-dependencies")); + + /// <summary> + /// Gets the latest stable release version that publishes ServiceMonitor.exe. + /// The returned version has no leading "v"; the manifest URL adds that. + /// </summary> + public async Task<string> GetLatestVersionAsync() + { + IReadOnlyList<Release> releases = await _github.Repository.Release.GetAll(Owner, Repository); + + NuGetVersion? latest = null; + foreach (Release release in releases) + { + if (release.Draft || release.Prerelease || + !release.Assets.Any(asset => asset.Name == AssetName)) + { + continue; + } + + // Tags look like "v2.0.1.10". The manifest stores the part after the "v". + string versionText = release.TagName.StartsWith('v') + ? release.TagName[1..] + : release.TagName; + if (!NuGetVersion.TryParse(versionText, out NuGetVersion? version) || version.IsPrerelease) + { + throw new InvalidOperationException( + $"Release {release.TagName} has {AssetName} but its tag is not a stable version."); + } + + if (latest is null || version > latest) + { + latest = version; + } + } + + if (latest is null) + { + throw new InvalidOperationException( + $"No IIS.ServiceMonitor release has a {AssetName} asset."); + } + + return latest.OriginalVersion ?? latest.ToNormalizedString(); + } +} diff --git a/eng/update-dependencies/ServiceMonitorVariableUpdater.cs b/eng/update-dependencies/ServiceMonitorVariableUpdater.cs new file mode 100644 index 00000000..f15469a1 --- /dev/null +++ b/eng/update-dependencies/ServiceMonitorVariableUpdater.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; + +namespace Microsoft.DotNet.Framework.UpdateDependencies; + +/// <summary> +/// Updates the ServiceMonitor version variable to the latest GitHub release +/// that publishes ServiceMonitor.exe. +/// </summary> +internal sealed class ServiceMonitorVariableUpdater(ServiceMonitorClient serviceMonitor) : IVariableUpdater +{ + /// <inheritdoc/> + public bool ShouldUpdate(string variableKey, IVariableContext variables) + { + // The manifest also has servicemonitor|url, which references this version. + return variableKey == "servicemonitor|version"; + } + + /// <inheritdoc/> + public async Task<string> GetNewValueAsync(string variableKey, IVariableContext variables) + { + string version = await serviceMonitor.GetLatestVersionAsync(); + Console.WriteLine($"ServiceMonitor version: {version}"); + return version; + } +} From 013814717e9b6aded79c9e4aef284b28e9c07822 Mon Sep 17 00:00:00 2001 From: Logan Bussell <loganbussell@microsoft.com> Date: Tue, 13 Oct 2026 09:37:08 -0700 Subject: [PATCH 5/6] Inline NuGet and ServiceMonitor clients into their updaters Each client had a single caller that only forwarded one method. Move that logic into NuGetVariableUpdater and ServiceMonitorVariableUpdater. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f408eb74-08a1-4f02-9d5b-30e0bd0d7c6f --- eng/update-dependencies/NuGet/NuGetClient.cs | 58 ----------------- .../NuGetVariableUpdater.cs | 50 ++++++++++++++- eng/update-dependencies/Program.cs | 7 +- .../ServiceMonitor/ServiceMonitorClient.cs | 64 ------------------- .../ServiceMonitorVariableUpdater.cs | 57 ++++++++++++++++- 5 files changed, 103 insertions(+), 133 deletions(-) delete mode 100644 eng/update-dependencies/NuGet/NuGetClient.cs delete mode 100644 eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs diff --git a/eng/update-dependencies/NuGet/NuGetClient.cs b/eng/update-dependencies/NuGet/NuGetClient.cs deleted file mode 100644 index 3b19f9e8..00000000 --- a/eng/update-dependencies/NuGet/NuGetClient.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.DotNet.Framework.UpdateDependencies.NuGet; - -/// <summary> -/// Client for the NuGet tools.json endpoint, which lists every released -/// nuget.exe. -/// </summary> -internal sealed class NuGetClient(HttpClient httpClient) -{ - private const string ToolsUrl = "https://dist.nuget.org/tools.json"; - - // Recommended for consumption. See https://learn.microsoft.com/nuget/api/tools-json - private const string ReleasedAndBlessed = "ReleasedAndBlessed"; - - private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) - { - RespectNullableAnnotations = true, - RespectRequiredConstructorParameters = true, - }; - - /// <summary> - /// Gets the latest recommended nuget.exe version. - /// </summary> - /// <remarks> - /// tools.json is sorted by SemVer descending, so the latest recommended - /// version is the first entry with stage ReleasedAndBlessed. - /// </remarks> - public async Task<string> GetLatestVersionAsync() - { - using var request = new HttpRequestMessage(HttpMethod.Get, ToolsUrl); - using HttpResponseMessage response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - string json = await response.Content.ReadAsStringAsync(); - NuGetTools tools = JsonSerializer.Deserialize<NuGetTools>(json, s_jsonOptions) ?? - throw new InvalidOperationException($"tools.json response from {ToolsUrl} was empty."); - - NuGetTool? latest = tools.NuGetExe.FirstOrDefault(tool => tool.Stage == ReleasedAndBlessed); - if (latest is null) - { - throw new InvalidOperationException( - $"tools.json from {ToolsUrl} has no ReleasedAndBlessed nuget.exe version."); - } - - return latest.Version; - } - - private sealed record NuGetTools( - [property: JsonPropertyName("nuget.exe")] IReadOnlyList<NuGetTool> NuGetExe); - - private sealed record NuGetTool(string Version, string Stage); -} diff --git a/eng/update-dependencies/NuGetVariableUpdater.cs b/eng/update-dependencies/NuGetVariableUpdater.cs index 0255fa8c..55317afd 100644 --- a/eng/update-dependencies/NuGetVariableUpdater.cs +++ b/eng/update-dependencies/NuGetVariableUpdater.cs @@ -2,15 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Microsoft.DotNet.Framework.UpdateDependencies; /// <summary> /// Updates the NuGet CLI version variable to the latest recommended nuget.exe. /// </summary> -internal sealed class NuGetVariableUpdater(NuGetClient nuget) : IVariableUpdater +internal sealed class NuGetVariableUpdater(HttpClient httpClient) : IVariableUpdater { + private const string ToolsUrl = "https://dist.nuget.org/tools.json"; + + // Recommended for consumption. See https://learn.microsoft.com/nuget/api/tools-json + private const string ReleasedAndBlessed = "ReleasedAndBlessed"; + + private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) + { + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = true, + }; + /// <inheritdoc/> public bool ShouldUpdate(string variableKey, IVariableContext variables) { @@ -21,8 +33,40 @@ public bool ShouldUpdate(string variableKey, IVariableContext variables) /// <inheritdoc/> public async Task<string> GetNewValueAsync(string variableKey, IVariableContext variables) { - string version = await nuget.GetLatestVersionAsync(); + string version = await GetLatestVersionAsync(); Console.WriteLine($"NuGet version: {version}"); return version; } + + /// <summary> + /// Gets the latest recommended nuget.exe version from tools.json. + /// </summary> + /// <remarks> + /// tools.json is sorted by SemVer descending, so the latest recommended + /// version is the first entry with stage ReleasedAndBlessed. + /// </remarks> + private async Task<string> GetLatestVersionAsync() + { + using var request = new HttpRequestMessage(HttpMethod.Get, ToolsUrl); + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(); + NuGetTools tools = JsonSerializer.Deserialize<NuGetTools>(json, s_jsonOptions) ?? + throw new InvalidOperationException($"tools.json response from {ToolsUrl} was empty."); + + NuGetTool? latest = tools.NuGetExe.FirstOrDefault(tool => tool.Stage == ReleasedAndBlessed); + if (latest is null) + { + throw new InvalidOperationException( + $"tools.json from {ToolsUrl} has no ReleasedAndBlessed nuget.exe version."); + } + + return latest.Version; + } + + private sealed record NuGetTools( + [property: JsonPropertyName("nuget.exe")] IReadOnlyList<NuGetTool> NuGetExe); + + private sealed record NuGetTool(string Version, string Stage); } diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index 3a7d64b1..a0d16875 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -4,7 +4,6 @@ using Microsoft.DotNet.Framework.UpdateDependencies; using Microsoft.DotNet.Framework.UpdateDependencies.NuGet; -using Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; using Microsoft.DotNet.Framework.UpdateDependencies.UpdateCatalog; using System.CommandLine; @@ -81,8 +80,7 @@ var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); using var httpClient = new HttpClient(); - var nuget = new NuGetClient(httpClient); - var nugetUpdater = new NuGetVariableUpdater(nuget); + var nugetUpdater = new NuGetVariableUpdater(httpClient); await manifestVersionsContext.ApplyAsync(nugetUpdater); await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); @@ -114,8 +112,7 @@ var manifestVersionsContent = await File.ReadAllTextAsync(manifestFilePath); var manifestVersionsContext = new ManifestVariableContext(manifestVersionsContent); - var serviceMonitor = new ServiceMonitorClient(); - var updater = new ServiceMonitorVariableUpdater(serviceMonitor); + var updater = new ServiceMonitorVariableUpdater(); await manifestVersionsContext.ApplyAsync(updater); await File.WriteAllTextAsync(manifestFilePath, manifestVersionsContext.Content); diff --git a/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs b/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs deleted file mode 100644 index bcde6e82..00000000 --- a/eng/update-dependencies/ServiceMonitor/ServiceMonitorClient.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using NuGet.Versioning; -using Octokit; - -namespace Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; - -/// <summary> -/// Client for the IIS.ServiceMonitor GitHub releases. -/// </summary> -internal sealed class ServiceMonitorClient -{ - private const string Owner = "microsoft"; - private const string Repository = "IIS.ServiceMonitor"; - - // The images download this exact file name from the release. - private const string AssetName = "ServiceMonitor.exe"; - - private readonly GitHubClient _github = new(new ProductHeaderValue("dotnet-framework-docker-update-dependencies")); - - /// <summary> - /// Gets the latest stable release version that publishes ServiceMonitor.exe. - /// The returned version has no leading "v"; the manifest URL adds that. - /// </summary> - public async Task<string> GetLatestVersionAsync() - { - IReadOnlyList<Release> releases = await _github.Repository.Release.GetAll(Owner, Repository); - - NuGetVersion? latest = null; - foreach (Release release in releases) - { - if (release.Draft || release.Prerelease || - !release.Assets.Any(asset => asset.Name == AssetName)) - { - continue; - } - - // Tags look like "v2.0.1.10". The manifest stores the part after the "v". - string versionText = release.TagName.StartsWith('v') - ? release.TagName[1..] - : release.TagName; - if (!NuGetVersion.TryParse(versionText, out NuGetVersion? version) || version.IsPrerelease) - { - throw new InvalidOperationException( - $"Release {release.TagName} has {AssetName} but its tag is not a stable version."); - } - - if (latest is null || version > latest) - { - latest = version; - } - } - - if (latest is null) - { - throw new InvalidOperationException( - $"No IIS.ServiceMonitor release has a {AssetName} asset."); - } - - return latest.OriginalVersion ?? latest.ToNormalizedString(); - } -} diff --git a/eng/update-dependencies/ServiceMonitorVariableUpdater.cs b/eng/update-dependencies/ServiceMonitorVariableUpdater.cs index f15469a1..031861a6 100644 --- a/eng/update-dependencies/ServiceMonitorVariableUpdater.cs +++ b/eng/update-dependencies/ServiceMonitorVariableUpdater.cs @@ -2,7 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.DotNet.Framework.UpdateDependencies.ServiceMonitor; +using NuGet.Versioning; +using Octokit; namespace Microsoft.DotNet.Framework.UpdateDependencies; @@ -10,8 +11,16 @@ namespace Microsoft.DotNet.Framework.UpdateDependencies; /// Updates the ServiceMonitor version variable to the latest GitHub release /// that publishes ServiceMonitor.exe. /// </summary> -internal sealed class ServiceMonitorVariableUpdater(ServiceMonitorClient serviceMonitor) : IVariableUpdater +internal sealed class ServiceMonitorVariableUpdater : IVariableUpdater { + private const string Owner = "microsoft"; + private const string Repository = "IIS.ServiceMonitor"; + + // The images download this exact file name from the release. + private const string AssetName = "ServiceMonitor.exe"; + + private readonly GitHubClient _github = new(new ProductHeaderValue("dotnet-framework-docker-update-dependencies")); + /// <inheritdoc/> public bool ShouldUpdate(string variableKey, IVariableContext variables) { @@ -22,8 +31,50 @@ public bool ShouldUpdate(string variableKey, IVariableContext variables) /// <inheritdoc/> public async Task<string> GetNewValueAsync(string variableKey, IVariableContext variables) { - string version = await serviceMonitor.GetLatestVersionAsync(); + string version = await GetLatestVersionAsync(); Console.WriteLine($"ServiceMonitor version: {version}"); return version; } + + /// <summary> + /// Gets the latest stable release version that publishes ServiceMonitor.exe. + /// The returned version has no leading "v"; the manifest URL adds that. + /// </summary> + private async Task<string> GetLatestVersionAsync() + { + IReadOnlyList<Release> releases = await _github.Repository.Release.GetAll(Owner, Repository); + + NuGetVersion? latest = null; + foreach (Release release in releases) + { + if (release.Draft || release.Prerelease || + !release.Assets.Any(asset => asset.Name == AssetName)) + { + continue; + } + + // Tags look like "v2.0.1.10". The manifest stores the part after the "v". + string versionText = release.TagName.StartsWith('v') + ? release.TagName[1..] + : release.TagName; + if (!NuGetVersion.TryParse(versionText, out NuGetVersion? version) || version.IsPrerelease) + { + throw new InvalidOperationException( + $"Release {release.TagName} has {AssetName} but its tag is not a stable version."); + } + + if (latest is null || version > latest) + { + latest = version; + } + } + + if (latest is null) + { + throw new InvalidOperationException( + $"No IIS.ServiceMonitor release has a {AssetName} asset."); + } + + return latest.OriginalVersion ?? latest.ToNormalizedString(); + } } From 1dfe614dbdfa5bd84e54541128c6387f40ca2d0e Mon Sep 17 00:00:00 2001 From: Logan Bussell <loganbussell@microsoft.com> Date: Thu, 15 Oct 2026 09:19:01 -0700 Subject: [PATCH 6/6] Tighten Server 2025 catalog matching Require the Server title marker when selecting the 24H2 x64 Update Catalog result so Windows 11 packages are excluded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f408eb74-08a1-4f02-9d5b-30e0bd0d7c6f --- eng/update-dependencies/LcuVariableUpdater.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/update-dependencies/LcuVariableUpdater.cs b/eng/update-dependencies/LcuVariableUpdater.cs index 507cbdde..55802ddc 100644 --- a/eng/update-dependencies/LcuVariableUpdater.cs +++ b/eng/update-dependencies/LcuVariableUpdater.cs @@ -118,6 +118,6 @@ private async Task<string> GetKbDownloadUrlAsync(string kb, string windowsVersio [GeneratedRegex(@"server.*21H2.*x64", RegexOptions.IgnoreCase, "en-US")] private static partial Regex Server2022TitleRegex { get; } - [GeneratedRegex(@"24H2.*x64", RegexOptions.IgnoreCase, "en-US")] + [GeneratedRegex(@"server.*24H2.*x64", RegexOptions.IgnoreCase, "en-US")] private static partial Regex Server2025TitleRegex { get; } }