From 45d274fd132f6824d2f38e67176e2d88b1983826 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 28 Jul 2026 09:14:03 -0700 Subject: [PATCH 01/39] Add artifact output service Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0f6c174-1904-42d2-8829-bc3cd05e05d0 --- src/ImageBuilder.Tests/OutputServiceTests.cs | 62 ++++++++++++++++++++ src/ImageBuilder/IOutputService.cs | 10 ++++ src/ImageBuilder/ImageBuilder.cs | 1 + src/ImageBuilder/OutputService.cs | 59 +++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 src/ImageBuilder.Tests/OutputServiceTests.cs create mode 100644 src/ImageBuilder/IOutputService.cs create mode 100644 src/ImageBuilder/OutputService.cs diff --git a/src/ImageBuilder.Tests/OutputServiceTests.cs b/src/ImageBuilder.Tests/OutputServiceTests.cs new file mode 100644 index 000000000..92b2bb4f9 --- /dev/null +++ b/src/ImageBuilder.Tests/OutputServiceTests.cs @@ -0,0 +1,62 @@ +// 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; +using System.IO; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.DotNet.ImageBuilder.Tests.Helpers; +using Microsoft.Extensions.Options; +using Shouldly; + +namespace Microsoft.DotNet.ImageBuilder.Tests; + +[TestClass] +public class OutputServiceTests +{ + private static readonly string s_outputRoot = Path.Combine(Path.GetTempPath(), "artifacts"); + + [TestMethod] + public void WriteAllText_WritesUnderArtifactStagingDirectory() + { + var fileSystem = new InMemoryFileSystem(); + var service = CreateService(fileSystem); + string expectedPath = Path.Combine(s_outputRoot, "image-info", "output.json"); + + service.WriteAllText(Path.Combine("image-info", "output.json"), "contents"); + + fileSystem.DirectoriesCreated.ShouldContain(Path.GetDirectoryName(expectedPath)); + fileSystem.GetFileText(expectedPath).ShouldBe("contents"); + } + + [TestMethod] + public void WriteAllText_MissingArtifactStagingDirectory_Throws() + { + var service = new OutputService( + new InMemoryFileSystem(), + Options.Create(new BuildConfiguration())); + + InvalidOperationException exception = Should.Throw( + () => service.WriteAllText("output.json", "contents")); + + exception.Message.ShouldContain(nameof(BuildConfiguration.ArtifactStagingDirectory)); + } + + [TestMethod] + public void WriteAllText_PathOutsideArtifactStagingDirectory_Throws() + { + var service = CreateService(new InMemoryFileSystem()); + + Should.Throw( + () => service.WriteAllText(Path.Combine("..", "output.json"), "contents")); + } + + private static OutputService CreateService(IFileSystem fileSystem) => + new( + fileSystem, + Options.Create( + new BuildConfiguration + { + ArtifactStagingDirectory = s_outputRoot + })); +} diff --git a/src/ImageBuilder/IOutputService.cs b/src/ImageBuilder/IOutputService.cs new file mode 100644 index 000000000..9850b1b6d --- /dev/null +++ b/src/ImageBuilder/IOutputService.cs @@ -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. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.DotNet.ImageBuilder; + +public interface IOutputService +{ + void WriteAllText(string artifactPath, string contents); +} diff --git a/src/ImageBuilder/ImageBuilder.cs b/src/ImageBuilder/ImageBuilder.cs index fe496a706..ff3b3765f 100644 --- a/src/ImageBuilder/ImageBuilder.cs +++ b/src/ImageBuilder/ImageBuilder.cs @@ -42,6 +42,7 @@ public static IHost CreateAppHost() // Register abstractions builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Register services builder.Services.AddSingleton(); diff --git a/src/ImageBuilder/OutputService.cs b/src/ImageBuilder/OutputService.cs new file mode 100644 index 000000000..3db952871 --- /dev/null +++ b/src/ImageBuilder/OutputService.cs @@ -0,0 +1,59 @@ +// 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; +using System.IO; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.DotNet.ImageBuilder; + +public sealed class OutputService(IFileSystem fileSystem, IOptions buildConfigOptions) + : IOutputService +{ + private readonly IFileSystem _fileSystem = fileSystem; + private readonly BuildConfiguration _buildConfig = buildConfigOptions.Value; + + public void WriteAllText(string artifactPath, string contents) + { + string outputPath = GetOutputPath(artifactPath); + string? outputDirectory = Path.GetDirectoryName(outputPath); + if (outputDirectory is not null) + { + _fileSystem.CreateDirectory(outputDirectory); + } + + _fileSystem.WriteAllText(outputPath, contents); + } + + private string GetOutputPath(string artifactPath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(artifactPath); + + if (string.IsNullOrWhiteSpace(_buildConfig.ArtifactStagingDirectory)) + { + throw new InvalidOperationException( + $"{nameof(BuildConfiguration.ArtifactStagingDirectory)} is not set. " + + "Configure it in appsettings.json or via environment variables."); + } + + if (Path.IsPathRooted(artifactPath)) + { + throw new ArgumentException("Output artifact paths must be relative.", nameof(artifactPath)); + } + + string outputRoot = Path.GetFullPath(_buildConfig.ArtifactStagingDirectory); + string outputPath = Path.GetFullPath(artifactPath, outputRoot); + string relativeOutputPath = Path.GetRelativePath(outputRoot, outputPath); + if (relativeOutputPath == ".." + || relativeOutputPath.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) + { + throw new ArgumentException( + "Output artifact paths must remain within the artifact staging directory.", + nameof(artifactPath)); + } + + return outputPath; + } +} From 59945ee40e4f98ebacc2f1a683fb6dffb1ed66d6 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 28 Jul 2026 09:17:28 -0700 Subject: [PATCH 02/39] Resolve merge output from artifact staging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0f6c174-1904-42d2-8829-bc3cd05e05d0 --- eng/docker-tools/templates/jobs/publish.yml | 2 +- src/ImageBuilder.Tests/Helpers/TestHelper.cs | 11 +++++ .../MergeImageInfoFilesCommandTests.cs | 41 +++++++++++-------- .../Commands/MergeImageInfoCommand.cs | 12 +++++- .../Commands/MergeImageInfoOptions.cs | 2 +- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/eng/docker-tools/templates/jobs/publish.yml b/eng/docker-tools/templates/jobs/publish.yml index 5839be2d7..b3d93e17a 100644 --- a/eng/docker-tools/templates/jobs/publish.yml +++ b/eng/docker-tools/templates/jobs/publish.yml @@ -156,7 +156,7 @@ jobs: - script: > $(runImageBuilderCmd) mergeImageInfo $(imageInfoContainerDir) - $(imageInfoContainerDir)/full-image-info-new.json + imageInfo/full-image-info-new.json $(manifestVariables) $(dryRunArg) --manifest $(manifest) diff --git a/src/ImageBuilder.Tests/Helpers/TestHelper.cs b/src/ImageBuilder.Tests/Helpers/TestHelper.cs index 7489caddd..f32fad5fa 100644 --- a/src/ImageBuilder.Tests/Helpers/TestHelper.cs +++ b/src/ImageBuilder.Tests/Helpers/TestHelper.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.Extensions.Options; namespace Microsoft.DotNet.ImageBuilder.Tests.Helpers { @@ -20,6 +22,15 @@ public static IManifestJsonService CreateManifestJsonService() => fileSystem: new FileSystem(), logger: new LoggerFactory().CreateLogger()); + public static IOutputService CreateOutputService(string artifactStagingDirectory) => + new OutputService( + new FileSystem(), + Options.Create( + new BuildConfiguration + { + ArtifactStagingDirectory = artifactStagingDirectory + })); + public static TempFolderContext UseTempFolder() { return new TempFolderContext(); diff --git a/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs b/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs index 733247b4f..bbb1d2a38 100644 --- a/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs +++ b/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs @@ -181,9 +181,9 @@ public async Task MergeImageInfoFilesCommand_HappyPath() } }; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(context.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(context.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(context.Path, "manifest.json"); Directory.CreateDirectory(command.Options.SourceImageInfoFolderPath); @@ -228,7 +228,7 @@ public async Task MergeImageInfoFilesCommand_HappyPath() command.LoadManifest(); await command.ExecuteAsync(); - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText(Path.Combine(context.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); PlatformData expectedPlatform = CreatePlatform( @@ -485,9 +485,9 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() } }; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(context.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(context.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(context.Path, "manifest.json"); Directory.CreateDirectory(command.Options.SourceImageInfoFolderPath); @@ -524,7 +524,7 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() command.LoadManifest(); await command.ExecuteAsync(); - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText(Path.Combine(context.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); ImageArtifactDetails expected = new ImageArtifactDetails @@ -620,7 +620,7 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() [TestMethod] public async Task MergeImageInfoFilesCommand_SourceFolderPathNotFound() { - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(Path.GetTempPath()); command.Options.SourceImageInfoFolderPath = "foo"; command.Options.DestinationImageInfoPath = "output.json"; @@ -643,7 +643,7 @@ public async Task MergeImageInfoFilesCommand_SourceFolderEmpty() // Store the content in a .txt file which the command should NOT be looking for. File.WriteAllText("image-info.txt", JsonHelper.SerializeObject(imageArtifactDetails)); - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = context.Path; command.Options.DestinationImageInfoPath = "output.json"; @@ -772,9 +772,9 @@ public async Task MergeImageInfoFilesCommand_Publish_ReplaceContent() } }; - MergeImageInfoCommand command = new(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(tempFolderContext.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(tempFolderContext.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(tempFolderContext.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.IsPublishScenario = true; @@ -828,7 +828,8 @@ public async Task MergeImageInfoFilesCommand_Publish_ReplaceContent() } }; - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText( + Path.Combine(tempFolderContext.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); CompareImageArtifactDetails(expectedImageArtifactDetails, actual); @@ -939,9 +940,9 @@ public async Task MergeImageInfoFilesCommand_Publish_RemoveOutOfDateContent() } }; - MergeImageInfoCommand command = new(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(tempFolderContext.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(tempFolderContext.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(tempFolderContext.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.IsPublishScenario = true; @@ -983,7 +984,8 @@ public async Task MergeImageInfoFilesCommand_Publish_RemoveOutOfDateContent() } }; - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText( + Path.Combine(tempFolderContext.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); CompareImageArtifactDetails(expectedImageArtifactDetails, actual); @@ -1116,9 +1118,9 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() File.WriteAllText(updatedImageInfoFile, JsonHelper.SerializeObject(updatedImageInfo)); File.WriteAllText(manifestFile, JsonConvert.SerializeObject(manifest)); - var outputImageInfoFile = Path.Combine(context.Path, "merged-image-info.json"); + const string outputImageInfoFile = "merged-image-info.json"; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = sourceImageInfoDir; command.Options.DestinationImageInfoPath = outputImageInfoFile; command.Options.InitialImageInfoPath = initialImageInfoFile; @@ -1128,7 +1130,7 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() await command.ExecuteAsync(); // Verify the merged result - string resultContent = File.ReadAllText(outputImageInfoFile); + string resultContent = File.ReadAllText(Path.Combine(context.Path, outputImageInfoFile)); ImageArtifactDetails mergedImageInfo = ImageArtifactDetails.FromJson(resultContent); mergedImageInfo.Repos.ShouldHaveSingleItem(); @@ -1154,5 +1156,10 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() var initialCommitResult = initialShaMatches[0].Value; initialCommitResult.ShouldBe(CommitOverride); } + + private static MergeImageInfoCommand CreateCommand(string artifactStagingDirectory) => + new( + TestHelper.CreateManifestJsonService(), + TestHelper.CreateOutputService(artifactStagingDirectory)); } } diff --git a/src/ImageBuilder/Commands/MergeImageInfoCommand.cs b/src/ImageBuilder/Commands/MergeImageInfoCommand.cs index 02395be50..6622f66c2 100644 --- a/src/ImageBuilder/Commands/MergeImageInfoCommand.cs +++ b/src/ImageBuilder/Commands/MergeImageInfoCommand.cs @@ -15,7 +15,15 @@ namespace Microsoft.DotNet.ImageBuilder.Commands { public partial class MergeImageInfoCommand : ManifestCommand { - public MergeImageInfoCommand(IManifestJsonService manifestJsonService) : base(manifestJsonService) { } + private readonly IOutputService _outputService; + + public MergeImageInfoCommand( + IManifestJsonService manifestJsonService, + IOutputService outputService) + : base(manifestJsonService) + { + _outputService = outputService; + } protected override string Description => "Merges the content of multiple image info files into one file"; @@ -89,7 +97,7 @@ public override Task ExecuteAsync() } string destinationContents = JsonHelper.SerializeObject(targetImageArtifactDetails) + Environment.NewLine; - File.WriteAllText(Options.DestinationImageInfoPath, destinationContents); + _outputService.WriteAllText(Options.DestinationImageInfoPath, destinationContents); return Task.CompletedTask; } diff --git a/src/ImageBuilder/Commands/MergeImageInfoOptions.cs b/src/ImageBuilder/Commands/MergeImageInfoOptions.cs index 5e986819d..ec8260823 100644 --- a/src/ImageBuilder/Commands/MergeImageInfoOptions.cs +++ b/src/ImageBuilder/Commands/MergeImageInfoOptions.cs @@ -27,7 +27,7 @@ public class MergeImageInfoOptions : ManifestOptions private static readonly Argument DestinationImageInfoPathArgument = new(nameof(DestinationImageInfoPath)) { - Description = "Path to store the merged image info content" + Description = "Artifact-relative path to store the merged image info content" }; private static readonly Option PublishOption = new("--publish") From a49b685138649128ee3d746f217231dbaeb5fa9a Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 28 Jul 2026 09:20:40 -0700 Subject: [PATCH 03/39] Resolve EOL data output from artifact staging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0f6c174-1904-42d2-8829-bc3cd05e05d0 --- eng/docker-tools/templates/jobs/publish.yml | 5 +---- .../GenerateEolAnnotationDataForPublishCommandTests.cs | 5 +++-- .../Commands/GenerateEolAnnotationDataCommandBase.cs | 5 ++++- .../GenerateEolAnnotationDataForPublishCommand.cs | 8 +++++++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/eng/docker-tools/templates/jobs/publish.yml b/eng/docker-tools/templates/jobs/publish.yml index b3d93e17a..de844ddc9 100644 --- a/eng/docker-tools/templates/jobs/publish.yml +++ b/eng/docker-tools/templates/jobs/publish.yml @@ -139,9 +139,6 @@ jobs: dryRunArg: $(dryRunArg) condition: and(succeeded(), eq(variables['publishReadme'], 'true')) - - script: mkdir -p $(Build.ArtifactStagingDirectory)/eol-annotation-data - displayName: Create EOL Annotation Data Directory - - script: |- cd $(versionsRepoRoot) git pull origin $(gitHubVersionsRepoInfo.branch) @@ -197,7 +194,7 @@ jobs: generateEolAnnotationDataForPublish '${{ parameters.publishConfig.PublishRegistry.server }}' '${{ parameters.publishConfig.PublishRegistry.repoPrefix }}' - '$(artifactsPath)/eol-annotation-data/eol-annotation-data.json' + 'eol-annotation-data/eol-annotation-data.json' '$(imageInfoContainerDir)/full-image-info-orig.json' '$(imageInfoContainerDir)/full-image-info-new.json' $(generateEolAnnotationDataExtraOptions) diff --git a/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs b/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs index b59776879..3e011d332 100644 --- a/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs +++ b/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs @@ -1148,10 +1148,11 @@ private static GenerateEolAnnotationDataForPublishCommand InitializeCommand( acrClientFactory: registryClientFactory, acrContentClientFactory: registryContentClientFactory, lifecycleMetadataService: lifecycleMetadataService, - registryCredentialsProvider: Mock.Of()); + registryCredentialsProvider: Mock.Of(), + outputService: TestHelper.CreateOutputService(Path.GetDirectoryName(newEolDigestsListPath))); command.Options.OldImageInfoPath = oldImageInfoPath; command.Options.NewImageInfoPath = newImageInfoPath; - command.Options.EolDigestsListPath = newEolDigestsListPath; + command.Options.EolDigestsListPath = Path.GetFileName(newEolDigestsListPath); command.Options.RegistryOptions = new() { RepoPrefix = repoPrefix, Registry = AcrName }; return command; } diff --git a/src/ImageBuilder/Commands/GenerateEolAnnotationDataCommandBase.cs b/src/ImageBuilder/Commands/GenerateEolAnnotationDataCommandBase.cs index ceda8cfa1..63b92aee5 100644 --- a/src/ImageBuilder/Commands/GenerateEolAnnotationDataCommandBase.cs +++ b/src/ImageBuilder/Commands/GenerateEolAnnotationDataCommandBase.cs @@ -127,9 +127,12 @@ protected void WriteDigestDataJson(IEnumerable digestsToAnnotate) eolAnnotations, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - File.WriteAllText(Options.EolDigestsListPath, annotationsJson); + WriteOutput(Options.EolDigestsListPath, annotationsJson); } + protected virtual void WriteOutput(string outputPath, string contents) => + File.WriteAllText(outputPath, contents); + private async Task> GetDigestsWithoutExistingAnnotationAsync( IEnumerable unsupportedDigests) { diff --git a/src/ImageBuilder/Commands/GenerateEolAnnotationDataForPublishCommand.cs b/src/ImageBuilder/Commands/GenerateEolAnnotationDataForPublishCommand.cs index 2b037fdba..1a86c555d 100644 --- a/src/ImageBuilder/Commands/GenerateEolAnnotationDataForPublishCommand.cs +++ b/src/ImageBuilder/Commands/GenerateEolAnnotationDataForPublishCommand.cs @@ -16,13 +16,15 @@ public class GenerateEolAnnotationDataForPublishCommand : GenerateEolAnnotationDataCommandBase { private readonly ILogger _logger; + private readonly IOutputService _outputService; public GenerateEolAnnotationDataForPublishCommand( ILogger logger, IAcrClientFactory acrClientFactory, IAcrContentClientFactory acrContentClientFactory, ILifecycleMetadataService lifecycleMetadataService, - IRegistryCredentialsProvider registryCredentialsProvider) + IRegistryCredentialsProvider registryCredentialsProvider, + IOutputService outputService) : base( logger, acrContentClientFactory, @@ -31,10 +33,14 @@ public GenerateEolAnnotationDataForPublishCommand( registryCredentialsProvider) { _logger = logger; + _outputService = outputService; } protected override string Description => "Generate EOL annotation data for all images not described in the new image info file"; + protected override void WriteOutput(string outputPath, string contents) => + _outputService.WriteAllText(outputPath, contents); + protected override async Task> GetDigestsToAnnotateAsync() { if (!File.Exists(Options.OldImageInfoPath) && !File.Exists(Options.NewImageInfoPath)) From a1d49e79f0171d5d4c6f10afc002882a2f47c8e8 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 28 Jul 2026 09:22:25 -0700 Subject: [PATCH 04/39] Resolve annotation output from artifact staging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0f6c174-1904-42d2-8829-bc3cd05e05d0 --- .../templates/steps/annotate-eol-digests.yml | 4 +--- .../AnnotateEolDigestsCommandTests.cs | 5 +++-- .../Commands/AnnotateEolDigestsCommand.cs | 13 +++++++++++-- .../Commands/AnnotateEolDigestsOptions.cs | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/eng/docker-tools/templates/steps/annotate-eol-digests.yml b/eng/docker-tools/templates/steps/annotate-eol-digests.yml index 8e2f7571b..7cff421fe 100644 --- a/eng/docker-tools/templates/steps/annotate-eol-digests.yml +++ b/eng/docker-tools/templates/steps/annotate-eol-digests.yml @@ -6,8 +6,6 @@ parameters: type: string steps: - - script: mkdir -p $(Build.ArtifactStagingDirectory)/annotation-digests - displayName: Create Annotation Digests Directory - template: /eng/docker-tools/templates/steps/run-imagebuilder.yml@self parameters: displayName: Annotate EOL Images (${{ parameters.acr.server }}) @@ -18,7 +16,7 @@ steps: "${{ parameters.dataFile }}" "${{ parameters.acr.server }}" "${{ parameters.acr.repoPrefix }}" - $(artifactsPath)/annotation-digests/annotation-digests.txt + annotation-digests/annotation-digests.txt $(dryRunArg) - template: /eng/docker-tools/templates/steps/publish-artifact.yml@self parameters: diff --git a/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs b/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs index 39701a638..28d8dc1fa 100644 --- a/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs +++ b/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs @@ -145,11 +145,12 @@ private AnnotateEolDigestsCommand InitializeCommand( AnnotateEolDigestsCommand command = new( loggerServiceMock.Object, lifecycleMetadataServiceMock.Object, - Mock.Of()); + Mock.Of(), + TestHelper.CreateOutputService(tempFolderContext.Path)); command.Options.RepoPrefix = RepoPrefix; command.Options.AcrName = AcrName; command.Options.EolDigestsListPath = eolDigestsListPath; - command.Options.AnnotationDigestsOutputPath = Path.Combine(tempFolderContext.Path, AnnotationsOutputPath); + command.Options.AnnotationDigestsOutputPath = AnnotationsOutputPath; return command; } diff --git a/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs b/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs index 876fc4931..c8812f567 100644 --- a/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs +++ b/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs @@ -21,6 +21,7 @@ public class AnnotateEolDigestsCommand : Command private readonly ILogger _logger; private readonly ILifecycleMetadataService _lifecycleMetadataService; private readonly IRegistryCredentialsProvider _registryCredentialsProvider; + private readonly IOutputService _outputService; private readonly ConcurrentBag _failedAnnotationImageDigests = []; private readonly ConcurrentBag _skippedAnnotationImageDigests = []; private readonly ConcurrentBag _existingAnnotationImageDigests = []; @@ -36,11 +37,13 @@ public class AnnotateEolDigestsCommand : Command public AnnotateEolDigestsCommand( ILogger logger, ILifecycleMetadataService lifecycleMetadataService, - IRegistryCredentialsProvider registryCredentialsProvider) + IRegistryCredentialsProvider registryCredentialsProvider, + IOutputService outputService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _lifecycleMetadataService = lifecycleMetadataService ?? throw new ArgumentNullException(nameof(lifecycleMetadataService)); _registryCredentialsProvider = registryCredentialsProvider ?? throw new ArgumentNullException(nameof(registryCredentialsProvider)); + _outputService = outputService ?? throw new ArgumentNullException(nameof(outputService)); } protected override string Description => "Annotates EOL digests in Docker Registry"; @@ -78,7 +81,13 @@ await Parallel.ForEachAsync(eolAnnotations.EolDigests, CancellationToken.None, $"Some digest annotations failed or were skipped due to existing non-matching EOL date annotations (failed: {_failedAnnotationImageDigests.Count}, skipped: {_existingAnnotationImageDigests.Count})."); } - File.WriteAllLines(Options.AnnotationDigestsOutputPath, _createdAnnotationDigests.Order()); + string annotationDigests = string.Join(Environment.NewLine, _createdAnnotationDigests.Order()); + if (annotationDigests.Length > 0) + { + annotationDigests += Environment.NewLine; + } + + _outputService.WriteAllText(Options.AnnotationDigestsOutputPath, annotationDigests); } private void WriteNonEmptySummaryForAnnotationDigests(IEnumerable annotationDigests, string message) diff --git a/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs b/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs index 4c92d1064..6275026b8 100644 --- a/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs +++ b/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs @@ -34,7 +34,7 @@ public class AnnotateEolDigestsOptions : Options private static readonly Argument AnnotationDigestsOutputPathArgument = new(nameof(AnnotationDigestsOutputPath)) { - Description = "Output path of file containing the list of annotation digests that were created" + Description = "Artifact-relative output path for the list of annotation digests that were created" }; public override IEnumerable