Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 110 additions & 0 deletions eng/update-dependencies/BuildUpdaterService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.DotNet.DarcLib;
using Microsoft.DotNet.ProductConstructionService.Client.Models;
using Microsoft.Extensions.Logging;

namespace Dotnet.Docker;

internal interface IBuildUpdaterService
{
Task<int> UpdateFrom(Build build, CreatePullRequestOptions pullRequestOptions);
}

internal class BuildUpdaterService(
IBuildAssetService buildAssetService,
IBasicBarClient barClient,
ILogger<BuildUpdaterService> logger) : IBuildUpdaterService
{
private readonly IBuildAssetService _buildAssetService = buildAssetService;
private readonly IBasicBarClient _barClient = barClient;
private readonly ILogger<BuildUpdaterService> _logger = logger;

/// <summary>
/// Updates product versions according to a specific BAR build of the VMR. This will update the
/// manifest.versions.json file, generate Dockerfiles and Readmes from the templates, and if
/// credentials are provided, submit a pull request.
/// </summary>
/// <param name="build">
/// A build of the VMR repo (dotnet/dotnet)
/// </param>
/// <param name="pullRequestOptions">
/// Options for creating a pull request. If credentials are provided, a pull request will be created.
/// </param>
/// <returns>Exit code (0 for success)</returns>
public async Task<int> UpdateFrom(Build build, CreatePullRequestOptions pullRequestOptions)
{
_logger.LogInformation("Updating to build {build.Id} with commit {options.Repo}@{build.Commit}",
build.Id, build.AzureDevOpsRepository ?? build.GitHubRepository, build.Commit);

if (!IsVmrBuild(build))
{
throw new InvalidOperationException(
"Expected a build of the VMR, but got a build of " +
$"{build.AzureDevOpsRepository ?? build.GitHubRepository} instead.");
}

IEnumerable<Asset> assets = await _barClient.GetAssetsAsync(buildId: build.Id);

Asset productCommitsAsset = assets.FirstOrDefault(a => ProductCommits.SdkAssetRegex.IsMatch(a.Name))
?? throw new InvalidOperationException($"Could not find product version commit in assets.");

string productCommitsJson = await _buildAssetService.GetAssetTextContentsAsync(productCommitsAsset);
ProductCommits productCommits = ProductCommits.FromJson(productCommitsJson);

Version dockerfileVersion = ResolveMajorMinorVersion(productCommits.Sdk.Version);

// Run old update-dependencies command using the resolved versions
var updateDependencies = new SpecificCommand();
var updateDependenciesOptions = new SpecificCommandOptions()
{
DockerfileVersion = dockerfileVersion.ToString(),
ProductVersions = new Dictionary<string, string?>()
{
// "dotnet" version is also required. It sets the "dotnet|*|product-version"
// variable which is used for runtime-deps, runtime, and aspnet tags.
{ "dotnet", productCommits.Runtime.Version },
{ "runtime", productCommits.Runtime.Version },
{ "aspnet", productCommits.AspNetCore.Version },
{ "aspnet-composite", productCommits.AspNetCore.Version },
{ "sdk", productCommits.Sdk.Version },
},

// Pass through all properties of CreatePullRequestOptions
User = pullRequestOptions.User,
Email = pullRequestOptions.Email,
Password = pullRequestOptions.Password,
AzdoOrganization = pullRequestOptions.AzdoOrganization,
AzdoProject = pullRequestOptions.AzdoProject,
AzdoRepo = pullRequestOptions.AzdoRepo,
VersionSourceName = pullRequestOptions.VersionSourceName,
SourceBranch = pullRequestOptions.SourceBranch,
TargetBranch = pullRequestOptions.TargetBranch,
};

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;
return repo == "https://github.com/dotnet/dotnet"
|| repo == "https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet";
}
}
27 changes: 27 additions & 0 deletions eng/update-dependencies/FromBuildCommand.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 System.Threading.Tasks;
using Microsoft.DotNet.DarcLib;
using Microsoft.DotNet.ProductConstructionService.Client.Models;
using Microsoft.Extensions.Logging;

namespace Dotnet.Docker;

internal class FromBuildCommand(
IBasicBarClient barClient,
IBuildUpdaterService buildUpdaterService,
ILogger<FromBuildCommand> logger)
: BaseCommand<FromBuildOptions>
{
private readonly IBasicBarClient _barClient = barClient;
private readonly IBuildUpdaterService _buildUpdaterService = buildUpdaterService;
private readonly ILogger<FromBuildCommand> _logger = logger;

public override async Task<int> ExecuteAsync(FromBuildOptions options)
{
_logger.LogInformation("Getting BAR build with ID {options.Id}", options.Id);
Build build = await _barClient.GetBuildAsync(options.Id);
return await _buildUpdaterService.UpdateFrom(build, options);
}
}
27 changes: 27 additions & 0 deletions eng/update-dependencies/FromBuildOptions.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 System.Collections.Generic;
using System.CommandLine;

