Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
79d03e9
Move internal versions method to helper class
lbussell Sep 30, 2025
771c236
Factor out parse/serialization logic for internal staging builds
lbussell Sep 30, 2025
9587f72
Add helper method for constructing mock command
lbussell Oct 1, 2025
7c1ec48
Extract ICommand interface from BaseCommand
lbussell Oct 1, 2025
163b241
Use InternalsVisibleTo for update dependencies tests
lbussell Oct 1, 2025
4487387
Add FromStagingPipelineCommand dependency to SyncInternalReleaseCommand
lbussell Oct 1, 2025
bd3497d
Add git restore method
lbussell Oct 1, 2025
cf0d112
Add --repo-root argmuent to existing commands
lbussell Oct 1, 2025
2eb7c5e
Make InternalVersionsHelper into a service
lbussell Oct 1, 2025
398e89f
Split InternalVersions* into one file per type
lbussell Oct 1, 2025
cb32300
Implement first pass at re-applying internal builds
lbussell Oct 2, 2025
996a5f7
Clean up tests and add more comments
lbussell Oct 2, 2025
ff5af1a
Add extra logging to GitRepoHelperFactory
lbussell Oct 2, 2025
4bd8a49
Checkout ref instead of checkout branch for remote branches
lbussell Oct 2, 2025
5545b06
Add extra git logging
lbussell Oct 2, 2025
c4b5cd7
Prevent duplicate logging when calling SpecificCommand more than once
lbussell Oct 2, 2025
9c4414c
Allow multi-line log output
lbussell Oct 2, 2025
9d4f4b0
Use existing CreatePullRequestOptions for SyncInternalReleaseCommand
lbussell Oct 2, 2025
0966724
Make FromStagingPipelineCommand respect repo root
lbussell Oct 2, 2025
a4c388a
Reset using source branch sha instead of branch name
lbussell Oct 2, 2025
79f1a41
Fix argument name for GetToolUpdaters
lbussell Oct 2, 2025
ca0901e
Add helper for shuttling around Manifest version variables and use "b…
lbussell Oct 2, 2025
7167d00
Throw if sub-command had a non-zero exit code
lbussell Oct 2, 2025
c6c5bf9
Fix tests
lbussell Oct 2, 2025
2edd452
Add branch variables to manifest.versions.json
lbussell Oct 2, 2025
3c15992
Reference new Azdo URL extension in SpecificCommand
lbussell Oct 3, 2025
397806b
Always trim org name
lbussell Oct 3, 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
5 changes: 2 additions & 3 deletions eng/update-dependencies/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@

using System.CommandLine;
using System.CommandLine.NamingConventionBinder;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Dotnet.Docker;

public abstract class BaseCommand<TOptions>() where TOptions : IOptions
public abstract class BaseCommand<TOptions>() : ICommand<TOptions> where TOptions : IOptions
{
public abstract Task<int> ExecuteAsync(TOptions options);

Expand All @@ -34,7 +33,7 @@ public static Command Create(string name, string description)
private static BindingHandler Handler =>
CommandHandler.Create<TOptions, IHost>(async (options, host) =>
{
var thisCommand = host.Services.GetRequiredService<BaseCommand<TOptions>>();
var thisCommand = host.Services.GetRequiredService<ICommand<TOptions>>();
return await thisCommand.ExecuteAsync(options);
});
}
9 changes: 4 additions & 5 deletions eng/update-dependencies/BaseUrlUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ internal class BaseUrlUpdater : FileRegexUpdater
/// If the base URL variable cannot be found in the manifest, the updater
/// won't do anything.
/// </summary>
public static IDependencyUpdater Create(string repoRoot, SpecificCommandOptions options)
public static IDependencyUpdater Create(SpecificCommandOptions options)
{
// Load manifest and extract variables once so the constructor doesn't duplicate this logic
var manifest = ManifestHelper.LoadManifest(SpecificCommand.VersionsFilename);
var manifest = ManifestHelper.LoadManifest(options.GetManifestVersionsFilePath());
var manifestVariables = (JObject?)manifest["variables"];

if (manifestVariables is null)
Expand All @@ -49,16 +49,15 @@ public static IDependencyUpdater Create(string repoRoot, SpecificCommandOptions
return new EmptyDependencyUpdater();
}

return new BaseUrlUpdater(repoRoot, options, manifestVariables, baseUrlVarName);
return new BaseUrlUpdater(options, manifestVariables, baseUrlVarName);
}

