Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0b1885f
Add BuildRepo enum
lbussell Oct 13, 2025
3a96a43
Make build updater a keyed singleton service based on repo
lbussell Oct 13, 2025
246bca4
Rename BuildUpdaterService to VmrBuildUpdaterService
lbussell Oct 13, 2025
654f945
Simplify repo error message
lbussell Oct 13, 2025
447d363
Add updater class for Aspire
lbussell Oct 13, 2025
01a10c3
Add super basic variable updater implementation
lbussell Oct 14, 2025
4930752
Extract interface from ManifestVariables class
lbussell Oct 14, 2025
f1aeb5d
Add manifest variables factory in DI container
lbussell Oct 14, 2025
19752c5
Compute new Aspire Dashboard versions and checksums
lbussell Oct 14, 2025
881e3b0
Add custom updater support to SpecificCommand
lbussell Oct 14, 2025
e266c80
Call custom updaters from AspireBuildUpdaterService
lbussell Oct 14, 2025
e72fef9
Move aspire dashboard Dockerfiles out of version/OS directories
lbussell Oct 14, 2025
6230041
Remove versions from aspire dashboard variable names
lbussell Oct 14, 2025
c107be9
Remove commented out code.
lbussell Oct 15, 2025
0a23d0a
Update baselines
lbussell Oct 15, 2025
0097634
Merge remote-tracking branch 'upstream/nightly' into aspire-automation
lbussell Oct 15, 2025
92d0d91
Allow optional version and OS segments when parsing Dockerfile paths …
lbussell Oct 15, 2025
12faf4f
Use ProductVersion from manifest instead of parsing it from Dockerfil…
lbussell Oct 15, 2025
113099a
Add missing package
lbussell Oct 15, 2025
8a78c1d
Add SpecificCommandOptions static constructor from pull request options
lbussell Oct 15, 2025
5770f23
Add comment explaining why not to use System.Version.Parse
lbussell Oct 15, 2025
4fcd3df
Share DotNetVersion code between update-deps and tests
lbussell Oct 15, 2025
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
1 change: 1 addition & 0 deletions Microsoft.DotNet.Docker.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Platform Name="x86" />
</Configurations>
<Project Path="eng/update-dependencies/update-dependencies.csproj" />
<Project Path="eng/shared/Microsoft.DotNet.Docker.Shared.csproj" />
<Project Path="tests/UpdateDependencies.Tests/UpdateDependencies.Tests.csproj" />
<Project Path="tests/Microsoft.DotNet.Docker.Tests/Microsoft.DotNet.Docker.Tests.csproj" />
</Solution>
4 changes: 2 additions & 2 deletions README.aspire-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ Limits are per-resource. For example, a `MaxLogCount` value of 10,000 configures

Tags | Dockerfile | OS Version
-----------| -------------| -------------
9.5.0, 9.5, 9, latest | [Dockerfile](src/aspire-dashboard/9.5/azurelinux-distroless/amd64/Dockerfile) | Azure Linux 3.0
9.5.0, 9.5, 9, latest | [Dockerfile](src/aspire-dashboard/amd64/Dockerfile) | Azure Linux 3.0

### Linux arm64 Tags

Tags | Dockerfile | OS Version
-----------| -------------| -------------
9.5.0, 9.5, 9, latest | [Dockerfile](src/aspire-dashboard/9.5/azurelinux-distroless/arm64v8/Dockerfile) | Azure Linux 3.0
9.5.0, 9.5, 9, latest | [Dockerfile](src/aspire-dashboard/arm64v8/Dockerfile) | Azure Linux 3.0
<!--End of generated tags-->

*Tags not listed in the table above are not supported. See the [Supported Tags Policy](https://github.com/dotnet/dotnet-docker/blob/main/documentation/supported-tags.md). See the [full list of tags](https://mcr.microsoft.com/v2/dotnet/nightly/aspire-dashboard/tags/list) for all supported and unsupported tags.*
Expand Down
10 changes: 5 additions & 5 deletions eng/dockerfile-templates/aspire-dashboard/Dockerfile.linux
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
OS_VERSION_NUMBER),
cat(ARCH_VERSIONED, "/buildpack-deps:", osVersionBase, "-curl")) ^

