From 79d03e9a3158256f08bb697a938abaf39b3441f7 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 30 Sep 2025 14:12:37 -0700 Subject: [PATCH 01/27] Move internal versions method to helper class --- .../FromStagingPipelineCommand.cs | 56 +------------------ .../Sync/InternalVersionsHelper.cs | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 eng/update-dependencies/Sync/InternalVersionsHelper.cs diff --git a/eng/update-dependencies/FromStagingPipelineCommand.cs b/eng/update-dependencies/FromStagingPipelineCommand.cs index 4fdbce61f2..cbabbbad2a 100644 --- a/eng/update-dependencies/FromStagingPipelineCommand.cs +++ b/eng/update-dependencies/FromStagingPipelineCommand.cs @@ -1,6 +1,7 @@ // 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; @@ -44,7 +45,7 @@ public override async Task 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()); + InternalVersionsHelper.RecordInternalVersion(dockerfileVersion, options.StagingPipelineRunId.ToString()); var productVersions = (options.Internal, releaseConfig.SdkOnly) switch { @@ -109,59 +110,6 @@ public override async Task ExecuteAsync(FromStagingPipelineOptions options) return await updateDependencies.ExecuteAsync(updateDependenciesOptions); } - /// - /// 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. - /// - /// - /// This will only store one staging pipeline run ID per dockerfileVersion - /// - /// major-minor version - /// the build ID of the staging pipeline run - 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: = - // - // 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 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); - } - /// /// Formats a storage account URL has a specific format: /// - Starts with "https://" diff --git a/eng/update-dependencies/Sync/InternalVersionsHelper.cs b/eng/update-dependencies/Sync/InternalVersionsHelper.cs new file mode 100644 index 0000000000..863d48092a --- /dev/null +++ b/eng/update-dependencies/Sync/InternalVersionsHelper.cs @@ -0,0 +1,56 @@ +// 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.Sync; + +internal static class InternalVersionsHelper +{ + /// + /// 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. + /// + /// + /// This will only store one staging pipeline run ID per dockerfileVersion + /// + /// major-minor version + /// the build ID of the staging pipeline run + public static 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: = + // + // 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 versions = []; + + 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); + } +} From 771c236c5e0a9dac9cda283f2d377a269a838494 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 30 Sep 2025 14:50:22 -0700 Subject: [PATCH 02/27] Factor out parse/serialization logic for internal staging builds --- .../FromStagingPipelineCommand.cs | 2 +- .../Sync/InternalVersionsHelper.cs | 54 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/eng/update-dependencies/FromStagingPipelineCommand.cs b/eng/update-dependencies/FromStagingPipelineCommand.cs index cbabbbad2a..8ea2b1c6da 100644 --- a/eng/update-dependencies/FromStagingPipelineCommand.cs +++ b/eng/update-dependencies/FromStagingPipelineCommand.cs @@ -45,7 +45,7 @@ public override async Task 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 - InternalVersionsHelper.RecordInternalVersion(dockerfileVersion, options.StagingPipelineRunId.ToString()); + InternalVersionsHelper.RecordInternalVersion(dockerfileVersion, options.StagingPipelineRunId); var productVersions = (options.Internal, releaseConfig.SdkOnly) switch { diff --git a/eng/update-dependencies/Sync/InternalVersionsHelper.cs b/eng/update-dependencies/Sync/InternalVersionsHelper.cs index 863d48092a..0ed8ca268f 100644 --- a/eng/update-dependencies/Sync/InternalVersionsHelper.cs +++ b/eng/update-dependencies/Sync/InternalVersionsHelper.cs @@ -1,8 +1,46 @@ // 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.Immutable; + namespace Dotnet.Docker.Sync; +/// +/// Records information about what internal staging pipeline run IDs were used +/// for which .NET Dockerfile versions. +/// +/// +/// Mapping of Major.Minor .NET version to staging pipeline run ID. +/// +internal sealed record InternalStagingBuilds(ImmutableDictionary Versions) +{ + /// + /// Parses from lines of text. + /// + /// + /// Each line should be formatted as: = + /// + public static InternalStagingBuilds Parse(IEnumerable lines) + { + var versions = lines + .Select(line => line.Split('=', 2)) + .Where(parts => parts.Length == 2) + .ToImmutableDictionary(parts => parts[0], parts => int.Parse(parts[1])); + + return new InternalStagingBuilds(versions); + } + + /// + /// Returns a new with the specified + /// version added. + /// + public InternalStagingBuilds Add(string dockerfileVersion, int stagingPipelineRunId) => + this with { Versions = Versions.SetItem(dockerfileVersion, stagingPipelineRunId) }; + + public override string ToString() => + string.Join(Environment.NewLine, Versions.Select(kv => $"{kv.Key}={kv.Value}")); +} + internal static class InternalVersionsHelper { /// @@ -16,7 +54,7 @@ internal static class InternalVersionsHelper /// /// major-minor version /// the build ID of the staging pipeline run - public static void RecordInternalVersion(string dockerfileVersion, string stagingPipelineRunId) + public static void RecordInternalVersion(string dockerfileVersion, int stagingPipelineRunId) { const string InternalVersionsFile = "internal-versions.txt"; @@ -34,23 +72,21 @@ public static void RecordInternalVersion(string dockerfileVersion, string stagin var versionsFilePath = Path.GetFullPath(SpecificCommand.VersionsFilename); var versionsFileDir = Path.GetDirectoryName(versionsFilePath) ?? ""; var internalVersionFile = Path.Combine(versionsFileDir, InternalVersionsFile); - Dictionary versions = []; + InternalStagingBuilds builds; 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]); + var fileContents = File.ReadAllLines(internalVersionFile); + builds = InternalStagingBuilds.Parse(fileContents); } catch (FileNotFoundException) { // File doesn't exist - it will be created + builds = new InternalStagingBuilds(ImmutableDictionary.Empty); } - versions[dockerfileVersion] = stagingPipelineRunId; - var versionLines = versions.Select(kv => $"{kv.Key}={kv.Value}"); - File.WriteAllLines(internalVersionFile, versionLines); + builds = builds.Add(dockerfileVersion, stagingPipelineRunId); + File.WriteAllText(internalVersionFile, builds.ToString()); } } From 9587f72f1674ac9b88bf0d2417c77c3545d3a910 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 08:44:36 -0700 Subject: [PATCH 03/27] Add helper method for constructing mock command --- .../SyncInternalReleaseTests.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs index 9dcd3584f9..309cea0ace 100644 --- a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs +++ b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs @@ -29,6 +29,21 @@ public sealed class SyncInternalReleaseTests TargetBranch = InternalReleaseBranch }; + /// + /// Helper method to create a instance with optional + /// mocked dependencies. If dependencies are not provided, default mocks will be used. + /// + private SyncInternalReleaseCommand CreateCommand( + IGitRepoHelperFactory? repoFactory = null, + ILogger? logger = null) + { + // New parameters should be null by default and initialized with mocks if not specified. + return new( + repoFactory ?? Mock.Of(), + logger ?? Mock.Of>() + ); + } + /// /// Calling the command with null or whitespace for any of the arguments should fail. /// @@ -42,9 +57,7 @@ public async Task WhitespaceArgumentsFails() TargetBranch = " " }; - var command = new SyncInternalReleaseCommand( - Mock.Of(), - Mock.Of>()); + var command = CreateCommand(); await Should.ThrowAsync(() => command.ExecuteAsync(options)); } @@ -59,9 +72,7 @@ public async Task InternalSourceBranchFails() { var options = s_defaultOptions with { SourceBranch = "internal/foo" }; - var command = new SyncInternalReleaseCommand( - Mock.Of(), - Mock.Of>()); + var command = CreateCommand(); await Should.ThrowAsync(() => command.ExecuteAsync(options)); } @@ -85,9 +96,7 @@ public async Task CreateInternalBranch() // Source branch exists on remote repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.SourceBranch)).ReturnsAsync(true); - var command = new SyncInternalReleaseCommand( - repoFactoryMock.Object, - Mock.Of>()); + var command = CreateCommand(repoFactory: repoFactoryMock.Object); var exitCode = await command.ExecuteAsync(options); exitCode.ShouldBe(0); @@ -120,9 +129,7 @@ public async Task AlreadyUpToDate() repoMock.Setup(r => r.Remote.GetRemoteBranchShaAsync(options.SourceBranch)).ReturnsAsync(Sha); repoMock.Setup(r => r.Dispose()); - var command = new SyncInternalReleaseCommand( - repoFactoryMock.Object, - Mock.Of>()); + var command = CreateCommand(repoFactory: repoFactoryMock.Object); // Command should succeed. var exitCode = await command.ExecuteAsync(options); @@ -170,9 +177,7 @@ public async Task FastForward() .Setup(r => r.GetPullRequestInfoAsync(It.IsAny())) .ReturnsAsync(Mock.Of()); - var command = new SyncInternalReleaseCommand( - repoFactoryMock.Object, - Mock.Of>()); + var command = CreateCommand(repoFactory: repoFactoryMock.Object); // Command should succeed. var exitCode = await command.ExecuteAsync(options); From 7c1ec48e83f6139cb2bdd78391bf4e50ecb5b7a4 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 09:57:18 -0700 Subject: [PATCH 04/27] Extract ICommand interface from BaseCommand --- eng/update-dependencies/BaseCommand.cs | 5 ++--- .../DependencyInjectionExtensions.cs | 2 +- eng/update-dependencies/ICommand.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 eng/update-dependencies/ICommand.cs diff --git a/eng/update-dependencies/BaseCommand.cs b/eng/update-dependencies/BaseCommand.cs index d5d459528d..402b3e7fc3 100644 --- a/eng/update-dependencies/BaseCommand.cs +++ b/eng/update-dependencies/BaseCommand.cs @@ -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() where TOptions : IOptions +public abstract class BaseCommand() : ICommand where TOptions : IOptions { public abstract Task ExecuteAsync(TOptions options); @@ -34,7 +33,7 @@ public static Command Create(string name, string description) private static BindingHandler Handler => CommandHandler.Create(async (options, host) => { - var thisCommand = host.Services.GetRequiredService>(); + var thisCommand = host.Services.GetRequiredService>(); return await thisCommand.ExecuteAsync(options); }); } diff --git a/eng/update-dependencies/DependencyInjectionExtensions.cs b/eng/update-dependencies/DependencyInjectionExtensions.cs index 18b2e76475..ccc3ba6f52 100644 --- a/eng/update-dependencies/DependencyInjectionExtensions.cs +++ b/eng/update-dependencies/DependencyInjectionExtensions.cs @@ -17,7 +17,7 @@ public static void AddCommand( where TCommand : BaseCommand where TOptions : IOptions { - serviceCollection.AddSingleton, TCommand>(); + serviceCollection.AddSingleton, TCommand>(); } /// diff --git a/eng/update-dependencies/ICommand.cs b/eng/update-dependencies/ICommand.cs new file mode 100644 index 0000000000..8918bcfb79 --- /dev/null +++ b/eng/update-dependencies/ICommand.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// A generic CLI command that takes an options object. +/// +/// +/// The type of options that the command accepts. +/// +/// +public interface ICommand where TOptions : IOptions +{ + Task ExecuteAsync(TOptions options); +} From 163b2415ef40cd4365731b26187e32082bfe74e4 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 09:59:07 -0700 Subject: [PATCH 05/27] Use InternalsVisibleTo for update dependencies tests --- eng/update-dependencies/Git/GitRemoteInfo.cs | 2 +- eng/update-dependencies/Git/GitRepoHelper.cs | 2 +- eng/update-dependencies/Git/GitRepoHelperFactory.cs | 4 ++-- eng/update-dependencies/Git/IGitRepoHelper.cs | 2 +- eng/update-dependencies/Git/ILocalGitRepoHelper.cs | 2 +- eng/update-dependencies/Git/IRemoteGitRepoHelper.cs | 2 +- eng/update-dependencies/Git/InvalidBranchException.cs | 2 +- eng/update-dependencies/Git/LocalGitRepoHelper.cs | 4 ++-- eng/update-dependencies/Git/RemoteGitRepoFactory.cs | 4 ++-- eng/update-dependencies/Git/RemoteGitRepoHelper.cs | 2 +- eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs | 2 +- eng/update-dependencies/update-dependencies.csproj | 6 ++++++ 12 files changed, 20 insertions(+), 14 deletions(-) diff --git a/eng/update-dependencies/Git/GitRemoteInfo.cs b/eng/update-dependencies/Git/GitRemoteInfo.cs index ba85d48cb6..2530ef6329 100644 --- a/eng/update-dependencies/Git/GitRemoteInfo.cs +++ b/eng/update-dependencies/Git/GitRemoteInfo.cs @@ -8,4 +8,4 @@ namespace Dotnet.Docker.Git; /// /// The name of the remote (e.g., "origin"). /// The URL of the remote repository. -public record GitRemoteInfo(string Name, string Url); +internal sealed record GitRemoteInfo(string Name, string Url); diff --git a/eng/update-dependencies/Git/GitRepoHelper.cs b/eng/update-dependencies/Git/GitRepoHelper.cs index 0357f1d1f0..1558bb1488 100644 --- a/eng/update-dependencies/Git/GitRepoHelper.cs +++ b/eng/update-dependencies/Git/GitRepoHelper.cs @@ -13,7 +13,7 @@ namespace Dotnet.Docker.Git; /// /// Use to instantiate this. /// -public sealed class GitRepoHelper( +internal sealed class GitRepoHelper( string remoteRepoUrl, ILocalGitRepoHelper localGitRepoHelper, IRemoteGitRepoHelper remoteGitRepoHelper, diff --git a/eng/update-dependencies/Git/GitRepoHelperFactory.cs b/eng/update-dependencies/Git/GitRepoHelperFactory.cs index 1e2f9e5769..8db2d180bc 100644 --- a/eng/update-dependencies/Git/GitRepoHelperFactory.cs +++ b/eng/update-dependencies/Git/GitRepoHelperFactory.cs @@ -11,12 +11,12 @@ namespace Dotnet.Docker.Git; -public interface IGitRepoHelperFactory +internal interface IGitRepoHelperFactory { Task CreateAsync(string repoUri); } -public sealed class GitRepoHelperFactory( +internal sealed class GitRepoHelperFactory( IGitRepoCloner gitRepoCloner, ILocalGitRepoFactory localGitRepoFactory, IRemoteGitRepoFactory remoteFactory, diff --git a/eng/update-dependencies/Git/IGitRepoHelper.cs b/eng/update-dependencies/Git/IGitRepoHelper.cs index f413dc3c2a..9aed8f1467 100644 --- a/eng/update-dependencies/Git/IGitRepoHelper.cs +++ b/eng/update-dependencies/Git/IGitRepoHelper.cs @@ -9,7 +9,7 @@ namespace Dotnet.Docker.Git; /// /// Handles operations that require interactions with both local and remote git repos. /// -public interface IGitRepoHelper : IDisposable +internal interface IGitRepoHelper : IDisposable { /// /// For any git operations that only affect the local repo. diff --git a/eng/update-dependencies/Git/ILocalGitRepoHelper.cs b/eng/update-dependencies/Git/ILocalGitRepoHelper.cs index 0457ce2386..472526b5d1 100644 --- a/eng/update-dependencies/Git/ILocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/ILocalGitRepoHelper.cs @@ -6,7 +6,7 @@ namespace Dotnet.Docker.Git; -public interface ILocalGitRepoHelper +internal interface ILocalGitRepoHelper { /// /// The local path where the repository is cloned. diff --git a/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs b/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs index 1390e2d4d0..b946c28268 100644 --- a/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs +++ b/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs @@ -6,7 +6,7 @@ namespace Dotnet.Docker.Git; -public interface IRemoteGitRepoHelper +internal interface IRemoteGitRepoHelper { /// /// Checks if a branch exists on the remote repository. diff --git a/eng/update-dependencies/Git/InvalidBranchException.cs b/eng/update-dependencies/Git/InvalidBranchException.cs index 26703c3ac0..2600e7e003 100644 --- a/eng/update-dependencies/Git/InvalidBranchException.cs +++ b/eng/update-dependencies/Git/InvalidBranchException.cs @@ -10,4 +10,4 @@ namespace Dotnet.Docker.Git; /// such as attempting to use a non-existent branch or an incorrectly named branch. /// /// The error message describing the invalid branch condition. -public sealed class InvalidBranchException(string message) : InvalidOperationException(message); +internal sealed class InvalidBranchException(string message) : InvalidOperationException(message); diff --git a/eng/update-dependencies/Git/LocalGitRepoHelper.cs b/eng/update-dependencies/Git/LocalGitRepoHelper.cs index 51f57d7f5e..1efe554651 100644 --- a/eng/update-dependencies/Git/LocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/LocalGitRepoHelper.cs @@ -10,7 +10,7 @@ namespace Dotnet.Docker.Git; -public sealed class LocalGitRepoHelper( +internal sealed class LocalGitRepoHelper( string localPath, ILocalGitRepo localGitRepo, ILogger logger @@ -67,7 +67,7 @@ public async Task CommitAsync(string message, (string Name, string Email var commitSha = await _localGitRepo.GetShaForRefAsync("HEAD"); return commitSha; } - + /// public async Task IsAncestorAsync(string ancestorRef, string descendantRef) { diff --git a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs index a2a7815d6c..59d72bd4b9 100644 --- a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs +++ b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs @@ -10,7 +10,7 @@ namespace Dotnet.Docker.Git; /// /// Factory for creating instances based on repository URLs. /// -public interface IRemoteGitRepoFactory +internal interface IRemoteGitRepoFactory { /// /// Creates a remote Git repository client appropriate for the given repository URL. @@ -21,7 +21,7 @@ public interface IRemoteGitRepoFactory } /// -public sealed class RemoteGitRepoFactory(IServiceProvider serviceProvider) : IRemoteGitRepoFactory +internal sealed class RemoteGitRepoFactory(IServiceProvider serviceProvider) : IRemoteGitRepoFactory { private readonly IServiceProvider _serviceProvider = serviceProvider; diff --git a/eng/update-dependencies/Git/RemoteGitRepoHelper.cs b/eng/update-dependencies/Git/RemoteGitRepoHelper.cs index c44cdf89d3..20fd42dd84 100644 --- a/eng/update-dependencies/Git/RemoteGitRepoHelper.cs +++ b/eng/update-dependencies/Git/RemoteGitRepoHelper.cs @@ -8,7 +8,7 @@ namespace Dotnet.Docker.Git; -public sealed class RemoteGitRepoHelper( +internal sealed class RemoteGitRepoHelper( string remoteRepoUrl, IRemoteGitRepo remoteGitRepo, ILocalGitRepo localGitRepo, diff --git a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs index b9ae8f4d60..c2f6fc27da 100644 --- a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs +++ b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs @@ -18,7 +18,7 @@ namespace Dotnet.Docker.Sync; /// internal/release/* branch may contain .NET versions that are not yet publicly available. /// "In sync" does not necessarily mean that the commit SHAs of both branches are identical. /// -public sealed class SyncInternalReleaseCommand( +internal sealed class SyncInternalReleaseCommand( IGitRepoHelperFactory gitRepoHelperFactory, ILogger logger ) : BaseCommand diff --git a/eng/update-dependencies/update-dependencies.csproj b/eng/update-dependencies/update-dependencies.csproj index 948948c2e7..aaf2315d41 100644 --- a/eng/update-dependencies/update-dependencies.csproj +++ b/eng/update-dependencies/update-dependencies.csproj @@ -9,6 +9,12 @@ Dotnet.Docker + + + + + + From 44873879d499fcddb0674d5d49d270d21d13a1f1 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 09:59:48 -0700 Subject: [PATCH 06/27] Add FromStagingPipelineCommand dependency to SyncInternalReleaseCommand --- eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs | 2 ++ tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs index c2f6fc27da..e8df83d971 100644 --- a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs +++ b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs @@ -20,10 +20,12 @@ namespace Dotnet.Docker.Sync; /// internal sealed class SyncInternalReleaseCommand( IGitRepoHelperFactory gitRepoHelperFactory, + ICommand updateFromStagingPipeline, ILogger logger ) : BaseCommand { private readonly IGitRepoHelperFactory _gitRepoHelperFactory = gitRepoHelperFactory; + private readonly ICommand _updateFromStagingPipeline = updateFromStagingPipeline; private readonly ILogger _logger = logger; public override async Task ExecuteAsync(SyncInternalReleaseOptions options) diff --git a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs index 309cea0ace..afd4e3d784 100644 --- a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs +++ b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs @@ -1,6 +1,7 @@ // 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; using Dotnet.Docker.Git; using Dotnet.Docker.Sync; using Microsoft.DotNet.DarcLib; @@ -35,11 +36,13 @@ public sealed class SyncInternalReleaseTests /// private SyncInternalReleaseCommand CreateCommand( IGitRepoHelperFactory? repoFactory = null, + ICommand? fromStagingPipelineCommand = null, ILogger? logger = null) { // New parameters should be null by default and initialized with mocks if not specified. return new( repoFactory ?? Mock.Of(), + fromStagingPipelineCommand ?? Mock.Of>(), logger ?? Mock.Of>() ); } From bd3497df80ae5994511f0c77264af0b0b34e5c34 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 10:55:36 -0700 Subject: [PATCH 07/27] Add git restore method --- .../Git/ILocalGitRepoHelper.cs | 16 ++++++++++++++++ .../Git/LocalGitRepoHelper.cs | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/eng/update-dependencies/Git/ILocalGitRepoHelper.cs b/eng/update-dependencies/Git/ILocalGitRepoHelper.cs index 472526b5d1..4c49eeae4a 100644 --- a/eng/update-dependencies/Git/ILocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/ILocalGitRepoHelper.cs @@ -69,4 +69,20 @@ internal interface ILocalGitRepoHelper /// List all remotes for the local repository. /// Task> ListAllRemotesAsync(); + + /// + /// Restore the working tree with the contents from a restore source. + /// Uncommitted changes in the working tree will be lost. + /// + /// + /// 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. + /// + /// + /// Restore the working tree files with the content from this tree. + /// This can be a commit, branch or tag. + /// + /// https://git-scm.com/docs/git-restore + Task RestoreAsync(string source); } diff --git a/eng/update-dependencies/Git/LocalGitRepoHelper.cs b/eng/update-dependencies/Git/LocalGitRepoHelper.cs index 1efe554651..1486ce426b 100644 --- a/eng/update-dependencies/Git/LocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/LocalGitRepoHelper.cs @@ -99,4 +99,10 @@ public async Task> ListAllRemotesAsync() // There are typically two entries per remote (fetch and push); we only want one .Distinct(); } + + /// + public async Task RestoreAsync(string source) + { + await _localGitRepo.ExecuteGitCommand("restore", "--source", source); + } } From cf0d112775017bed89215350379f88d74a699e1d Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Wed, 1 Oct 2025 12:01:52 -0700 Subject: [PATCH 08/27] Add --repo-root argmuent to existing commands --- eng/update-dependencies/BaseUrlUpdater.cs | 9 ++++--- .../CreatePullRequestOptions.cs | 6 +++++ .../CreatePullRequestOptionsExtensions.cs | 10 ++++++++ .../DockerfileShaUpdater.cs | 9 +++---- .../FromStagingPipelineCommand.cs | 5 +++- .../GitHubReleaseUpdaterBase.cs | 4 ++-- .../GitHubReleaseVersionUpdater.cs | 4 ++-- eng/update-dependencies/ManifestHelper.cs | 9 ++++--- eng/update-dependencies/NuGetConfigUpdater.cs | 6 ++--- .../RocksToolboxUpdater.cs | 4 ++-- eng/update-dependencies/SpecificCommand.cs | 24 ++++++++----------- .../SpecificCommandOptions.cs | 5 ++++ .../Sync/InternalVersionsHelper.cs | 10 ++++---- eng/update-dependencies/Tools.cs | 10 ++++---- .../VariableUpdaterBase.cs | 8 +++---- eng/update-dependencies/VersionUpdater.cs | 4 ++-- 16 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 eng/update-dependencies/CreatePullRequestOptionsExtensions.cs diff --git a/eng/update-dependencies/BaseUrlUpdater.cs b/eng/update-dependencies/BaseUrlUpdater.cs index d9e7909061..d104c653c7 100644 --- a/eng/update-dependencies/BaseUrlUpdater.cs +++ b/eng/update-dependencies/BaseUrlUpdater.cs @@ -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. /// - 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) @@ -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; diff --git a/eng/update-dependencies/CreatePullRequestOptions.cs b/eng/update-dependencies/CreatePullRequestOptions.cs index 46e88b2d51..d3250ecc13 100644 --- a/eng/update-dependencies/CreatePullRequestOptions.cs +++ b/eng/update-dependencies/CreatePullRequestOptions.cs @@ -10,6 +10,11 @@ public abstract record CreatePullRequestOptions { private string? _targetBranch = null; + /// + /// The root of the dotnet-docker repo to run against. + /// + public string RepoRoot { get; init; } = Directory.GetCurrentDirectory(); + public string User { get; init; } = ""; public string Email { get; init; } = ""; public string Password { get; init; } = ""; @@ -26,6 +31,7 @@ public string TargetBranch public static List internal class GitHubReleaseVersionUpdater( - string repoRoot, + string manifestVersionsFilePath, string toolName, string variableName, string owner, string repo) : GitHubReleaseUpdaterBase( - repoRoot, + manifestVersionsFilePath, toolName, variableName, owner, diff --git a/eng/update-dependencies/ManifestHelper.cs b/eng/update-dependencies/ManifestHelper.cs index 3d3fb2716d..692e07ac80 100644 --- a/eng/update-dependencies/ManifestHelper.cs +++ b/eng/update-dependencies/ManifestHelper.cs @@ -106,13 +106,12 @@ public static string GetVariableValue(string variableName, JObject variables) ?? throw new ArgumentException($"Manifest does not contain a value for {variableName}"); /// - /// Loads the manifest from the given filename. + /// Loads the manifest from the given file path. /// - /// Name, not path, of the manifest file located at the root of the repo. - public static JObject LoadManifest(string filename) + /// Path of the manifest file located at the root of the repo. + public static JObject LoadManifest(string filePath) { - string path = Path.Combine(SpecificCommand.RepoRoot, filename); - string contents = File.ReadAllText(path); + string contents = File.ReadAllText(filePath); return JObject.Parse(contents); } diff --git a/eng/update-dependencies/NuGetConfigUpdater.cs b/eng/update-dependencies/NuGetConfigUpdater.cs index 25fe51ecee..22ad5aa2b5 100644 --- a/eng/update-dependencies/NuGetConfigUpdater.cs +++ b/eng/update-dependencies/NuGetConfigUpdater.cs @@ -17,17 +17,15 @@ namespace Dotnet.Docker; internal class NuGetConfigUpdater : IDependencyUpdater { private const string PkgSrcSuffix = "_internal"; - private readonly string _repoRoot; private readonly SpecificCommandOptions _options; private readonly string _configPath; - public NuGetConfigUpdater(string repoRoot, SpecificCommandOptions options) + public NuGetConfigUpdater(SpecificCommandOptions options) { - _repoRoot = repoRoot; _options = options; string configSuffix = _options.IsInternal ? ".internal" : _options.SourceBranch == "nightly" ? ".nightly" : string.Empty; - _configPath = Path.Combine(_repoRoot, $"tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/NuGet.config{configSuffix}"); + _configPath = Path.Combine(_options.RepoRoot, $"tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/NuGet.config{configSuffix}"); } public IEnumerable GetUpdateTasks(IEnumerable dependencyInfos) diff --git a/eng/update-dependencies/RocksToolboxUpdater.cs b/eng/update-dependencies/RocksToolboxUpdater.cs index 044762eaa6..f842ea15d3 100644 --- a/eng/update-dependencies/RocksToolboxUpdater.cs +++ b/eng/update-dependencies/RocksToolboxUpdater.cs @@ -14,9 +14,9 @@ internal static class RocksToolboxUpdater private const string Repo = "rocks-toolbox"; - public static IDependencyUpdater GetUpdater(string repoRoot) => + public static IDependencyUpdater GetUpdater(string manifestVersionsFilePath) => new GitHubReleaseVersionUpdater( - repoRoot: repoRoot, + manifestVersionsFilePath: manifestVersionsFilePath, toolName: ToolName, variableName: $"{ToolName}|latest|version", owner: Owner, diff --git a/eng/update-dependencies/SpecificCommand.cs b/eng/update-dependencies/SpecificCommand.cs index 2a0d2fd675..04b46f795f 100644 --- a/eng/update-dependencies/SpecificCommand.cs +++ b/eng/update-dependencies/SpecificCommand.cs @@ -18,8 +18,6 @@ namespace Dotnet.Docker { public class SpecificCommand : BaseCommand { - public const string ManifestFilename = "manifest.json"; - public const string VersionsFilename = "manifest.versions.json"; private static SpecificCommandOptions? s_options; @@ -28,8 +26,6 @@ private static SpecificCommandOptions Options { set => s_options = value; } - public static string RepoRoot { get; } = Directory.GetCurrentDirectory(); - public override async Task ExecuteAsync(SpecificCommandOptions options) { Options = options; @@ -57,7 +53,7 @@ public override async Task ExecuteAsync(SpecificCommandOptions options) if (toolBuildInfos.Length != 0) { - IEnumerable toolUpdaters = Tools.GetToolUpdaters(RepoRoot); + IEnumerable toolUpdaters = Tools.GetToolUpdaters(Options.GetManifestVersionsFilePath()); DependencyUpdateResults toolUpdateResults = UpdateFiles(toolBuildInfos, toolUpdaters); updateResults.Add(toolUpdateResults); } @@ -151,7 +147,7 @@ private static IDependencyInfo CreateDependencyBuildInfo(string name, string? ve private static void PushToAzdoBranch(string commitMessage, string targetBranch) { - using Repository repo = new(RepoRoot); + using Repository repo = new(Options.RepoRoot); // Commit the existing changes Commands.Stage(repo, "*"); @@ -382,16 +378,16 @@ private static IEnumerable GetProductUpdaters() List updaters = [ - new NuGetConfigUpdater(RepoRoot, Options), - BaseUrlUpdater.Create(RepoRoot, Options) + new NuGetConfigUpdater(Options), + BaseUrlUpdater.Create(Options) ]; foreach (string productName in Options.ProductVersions.Keys) { - updaters.Add(new VersionUpdater(VersionType.Build, productName, Options.DockerfileVersion, RepoRoot, Options)); - updaters.Add(new VersionUpdater(VersionType.Product, productName, Options.DockerfileVersion, RepoRoot, Options)); + updaters.Add(new VersionUpdater(VersionType.Build, productName, Options.DockerfileVersion, Options)); + updaters.Add(new VersionUpdater(VersionType.Product, productName, Options.DockerfileVersion, Options)); - foreach (IDependencyUpdater shaUpdater in DockerfileShaUpdater.CreateUpdaters(productName, Options.DockerfileVersion, RepoRoot, Options)) + foreach (IDependencyUpdater shaUpdater in DockerfileShaUpdater.CreateUpdaters(productName, Options.DockerfileVersion, Options)) { updaters.Add(shaUpdater); } @@ -400,10 +396,10 @@ private static IEnumerable GetProductUpdaters() return updaters; } - private static IEnumerable GetGeneratedContentUpdaters() => + private IEnumerable GetGeneratedContentUpdaters() => [ - ScriptRunnerUpdater.GetDockerfileUpdater(RepoRoot), - ScriptRunnerUpdater.GetReadMeUpdater(RepoRoot) + ScriptRunnerUpdater.GetDockerfileUpdater(Options.RepoRoot), + ScriptRunnerUpdater.GetReadMeUpdater(Options.RepoRoot) ]; } } diff --git a/eng/update-dependencies/SpecificCommandOptions.cs b/eng/update-dependencies/SpecificCommandOptions.cs index 17b7a0fbdb..2d010126c2 100644 --- a/eng/update-dependencies/SpecificCommandOptions.cs +++ b/eng/update-dependencies/SpecificCommandOptions.cs @@ -58,6 +58,11 @@ private static List