namespace Dotnet.Docker;

internal class FromBuildOptions : CreatePullRequestOptions, IOptions
{
public required int Id { get; init; }

public static new List<Argument> Arguments { get; } =
[
new Argument<int>("id")
{
Arity = ArgumentArity.ExactlyOne,
Description = "The BAR build ID to use as a source for the update (see https://aka.ms/bar)"
},
..CreatePullRequestOptions.Arguments,
];

public static new List<Option> Options { get; } =
[
..CreatePullRequestOptions.Options,
];
}
71 changes: 3 additions & 68 deletions eng/update-dependencies/FromChannelCommand.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.DotNet.DarcLib;
Expand All @@ -12,13 +10,13 @@
namespace Dotnet.Docker;

internal class FromChannelCommand(
IBuildAssetService buildAssetService,
IBasicBarClient barClient,
IBuildUpdaterService buildUpdaterService,
ILogger<FromChannelCommand> logger)
: BaseCommand<FromChannelOptions>
{
private readonly IBuildAssetService _buildAssetService = buildAssetService;
private readonly IBasicBarClient _barClient = barClient;
private readonly IBuildUpdaterService _buildUpdaterService = buildUpdaterService;
private readonly ILogger<FromChannelCommand> _logger = logger;

public override async Task<int> ExecuteAsync(FromChannelOptions options)
Expand All @@ -31,70 +29,7 @@ public override async Task<int> ExecuteAsync(FromChannelOptions options)

_logger.LogInformation("Channel {options.Channel} is '{channel.Name}'",
options.Channel, latestBuild.Channels.FirstOrDefault(c => c.Id == options.Channel)?.Name);
_logger.LogInformation("Got latest build {latestBuild.Id} with commit {options.Repo}@{latestBuild.Commit}",
latestBuild.Id, latestBuild.AzureDevOpsRepository ?? latestBuild.GitHubRepository, latestBuild.Commit);

if (!IsVmrBuild(latestBuild))
{
throw new InvalidOperationException(
"Expected a build of the VMR, but got a build of " +
$"{latestBuild.AzureDevOpsRepository ?? latestBuild.GitHubRepository} instead.");
}

IEnumerable<Asset> assets = await _barClient.GetAssetsAsync(buildId: latestBuild.Id);

Asset productCommitsAsset = assets.FirstOrDefault(a => ProductCommits.SdkAssetRegex.IsMatch(a.Name))
?? throw new InvalidOperationException($"Could not find product version commit in assets.");

string productCommitsJson = await _buildAssetService.GetAssetTextContentsAsync(productCommitsAsset);
ProductCommits productCommits = ProductCommits.FromJson(productCommitsJson);

Version dockerfileVersion = ResolveMajorMinorVersion(productCommits.Sdk.Version);

// Run old update-dependencies command using the resolved versions
var updateDependencies = new SpecificCommand();
var updateDependenciesOptions = new SpecificCommandOptions()
{
DockerfileVersion = dockerfileVersion.ToString(),
ProductVersions = new Dictionary<string, string?>()
{
// In the VMR, runtime and aspnetcore versions are coupled
{ "runtime", productCommits.Runtime.Version },
{ "aspnet", productCommits.AspNetCore.Version },
{ "aspnet-composite", productCommits.AspNetCore.Version },
{ "sdk", productCommits.Sdk.Version },
},

// 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);
}

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;
return repo == "https://github.com/dotnet/dotnet"
|| repo == "https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet";
return await _buildUpdaterService.UpdateFrom(latestBuild, options);
}
}
5 changes: 5 additions & 0 deletions eng/update-dependencies/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

var rootCommand = new RootCommand()
{
FromBuildCommand.Create(
name: "from-build",
description: "Update dependencies using a specific BAR build"),
FromChannelCommand.Create(
name: "from-channel",
description: "Update dependencies using the latest build from a channel"),
Expand All @@ -40,11 +43,13 @@
},
configureHost: host => host.ConfigureServices(services =>
{
services.AddSingleton<IBuildUpdaterService, BuildUpdaterService>();
services.AddSingleton<IBasicBarClient>(_ =>
new BarApiClient(null, null, disableInteractiveAuth: true));
services.AddSingleton<IBuildAssetService, BuildAssetService>();
services.AddSingleton<HttpClient>();

FromBuildCommand.Register<FromBuildCommand>(services);
FromChannelCommand.Register<FromChannelCommand>(services);
SpecificCommand.Register<SpecificCommand>(services);
})
Expand Down