set baseUrl to VARIABLES[cat("aspire-dashboard|", aspireMajorMinor, "|base-url|", VARIABLES["branch"])] ^
set baseUrl to VARIABLES[cat("aspire-dashboard|base-url|", VARIABLES["branch"])] ^
set isInternal to find(baseUrl, "artifacts.visualstudio.com") >= 0 ^

set buildVersion to VARIABLES[cat("aspire-dashboard|", aspireMajorMinor, "|build-version")] ^
set buildVersion to VARIABLES["aspire-dashboard|build-version"] ^
set aspireVersionVariable to when(find(buildVersion, '-rtm') >= 0 || find(buildVersion, '-servicing') >= 0, "product-version", "build-version") ^
set aspireVersion to VARIABLES[cat("aspire-dashboard|", aspireMajorMinor, "|", aspireVersionVariable)] ^
set aspireVersion to VARIABLES[cat("aspire-dashboard|", aspireVersionVariable)] ^
set versionFolder to when(buildVersion != aspireVersion, buildVersion, '$dotnet_aspire_version') ^
set aspireBaseUrl to cat(VARIABLES[cat("aspire-dashboard|", aspireMajorMinor, "|base-url|", VARIABLES["branch"])], "/aspire/", versionFolder, "/") ^
set aspireBaseUrl to cat(VARIABLES[cat("aspire-dashboard|base-url|", VARIABLES["branch"])], "/aspire/", versionFolder, "/") ^

set downloadUrl to cat(aspireBaseUrl, "aspire-dashboard-linux-", ARCH_SHORT, ".zip") ^
set outFile to "aspire_dashboard.zip" ^
set appDir to "/app" ^
set sha to VARIABLES[join(["aspire-dashboard", aspireMajorMinor, "linux", ARCH_SHORT, "sha"], "|")]
set sha to VARIABLES[join(["aspire-dashboard", "linux", ARCH_SHORT, "sha"], "|")]

}}ARG REPO=mcr.microsoft.com/dotnet/aspnet

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
$(McrTagsYmlRepo:aspire-dashboard)
$(McrTagsYmlTagGroup:9.5)
$(McrTagsYmlTagGroup:latest)
5 changes: 5 additions & 0 deletions eng/shared/.editorconfig
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
94 changes: 94 additions & 0 deletions eng/shared/DotNetVersion.cs
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;
Comment thread
lbussell marked this conversation as resolved.
Resolved
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; }
}
18 changes: 18 additions & 0 deletions eng/shared/Microsoft.DotNet.Docker.Shared.csproj
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>
5 changes: 5 additions & 0 deletions eng/shared/README.md
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/).
111 changes: 111 additions & 0 deletions eng/update-dependencies/AspireBuildUpdaterService.cs
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

}
27 changes: 27 additions & 0 deletions eng/update-dependencies/BuildExtensions.cs
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)
Comment thread
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}'"),
};
}
}
15 changes: 15 additions & 0 deletions eng/update-dependencies/BuildRepo.cs
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()"
}
3 changes: 3 additions & 0 deletions eng/update-dependencies/CreatePullRequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public string AzdoOrganization
public string SourceBranch { get; init; } = "";
public string TargetBranch { get; init; } = "nightly";

// If new properties or options are added, they may need to be added to
// SpecificCommandOptions.FromPullRequestOptions(...)

public static List<Option> Options =>
[
new Option<string>("--repo-root") { Description = "The root of the dotnet-docker repo to run against (defaults to current working directory)" },
Expand Down
43 changes: 0 additions & 43 deletions eng/update-dependencies/DotNetVersion.cs

This file was deleted.

Loading