diff --git a/eng/update-dependencies/BuildUpdaterService.cs b/eng/update-dependencies/BuildUpdaterService.cs index 73ff8996dd..1325e6a262 100644 --- a/eng/update-dependencies/BuildUpdaterService.cs +++ b/eng/update-dependencies/BuildUpdaterService.cs @@ -57,7 +57,7 @@ public async Task UpdateFrom(Build build, CreatePullRequestOptions pullRequ string productCommitsJson = await _buildAssetService.GetAssetTextContentsAsync(productCommitsAsset); ProductCommits productCommits = ProductCommits.FromJson(productCommitsJson); - Version dockerfileVersion = ResolveMajorMinorVersion(productCommits.Sdk.Version); + Version dockerfileVersion = VersionHelper.ResolveMajorMinorVersion(productCommits.Sdk.Version); // Run old update-dependencies command using the resolved versions var updateDependencies = new SpecificCommand(); @@ -82,6 +82,7 @@ public async Task UpdateFrom(Build build, CreatePullRequestOptions pullRequ AzdoOrganization = pullRequestOptions.AzdoOrganization, AzdoProject = pullRequestOptions.AzdoProject, AzdoRepo = pullRequestOptions.AzdoRepo, + AzdoToken = pullRequestOptions.AzdoToken, VersionSourceName = pullRequestOptions.VersionSourceName, SourceBranch = pullRequestOptions.SourceBranch, TargetBranch = pullRequestOptions.TargetBranch, @@ -90,17 +91,6 @@ public async Task UpdateFrom(Build build, CreatePullRequestOptions pullRequ return await updateDependencies.ExecuteAsync(updateDependenciesOptions); } - private static Version ResolveMajorMinorVersion(string versionString) - { - string[] versionParts = versionString.Split('.'); - if (versionParts.Length < 2) - { - throw new InvalidOperationException($"Could not parse major-minor version from '{versionString}'."); - } - - return new Version(major: int.Parse(versionParts[0]), minor: int.Parse(versionParts[1])); - } - private static bool IsVmrBuild(Build build) { string repo = build.GitHubRepository ?? build.AzureDevOpsRepository; diff --git a/eng/update-dependencies/CreatePullRequestOptions.cs b/eng/update-dependencies/CreatePullRequestOptions.cs index 37db9078c9..cb5057568d 100644 --- a/eng/update-dependencies/CreatePullRequestOptions.cs +++ b/eng/update-dependencies/CreatePullRequestOptions.cs @@ -6,7 +6,7 @@ namespace Dotnet.Docker; -public abstract class CreatePullRequestOptions +public abstract record CreatePullRequestOptions { private string? _targetBranch = null; diff --git a/eng/update-dependencies/FromBuildOptions.cs b/eng/update-dependencies/FromBuildOptions.cs index f8fe9139e4..f5ae9736f1 100644 --- a/eng/update-dependencies/FromBuildOptions.cs +++ b/eng/update-dependencies/FromBuildOptions.cs @@ -6,7 +6,7 @@ namespace Dotnet.Docker; -internal class FromBuildOptions : CreatePullRequestOptions, IOptions +internal record FromBuildOptions : CreatePullRequestOptions, IOptions { public required int Id { get; init; } diff --git a/eng/update-dependencies/FromChannelOptions.cs b/eng/update-dependencies/FromChannelOptions.cs index 973ef64568..4b9c674815 100644 --- a/eng/update-dependencies/FromChannelOptions.cs +++ b/eng/update-dependencies/FromChannelOptions.cs @@ -6,7 +6,7 @@ namespace Dotnet.Docker; -internal class FromChannelOptions : CreatePullRequestOptions, IOptions +internal record FromChannelOptions : CreatePullRequestOptions, IOptions { public required int Channel { get; init; } public required string Repo { get; init; } diff --git a/eng/update-dependencies/FromStagingPipelineCommand.cs b/eng/update-dependencies/FromStagingPipelineCommand.cs new file mode 100644 index 0000000000..9502382a6a --- /dev/null +++ b/eng/update-dependencies/FromStagingPipelineCommand.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Dotnet.Docker.Model.Release; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Dotnet.Docker; + +internal partial class FromStagingPipelineCommand(ILogger logger) + : BaseCommand +{ + private readonly ILogger _logger = logger; + + public override async Task ExecuteAsync(FromStagingPipelineOptions options) + { + if (options.Internal) + { + throw new NotImplementedException("Updating Dockerfiles for internal builds is not implemented yet."); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(options.AzdoOrganization); + ArgumentException.ThrowIfNullOrWhiteSpace(options.AzdoProject); + + _logger.LogInformation( + "Updating dependencies based on staging pipeline run ID {options.StagingPipelineRunId}", + options.StagingPipelineRunId); + + // Each pipeline run has a corresponding blob container named stage-${options.StagingPipelineRunId}. + // Release metadata is stored in metadata/ReleaseManifest.json. + // Release assets are stored individually under in assets/shipping/assets/[Sdk|Runtime|aspnetcore|...]. + + // Get release manifest from staging storage account + var storageAccount = new StorageAccount(options.StagingStorageAccount); + var releaseManifestJson = await storageAccount.DownloadTextAsync( + containerName: $"stage-{options.StagingPipelineRunId}", + blobPath: "metadata/ReleaseManifest.json"); + + var buildManifest = BuildManifest.FromJson(releaseManifestJson); + var allAssets = buildManifest.AllAssets.ToList(); + + // Look through all the assets and get the version of the highest SDK feature band + var sdkAssets = allAssets + .Where(asset => SdkRegex.IsMatch(asset.Name)) + .ToList(); + var sdkVersions = sdkAssets.Select(asset => asset.Version); + string highestSdkVersion = VersionHelper.GetHighestSdkVersion(sdkVersions); + string dockerfileVersion = VersionHelper.ResolveMajorMinorVersion(highestSdkVersion).ToString(); + + string runtimeVersion = allAssets + .Where(asset => RuntimeRegex.IsMatch(asset.Name)) + .Select(asset => asset.Version) + .First(); + + string aspnetVersion = allAssets + .Where(asset => AspNetCoreRegex.IsMatch(asset.Name)) + .Select(asset => asset.Version) + .First(); + + _logger.LogInformation( + """ + Resolved .NET versions: + .NET: {dockerfileVersion} + - SDK: {highestSdkVersion} + - Runtime: {runtimeVersion} + - ASP.NET Core: {aspnetVersion} + """, + dockerfileVersion, highestSdkVersion, runtimeVersion, aspnetVersion); + + // Run old update-dependencies command using the resolved versions + var updateDependencies = new SpecificCommand(); + var updateDependenciesOptions = new SpecificCommandOptions() + { + DockerfileVersion = dockerfileVersion.ToString(), + ProductVersions = new Dictionary() + { + // "dotnet" version is required. It sets the "dotnet|*|product-version" + // variable which is used for runtime-deps, runtime, and aspnet tags. + { "dotnet", runtimeVersion }, + { "runtime", runtimeVersion }, + { "aspnet", aspnetVersion }, + { "aspnet-composite", aspnetVersion }, + { "sdk", highestSdkVersion }, + }, + + // Pass through all properties of CreatePullRequestOptions + User = options.User, + Email = options.Email, + Password = options.Password, + AzdoOrganization = options.AzdoOrganization, + AzdoProject = options.AzdoProject, + AzdoRepo = options.AzdoRepo, + VersionSourceName = options.VersionSourceName, + SourceBranch = options.SourceBranch, + TargetBranch = options.TargetBranch, + }; + + return await updateDependencies.ExecuteAsync(updateDependenciesOptions); + } + + // Examples: + // - "Sdk/8.0.117-servicing.25269.14/dotnet-sdk-8.0.117-linux-x64.tar.gz" + // - "Sdk/8.0.310-servicing.25113.14/dotnet-sdk-8.0.310-linux-musl-arm64.tar.gz" + [GeneratedRegex(@"Sdk/.*/dotnet-sdk-.*-linux-x64.tar.gz$")] + private static partial Regex SdkRegex { get; } + + // "aspnetcore/Runtime/8.0.14-servicing.25112.21/aspnetcore-runtime-8.0.14-linux-x64.tar.gz", + [GeneratedRegex(@"aspnetcore/Runtime/.*/aspnetcore-runtime-.*-linux-x64.tar.gz")] + private static partial Regex AspNetCoreRegex { get; } + + // "Runtime/8.0.14-servicing.25111.18/dotnet-runtime-8.0.14-linux-x64.tar.gz" + [GeneratedRegex(@"Runtime/.*/dotnet-runtime-.*-linux-x64.tar.gz")] + private static partial Regex RuntimeRegex { get; } +} diff --git a/eng/update-dependencies/FromStagingPipelineOptions.cs b/eng/update-dependencies/FromStagingPipelineOptions.cs new file mode 100644 index 0000000000..1a0153dd44 --- /dev/null +++ b/eng/update-dependencies/FromStagingPipelineOptions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.CommandLine; + +namespace Dotnet.Docker; + +internal record FromStagingPipelineOptions : CreatePullRequestOptions, IOptions +{ + /// + /// The staging pipeline run ID to use as a source for the update. + /// + public required int StagingPipelineRunId { get; init; } + + /// + /// Whether or not to use the internal versions of the staged build. + /// + public bool Internal { get; init; } = false; + + /// + /// This Azure Storage Account will be used as a source for the update. + /// This should be one of two storage accounts: dotnetstagetest or dotnetstage. + /// + public required string StagingStorageAccount { get; init; } + + public static new List Arguments { get; } = + [ + new Argument("staging-pipeline-run-id") + { + Arity = ArgumentArity.ExactlyOne, + Description = "The staging pipeline run ID to use as a source for the update" + }, + new Argument("staging-storage-account") + { + Arity = ArgumentArity.ExactlyOne, + Description = "The Azure Storage Account to use as a source for the update." + + " This should be one of two storage accounts: dotnetstagetest or dotnetstage." + + " For example: https://dotnetstagetest.blob.core.windows.net/" + }, + ..CreatePullRequestOptions.Arguments, + ]; + + public static new List