Repository navigation
Add Aspire Dashboard support to update-dependencies tooling #6726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0b1885f
3a96a43
246bca4
654f945
447d363
01a10c3
4930752
f1aeb5d
19752c5
881e3b0
e266c80
e72fef9
6230041
c107be9
0a23d0a
0097634
92d0d91
12faf4f
113099a
8a78c1d
5770f23
4fcd3df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| $(McrTagsYmlRepo:aspire-dashboard) | ||
| $(McrTagsYmlTagGroup:9.5) | ||
| $(McrTagsYmlTagGroup:latest) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| [*.cs] | ||
| # CS1591: Missing XML comment for publicly visible type or member | ||
| # https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1591 | ||
| # Since this is a shared project, public members should be documented. | ||
| dotnet_diagnostic.CS1591.severity = suggestion |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // 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.RegularExpressions; | ||
| using NuGet.Versioning; | ||
|
|
||
| namespace Microsoft.DotNet.Docker.Shared; | ||
|
|
||
| /// <summary> | ||
| /// Represents the version of a .NET build artifact. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Additional notes about .NET versions: | ||
| /// - <see cref="SemanticVersion.Patch"/>: For runtime versions, this is number typically starts at | ||
| /// 0 and counts up with new releases. For SDK versions, this number starts at 100 and can be as | ||
| /// high as 400+. | ||
| /// - <see cref="SemanticVersion.Release"/>: For stable release .NET versions, this is typically | ||
| /// empty. For preview .NET versions, it is usually a suffix like "preview.6.12345.678-shipping". | ||
| /// For internal builds of stable versions, it can be a suffix like "servicing.12345.6". | ||
| /// </remarks> | ||
| public sealed partial class DotNetVersion(SemanticVersion version) : SemanticVersion(version) | ||
| { | ||
| /// <summary> | ||
| /// Parse a string into a <see cref="DotNetVersion"/>. This is not a | ||
| /// "strict" parse. Major-only versions are allowed. Unspecified minor and | ||
| /// patch versions default to 0. | ||
| /// </summary> | ||
| public static new DotNetVersion Parse(string versionString) | ||
| { | ||
| // SemanticVersion only handles full, strict semantic versions. | ||
| // If we can parse it as a full semantic version, use that. It resolves | ||
| // full build versions correctly including prerelease suffixes (e.g. '-preview.7') | ||
| if (SemanticVersion.TryParse(versionString, out var fullSemanticVersion)) | ||
| { | ||
| return new DotNetVersion(fullSemanticVersion); | ||
| } | ||
|
|
||
| // We also need to allow major-only and major.minor versions, so fall | ||
| // back to simple parsing when that happens. | ||
| // A valid major.minor.patch version would have been handled above so | ||
| // we don't need to account for that in our parsing. | ||
| // Don't use System.Version.Parse here, because it defaults unmatched | ||
| // minor/patch versions to -1, whereas we want them to default to 0. | ||
| var matchGroups = MajorMinorVersionRegex.Match(versionString).Groups; | ||
| int major = int.Parse(matchGroups["major"].Value); | ||
| int minor = int.TryParse(matchGroups["minor"].Value.TrimStart('.'), out int minorValue) ? minorValue : 0; | ||
| return new DotNetVersion(new SemanticVersion(major, minor, 0)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Implicitly converts a string to a <see cref="DotNetVersion"/>. | ||
| /// </summary> | ||
| public static implicit operator DotNetVersion(string versionString) | ||
| { | ||
| SemanticVersion version = Parse(versionString); | ||
| return new(version); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Formats the version to a string with the specified number of parts. | ||
| /// </summary> | ||
| /// <param name="parts"> | ||
| /// Number of parts to include. If greater than 3, the full version is | ||
| /// returned, whether or not there are any additional parts. | ||
| /// </param> | ||
| public string ToString(int parts) => parts switch | ||
| { | ||
| < 1 => throw new ArgumentOutOfRangeException(nameof(parts), "Cannot format less than 1 version part"), | ||
| 1 => $"{Major}", | ||
| 2 => $"{Major}.{Minor}", | ||
| 3 => $"{Major}.{Minor}.{Patch}", | ||
| > 3 => ToString(), | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Whether the .NET version is a public preview version. | ||
| /// </summary> | ||
| public bool IsPublicPreview => | ||
| // Assume all "preview" versions are public, non-security releases. | ||
| // Assume that all "rc" and "servicing" versions are internal security releases. | ||
| Release.StartsWith("preview", StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| /// <summary> | ||
| /// Determines if the version is considered a GA version. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// RTM versions are also accepted as GA versions. | ||
| /// </remarks> | ||
| public bool IsGA => ReleaseLabels.Contains("rtm") || !IsPrerelease; | ||
|
|
||
| [GeneratedRegex(@"^(?<major>\d+)(?<minor>\.\d+)?")] | ||
| private static partial Regex MajorMinorVersionRegex { get; } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
| <RootNamespace>Microsoft.DotNet.Docker.Shared</RootNamespace> | ||
|
|
||
| <!-- Enables CS1591, which lets us know when public members aren't documented --> | ||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="NuGet.Versioning" Version="6.14.0" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Microsoft.DotNet.Docker.Shared | ||
|
|
||
| This class library contains code that's shared between the | ||
| [`update-dependencies` tool](../update-dependencies/README.md) and the | ||
| [.NET Docker test project](../../tests/Microsoft.DotNet.Docker.Tests/). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Microsoft.DotNet.ProductConstructionService.Client.Models; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Dotnet.Docker; | ||
|
|
||
| internal class AspireBuildUpdaterService( | ||
| ILogger<AspireBuildUpdaterService> logger, | ||
| HttpClient httpClient, | ||
| Func<string, IManifestVariables> manifestVariablesFactory | ||
| ) : IBuildUpdaterService | ||
| { | ||
| private readonly ILogger<AspireBuildUpdaterService> _logger = logger; | ||
| private readonly HttpClient _httpClient = httpClient; | ||
| private readonly Func<string, IManifestVariables> _createManifestVariables = manifestVariablesFactory; | ||
|
|
||
| /// <summary> | ||
| /// Given a BAR (https://aka.ms/bar) build of Aspire, updates the Aspire Dashboard build version. | ||
| /// </summary> | ||
| public async Task<int> UpdateFrom(Build build, CreatePullRequestOptions pullRequestOptions) | ||
| { | ||
| if (build.GetBuildRepo() != BuildRepo.Aspire) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"AspireBuildUpdaterService cannot process builds from repo: {build.GitHubRepository}"); | ||
| } | ||
|
|
||
| // Read manifest.versions.json early so that we can fail fast in case we have the wrong file path. | ||
| var manifestVariables = _createManifestVariables(pullRequestOptions.GetManifestVersionsFilePath()); | ||
| var branch = manifestVariables.GetValue("branch"); | ||
| var dashboardBaseUrl = manifestVariables.GetValue($"aspire-dashboard|base-url|{branch}"); | ||
|
|
||
| var dashboardAssets = build.Assets.Where(asset => | ||
| asset.Name.Contains("aspire-dashboard-linux-x64") | ||
| || asset.Name.Contains("aspire-dashboard-linux-arm64")); | ||
|
|
||
| if (!dashboardAssets.Any()) | ||
| { | ||
| throw new InvalidOperationException($"Could not find aspire-dashboard-linux-* assets in build {build.Id}"); | ||
| } | ||
|
|
||
| _logger.LogInformation("Found Aspire build version: {version}", dashboardAssets.First().Version); | ||
|
|
||
| IEnumerable<string?> dashboardChecksums = await dashboardAssets | ||
| .Select(asset => GetDashboardUrl(dashboardBaseUrl, asset)) | ||
| .ToAsyncEnumerable() | ||
| .SelectAwait(async url => await GetChecksumAsync(url)) | ||
| .ToListAsync(); | ||
|
|
||
| var dashboardChecksumInfos = dashboardAssets.Zip(dashboardChecksums); | ||
|
|
||
| var manifestVersionsPath = pullRequestOptions.GetManifestVersionsFilePath(); | ||
|
|
||
| var version = dashboardAssets.First().Version; | ||
| var majorMinorVersion = VersionHelper.ResolveMajorMinorVersion(version); | ||
|
|
||
| List<VariableUpdateInfo> variableUpdates = | ||
| [ | ||
| new VariableUpdateInfo("aspire-dashboard|build-version", version), | ||
| new VariableUpdateInfo("aspire-dashboard|product-version", VersionHelper.ResolveProductVersion(version)), | ||
| new VariableUpdateInfo("aspire-dashboard|fixed-tag", VersionHelper.ResolveProductVersion(version)), | ||
| new VariableUpdateInfo("aspire-dashboard|minor-tag", majorMinorVersion.ToString(2)), | ||
| new VariableUpdateInfo("aspire-dashboard|major-tag", majorMinorVersion.Major.ToString()), | ||
| ]; | ||
|
|
||
| variableUpdates.AddRange(dashboardChecksumInfos | ||
| // Filter out null checksums (indicates the checksum was not found above) | ||
| .Where(info => info.Second is not null) | ||
| .Select(info => | ||
| { | ||
| var (asset, checksum) = info; | ||
| var arch = asset.Name.Contains("arm64") ? "arm64" : "x64"; | ||
| // Null-forgiving operator is OK since we filtered out null checksums above. | ||
| return new VariableUpdateInfo($"aspire-dashboard|linux|{arch}|sha", checksum!); | ||
| })); | ||
|
|
||
| var dependencyUpdaters = variableUpdates | ||
| .Select(updateInfo => new VariableUpdater(manifestVersionsPath, updateInfo)); | ||
|
|
||
| var updateDependencies = new SpecificCommand(); | ||
| updateDependencies.CustomUpdateInfos.AddRange(variableUpdates); | ||
| updateDependencies.CustomUpdaters.AddRange(dependencyUpdaters); | ||
|
|
||
| // Don't pass in any through options, since we calculated all of the variables | ||
| // and their new versions to update above. Everything is handled through CustomUpdateInfos. | ||
| // Using the SpecificCommand here just allows us to easily create automated pull requests. | ||
| var updateDependenciesOptions = SpecificCommandOptions.FromPullRequestOptions(pullRequestOptions); | ||
| return await updateDependencies.ExecuteAsync(updateDependenciesOptions); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the full download URL to an Aspire Dashboard asset given its base | ||
| /// URL and the BAR asset information. | ||
| /// </summary> | ||
| private string GetDashboardUrl(string baseUrl, Asset dashboardAsset) => $"{baseUrl}/{dashboardAsset.Name}"; | ||
|
|
||
| /// <summary> | ||
| /// Manually compute the checksum of an Aspire Dashboard asset by downloading it from | ||
| /// <paramref name="url"/> and hashing the contents. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Remove once https://github.com/dotnet/dotnet-docker/issues/6568 is completed. | ||
| /// </remarks> | ||
| /// <returns> | ||
| /// Null if the checksum could not be computed for any reason. An error will | ||
| /// be logged to the console if this happens. | ||
| /// </returns> | ||
| private Task<string?> GetChecksumAsync(string url) => ChecksumHelper.ComputeChecksumShaAsync(_httpClient, url); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As FYI, we should be able to upload checksums for the dashboard very easily if you prefer.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's tracked by microsoft/aspire#10605. If that could be done soon that'd be awesome. We still need to leave this code in until both microsoft/aspire#10605 and #6568 are complete. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Microsoft.DotNet.ProductConstructionService.Client.Models; | ||
|
|
||
| namespace Dotnet.Docker; | ||
|
|
||
| /// <summary> | ||
| /// Extensions for .NET build asset registry <see cref="Build"/>s. | ||
| /// </summary> | ||
| internal static class BuildExtensions | ||
| { | ||
| /// <summary> | ||
| /// Given a <see cref="Build"/>, maps its source repository (either GitHub | ||
| /// or Azure Devops) to a supported <see cref="BuildRepo"/> enum value. | ||
| /// </summary> | ||
| public static BuildRepo GetBuildRepo(this Build build) | ||
|
lbussell marked this conversation as resolved.
Resolved
|
||
| { | ||
| string repo = build.GitHubRepository ?? build.AzureDevOpsRepository; | ||
| return repo switch | ||
| { | ||
| "https://github.com/dotnet/dotnet" or "https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet" => BuildRepo.Vmr, | ||
| "https://github.com/dotnet/aspire" or "https://dev.azure.com/dnceng/internal/_git/dotnet-aspire" => BuildRepo.Aspire, | ||
| _ => throw new InvalidOperationException($"Build {build.Id} was from unsupported repository '{repo}'"), | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Dotnet.Docker; | ||
|
|
||
| /// <summary> | ||
| /// Represents supported source repositories for builds from the .NET build | ||
| /// asset registry. | ||
| /// </summary> | ||
| internal enum BuildRepo | ||
| { | ||
| Vmr, | ||
| Aspire, | ||
| // When adding new repos, also update "BuildExtensions.GetBuildRepo()" | ||
| } |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.