private BaseUrlUpdater(
string repoRoot,
SpecificCommandOptions options,
JObject manifestVariables,
string manifestVariableName)
{
Path = System.IO.Path.Combine(repoRoot, SpecificCommand.VersionsFilename);
Path = options.GetManifestVersionsFilePath();
VersionGroupName = BaseUrlGroupName;
_options = options;
_manifestVariables = manifestVariables;
Expand Down
6 changes: 6 additions & 0 deletions eng/update-dependencies/CreatePullRequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public abstract record CreatePullRequestOptions
{
private string? _targetBranch = null;

/// <summary>
/// The root of the dotnet-docker repo to run against.
/// </summary>
public string RepoRoot { get; init; } = Directory.GetCurrentDirectory();

public string User { get; init; } = "";
public string Email { get; init; } = "";
public string Password { get; init; } = "";
Expand All @@ -26,6 +31,7 @@ public string TargetBranch

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)" },
new Option<string>("--user") { Description = "GitHub or AzDO user used to make PR (if not specified, a PR will not be created)" },
new Option<string>("--email") { Description = "GitHub or AzDO email used to make PR (if not specified, a PR will not be created)" },
new Option<string>("--password") { Description = "GitHub or AzDO password used to make PR (if not specified, a PR will not be created)" },
Expand Down
10 changes: 10 additions & 0 deletions eng/update-dependencies/CreatePullRequestOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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;

internal static class CreatePullRequestOptionsExtensions
{
public static string GetManifestVersionsFilePath(this CreatePullRequestOptions options) =>
Path.Combine(options.RepoRoot, "manifest.versions.json");
}
2 changes: 1 addition & 1 deletion eng/update-dependencies/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static void AddCommand<TCommand, TOptions>(
where TCommand : BaseCommand<TOptions>
where TOptions : IOptions
{
serviceCollection.AddSingleton<BaseCommand<TOptions>, TCommand>();
serviceCollection.AddSingleton<ICommand<TOptions>, TCommand>();
}

/// <summary>
Expand Down
9 changes: 5 additions & 4 deletions eng/update-dependencies/DockerfileShaUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ public DockerfileShaUpdater(
_manifestVariables = new Lazy<JObject>(
() =>
{
var versionsFile = options.GetManifestVersionsFilePath();
const string VariablesProperty = "variables";
JToken? variables = ManifestHelper.LoadManifest(SpecificCommand.VersionsFilename)[VariablesProperty];
JToken? variables = ManifestHelper.LoadManifest(versionsFile)[VariablesProperty];
if (variables is null)
{
throw new InvalidOperationException($"'{VariablesProperty}' property missing in '{SpecificCommand.VersionsFilename}'");
throw new InvalidOperationException($"'{VariablesProperty}' property missing in '{versionsFile}'");
}
return (JObject)variables;
});
Expand All @@ -91,9 +92,9 @@ public DockerfileShaUpdater(
}

public static IEnumerable<IDependencyUpdater> CreateUpdaters(
string productName, string dockerfileVersion, string repoRoot, SpecificCommandOptions options)
string productName, string dockerfileVersion, SpecificCommandOptions options)
{
string versionsPath = System.IO.Path.Combine(repoRoot, SpecificCommand.VersionsFilename);
string versionsPath = options.GetManifestVersionsFilePath();
string versions = File.ReadAllText(versionsPath);

// The format of the sha variable name is '<productName>|<dockerfileVersion>|<os>|<arch>|sha'.
Expand Down
63 changes: 8 additions & 55 deletions eng/update-dependencies/FromStagingPipelineCommand.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Dotnet.Docker.Sync;
using Microsoft.Extensions.Logging;

namespace Dotnet.Docker;

internal partial class FromStagingPipelineCommand(
ILogger<FromStagingPipelineCommand> logger,
PipelineArtifactProvider pipelineArtifactProvider)
PipelineArtifactProvider pipelineArtifactProvider,
IInternalVersionsService internalVersionsService)
: BaseCommand<FromStagingPipelineOptions>
{
private readonly ILogger<FromStagingPipelineCommand> _logger = logger;
private readonly PipelineArtifactProvider _pipelineArtifactProvider = pipelineArtifactProvider;
private readonly IInternalVersionsService _internalVersionsService = internalVersionsService;

public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
{
Expand Down Expand Up @@ -44,7 +47,10 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
string dockerfileVersion = VersionHelper.ResolveMajorMinorVersion(releaseConfig.RuntimeBuild).ToString();

// Record pipeline run ID for this dockerfileVersion, for later use by sync-internal-release command
RecordInternalVersion(dockerfileVersion, options.StagingPipelineRunId.ToString());
_internalVersionsService.RecordInternalStagingBuild(
options.RepoRoot,
dockerfileVersion,
options.StagingPipelineRunId);

var productVersions = (options.Internal, releaseConfig.SdkOnly) switch
{
Expand Down Expand Up @@ -109,59 +115,6 @@ public override async Task<int> ExecuteAsync(FromStagingPipelineOptions options)
return await updateDependencies.ExecuteAsync(updateDependenciesOptions);
}

/// <summary>
/// Records the staging pipeline run ID in an easy to parse format. This
/// can be used by the sync-internal-release pipeline to record and
/// re-apply the same staging builds after resetting the state of the repo
/// to match the public release branch.
/// </summary>
/// <remarks>
/// This will only store one staging pipeline run ID per dockerfileVersion
/// </remarks>
/// <param name="dockerfileVersion">major-minor version</param>
/// <param name="stagingPipelineRunId">the build ID of the staging pipeline run</param>
private void RecordInternalVersion(string dockerfileVersion, string stagingPipelineRunId)
{
const string InternalVersionsFile = "internal-versions.txt";

// Internal versions file should have one line per dockerfileVersion
// Each line should be formatted as: <dockerfileVersion>=<stagingPipelineRunId>
//
// The preferable way to do this would be to record the version in
// manifest.versions.json, however that would require one of the following:
// 1) round-trip serialization, which would remove any whitespace/blank lines - which are
// important for keeping the file readable and reducing git merge conflicts
// 2) lots of regex JSON manipulation which is error-prone and harder to maintain
//
// So for now, the separate file and format is a compromise.

var versionsFilePath = Path.GetFullPath(SpecificCommand.VersionsFilename);
var versionsFileDir = Path.GetDirectoryName(versionsFilePath) ?? "";
var internalVersionFile = Path.Combine(versionsFileDir, InternalVersionsFile);
Dictionary<string, string> versions = [];

_logger.LogInformation(
"Recording staging pipeline build ID in {internalVersionFile}",
internalVersionFile);

try
{
// File already exists - read existing versions
versions = File.ReadAllLines(internalVersionFile)
.Select(line => line.Split('=', 2))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1]);
}
catch (FileNotFoundException)
{
// File doesn't exist - it will be created
}

versions[dockerfileVersion] = stagingPipelineRunId;
var versionLines = versions.Select(kv => $"{kv.Key}={kv.Value}");
File.WriteAllLines(internalVersionFile, versionLines);
}

/// <summary>
/// Formats a storage account URL has a specific format:
/// - Starts with "https://"
Expand Down
16 changes: 9 additions & 7 deletions eng/update-dependencies/FromStagingPipelineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Dotnet.Docker;

internal record FromStagingPipelineOptions : CreatePullRequestOptions, IOptions
{
public const string StagingStorageAccountOption = "--staging-storage-account";
public const string StagingStorageAccountOptionName = "--staging-storage-account";
public const string InternalOption = "--internal";

/// <summary>
Expand Down Expand Up @@ -37,14 +37,16 @@ internal record FromStagingPipelineOptions : CreatePullRequestOptions, IOptions
..CreatePullRequestOptions.Arguments,
];

public static Option<string> StagingStorageAccountOption = new(StagingStorageAccountOptionName)
{
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/"
};

public static new List<Option> Options { get; } =
[
new Option<string>(StagingStorageAccountOption)
{
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/"
},
StagingStorageAccountOption,
new Option<bool>(InternalOption)
{
Description = "Whether or not to use the internal versions of the staged build. When not using an internal"
Expand Down
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/GitRemoteInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ namespace Dotnet.Docker.Git;
/// </summary>
/// <param name="Name">The name of the remote (e.g., "origin").</param>
/// <param name="Url">The URL of the remote repository.</param>
public record GitRemoteInfo(string Name, string Url);
internal sealed record GitRemoteInfo(string Name, string Url);
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/GitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Dotnet.Docker.Git;
/// <remarks>
/// Use <see cref="IGitRepoHelperFactory"/> to instantiate this.
/// </remarks>
public sealed class GitRepoHelper(
internal sealed class GitRepoHelper(
string remoteRepoUrl,
ILocalGitRepoHelper localGitRepoHelper,
IRemoteGitRepoHelper remoteGitRepoHelper,
Expand Down
4 changes: 2 additions & 2 deletions eng/update-dependencies/Git/GitRepoHelperFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

namespace Dotnet.Docker.Git;

public interface IGitRepoHelperFactory
internal interface IGitRepoHelperFactory
{
Task<IGitRepoHelper> CreateAsync(string repoUri);
}

public sealed class GitRepoHelperFactory(
internal sealed class GitRepoHelperFactory(
IGitRepoCloner gitRepoCloner,
ILocalGitRepoFactory localGitRepoFactory,
IRemoteGitRepoFactory remoteFactory,
Expand Down
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/IGitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Dotnet.Docker.Git;
/// <summary>
/// Handles operations that require interactions with both local and remote git repos.
/// </summary>
public interface IGitRepoHelper : IDisposable
internal interface IGitRepoHelper : IDisposable
{
/// <summary>
/// For any git operations that only affect the local repo.
Expand Down
18 changes: 17 additions & 1 deletion eng/update-dependencies/Git/ILocalGitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Dotnet.Docker.Git;

public interface ILocalGitRepoHelper
internal interface ILocalGitRepoHelper
{
/// <summary>
/// The local path where the repository is cloned.
Expand Down Expand Up @@ -69,4 +69,20 @@ public interface ILocalGitRepoHelper
/// List all remotes for the local repository.
/// </summary>
Task<IEnumerable<GitRemoteInfo>> ListAllRemotesAsync();

/// <summary>
/// Restore the working tree with the contents from a restore source.
/// Uncommitted changes in the working tree will be lost.
/// </summary>
/// <remarks>
/// Because contents are restored to the working tree, they will not be
/// staged. If the changes resulting from this command need to be
/// committed, they must be staged first.
/// </remarks>
/// <param name="source">
/// Restore the working tree files with the content from this tree.
/// This can be a commit, branch or tag.
/// </param>
/// <see href="link">https://git-scm.com/docs/git-restore</see>
Task RestoreAsync(string source);
}
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/IRemoteGitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Dotnet.Docker.Git;

public interface IRemoteGitRepoHelper
internal interface IRemoteGitRepoHelper
{
/// <summary>
/// Checks if a branch exists on the remote repository.
Expand Down
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/InvalidBranchException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ namespace Dotnet.Docker.Git;
/// such as attempting to use a non-existent branch or an incorrectly named branch.
/// </summary>
/// <param name="message">The error message describing the invalid branch condition.</param>
public sealed class InvalidBranchException(string message) : InvalidOperationException(message);
internal sealed class InvalidBranchException(string message) : InvalidOperationException(message);
10 changes: 8 additions & 2 deletions eng/update-dependencies/Git/LocalGitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Dotnet.Docker.Git;

public sealed class LocalGitRepoHelper(
internal sealed class LocalGitRepoHelper(
string localPath,
ILocalGitRepo localGitRepo,
ILogger<LocalGitRepoHelper> logger
Expand Down Expand Up @@ -67,7 +67,7 @@ public async Task<string> CommitAsync(string message, (string Name, string Email
var commitSha = await _localGitRepo.GetShaForRefAsync("HEAD");
return commitSha;
}

/// <inheritdoc />
public async Task<bool> IsAncestorAsync(string ancestorRef, string descendantRef)
{
Expand Down Expand Up @@ -99,4 +99,10 @@ public async Task<IEnumerable<GitRemoteInfo>> ListAllRemotesAsync()
// There are typically two entries per remote (fetch and push); we only want one
.Distinct();
}

/// <inheritdoc />
public async Task RestoreAsync(string source)
{
await _localGitRepo.ExecuteGitCommand("restore", "--source", source);
}
}
4 changes: 2 additions & 2 deletions eng/update-dependencies/Git/RemoteGitRepoFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Dotnet.Docker.Git;
/// <summary>
/// Factory for creating <see cref="IRemoteGitRepo"/> instances based on repository URLs.
/// </summary>
public interface IRemoteGitRepoFactory
internal interface IRemoteGitRepoFactory
{
/// <summary>
/// Creates a remote Git repository client appropriate for the given repository URL.
Expand All @@ -21,7 +21,7 @@ public interface IRemoteGitRepoFactory
}

/// <inheritdoc />
public sealed class RemoteGitRepoFactory(IServiceProvider serviceProvider) : IRemoteGitRepoFactory
internal sealed class RemoteGitRepoFactory(IServiceProvider serviceProvider) : IRemoteGitRepoFactory
{
private readonly IServiceProvider _serviceProvider = serviceProvider;

Expand Down
2 changes: 1 addition & 1 deletion eng/update-dependencies/Git/RemoteGitRepoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Dotnet.Docker.Git;

public sealed class RemoteGitRepoHelper(
internal sealed class RemoteGitRepoHelper(
string remoteRepoUrl,
IRemoteGitRepo remoteGitRepo,
ILocalGitRepo localGitRepo,
Expand Down
Loading