Skip to content
Open
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
9 changes: 9 additions & 0 deletions eng/docker-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All breaking changes and new features in `eng/docker-tools` will be documented i

---

## 2026-06-01: Post_Build validates manifest list platform completeness

- Pull request: [#2126](https://github.com/dotnet/docker-tools/pull/2126)

The `Create Manifest Lists` step in the `Post_Build` stage now fails if a generated multi-arch manifest tag would omit platforms expected by `manifest.json`.
This is evaluated only after attempting to import missing platform images into staging in the case of a path-filtered build.

---

## 2026-04-02: Extra Docker build options can be passed through ImageBuilder

- Pull request: [#2063](https://github.com/dotnet/docker-tools/pull/2063)
Expand Down
3 changes: 2 additions & 1 deletion eng/docker-tools/DEV-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Build Stage
Post_Build Stage
├── Merge image info files
├── Create multi-arch manifests
└── Consolidate SBOMs
Expand Down Expand Up @@ -191,7 +192,7 @@ Common patterns:
- `"publish"` - Publish only (when re-running a failed publish from a previous build)
- `"build,test,sign,publish"` - Full pipeline

**Note:** The `Post_Build` stage is implicitly included whenever `build` is in the stages list. You don't need to specify it separately—it automatically runs after Build to merge image info files and consolidate SBOMs.
**Note:** The `Post_Build` stage is implicitly included whenever `build` is in the stages list. You don't need to specify it separately—it automatically runs after Build to merge image info files, create and validate multi-arch manifests, and consolidate SBOMs.

The stages variable is useful for:
- Re-running just the publish stage after fixing a transient failure
Expand Down
58 changes: 58 additions & 0 deletions src/ImageBuilder.Tests/CreateManifestListCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,64 @@ public async Task ExecuteAsync_DoesNotPortImagesThatAreAbsentFromImageInfo()
Times.Never);
}

/// <summary>
/// Verifies that a missing manifest platform is reported when the old-build
/// import path has no platform to port forward.
/// </summary>
[Fact]
public async Task ExecuteAsync_ThrowsMissingPlatform_WhenNoOldBuildImportExists()
{
Mock<IManifestService> manifestServiceMock = new(MockBehavior.Strict);
Mock<IDockerService> dockerServiceMock = new();
Mock<ICopyImageService> copyImageServiceMock = new();

CreateManifestListCommand command = CreateCommand(
CreateManifestServiceFactoryMock(manifestServiceMock),
dockerServiceMock,
copyImageServiceMock,
Mock.Of<IDateTimeService>());

using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();

string dockerfileAmd64 = CreateDockerfile("1.0/repo/linux-amd64", tempFolderContext);
string dockerfileArm64 = CreateDockerfile("1.0/repo/linux-arm64", tempFolderContext);

Manifest manifest = CreateManifest(
CreateRepo("repo",
CreateImage(
["sharedtag"],
CreatePlatform(dockerfileAmd64, ["tag-amd64"]),
CreatePlatform(dockerfileArm64, ["tag-arm64"], architecture: Architecture.ARM64))));

ImageArtifactDetails imageArtifactDetails = CreateImageArtifactDetails(
CreateRepoData("repo",
CreateImageData(
CreatePlatform(dockerfileAmd64, simpleTags: ["tag-amd64"]))));

SetupCommand(command, manifest, imageArtifactDetails, tempFolderContext);

InvalidOperationException exception = await Should.ThrowAsync<InvalidOperationException>(
() => command.ExecuteAsync());

exception.Message.ShouldContain("Generated manifest list tags are missing expected platforms defined in the manifest");
exception.Message.ShouldContain("repo:sharedtag");
exception.Message.ShouldContain("linux/arm64");
exception.Message.ShouldContain(dockerfileArm64);

manifestServiceMock.Verify(
o => o.GetManifestAsync(It.IsAny<ImageName>(), It.IsAny<bool>()),
Times.Never);
copyImageServiceMock.Verify(o => o.ImportImageAsync(
It.IsAny<string[]>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<bool>(), It.IsAny<string>(),
It.IsAny<ContainerRegistryImportSourceCredentials>(),
It.IsAny<bool>()),
Times.Never);
dockerServiceMock.Verify(o => o.CreateManifestList(
It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<bool>()),
Times.Never);
}

/// <summary>
/// Verifies that when a manifest-declared platform is missing from image-info AND
/// the source registry returns 404 for its tag (e.g. a misconfigured <c>--path</c>
Expand Down
73 changes: 73 additions & 0 deletions src/ImageBuilder.Tests/ManifestListHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,79 @@ public void GetManifestListsForImages_OnlyIncludesBuiltPlatforms()
results[0].PlatformTags.ShouldNotContain("repo:tag-windows");
}

/// <summary>
/// Verifies that validation reports generated manifest lists that are missing expected platforms.
/// </summary>
[Fact]
public void GetManifestListPlatformValidationIssues_MissingPlatform()
{
using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();

string dockerfileAmd64 = CreateDockerfile("1.0/repo/linux-amd64", tempFolderContext);
string dockerfileArm64 = CreateDockerfile("1.0/repo/linux-arm64", tempFolderContext);
string dockerfileWindows = CreateDockerfile("1.0/repo/windows", tempFolderContext);

Manifest manifest = CreateManifest(
CreateRepo("repo",
CreateImage(
["sharedtag"],
CreatePlatform(dockerfileAmd64, ["tag-amd64"]),
CreatePlatform(dockerfileArm64, ["tag-arm64"], architecture: Architecture.ARM64),
CreatePlatform(dockerfileWindows, ["tag-windows"], os: OS.Windows, osVersion: "ltsc2022"))));

ImageArtifactDetails imageArtifactDetails = CreateImageArtifactDetails(
CreateRepoData("repo",
CreateImageData(
["sharedtag"],
CreatePlatform(dockerfileAmd64, simpleTags: ["tag-amd64"]),
CreatePlatform(dockerfileArm64, simpleTags: ["tag-arm64"], architecture: "arm64"))));

ManifestInfo manifestInfo = LoadManifest(manifest, tempFolderContext);
ImageArtifactDetails linkedImageInfo = LoadImageInfo(imageArtifactDetails, manifestInfo, tempFolderContext);

IReadOnlyList<ManifestListPlatformValidationIssue> issues =
ManifestListHelper.GetManifestListPlatformValidationIssues(
manifestInfo, linkedImageInfo, repoPrefix: null);

issues.Count.ShouldBe(1);
issues[0].ManifestListTag.ShouldBe("repo:sharedtag");
issues[0].MissingPlatforms.Count.ShouldBe(1);
issues[0].MissingPlatforms[0].ShouldContain("windows/amd64");
issues[0].MissingPlatforms[0].ShouldContain(dockerfileWindows);
}

/// <summary>
/// Verifies that validation succeeds when every generated manifest list has all expected platforms.
/// </summary>
[Fact]
public void ValidateManifestListPlatforms_AllPlatformsPresent()
{
using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();

string dockerfileAmd64 = CreateDockerfile("1.0/repo/linux-amd64", tempFolderContext);
string dockerfileArm64 = CreateDockerfile("1.0/repo/linux-arm64", tempFolderContext);

Manifest manifest = CreateManifest(
CreateRepo("repo",
CreateImage(
["sharedtag"],
CreatePlatform(dockerfileAmd64, ["tag-amd64"]),
CreatePlatform(dockerfileArm64, ["tag-arm64"], architecture: Architecture.ARM64))));

ImageArtifactDetails imageArtifactDetails = CreateImageArtifactDetails(
CreateRepoData("repo",
CreateImageData(
["sharedtag"],
CreatePlatform(dockerfileAmd64, simpleTags: ["tag-amd64"]),
CreatePlatform(dockerfileArm64, simpleTags: ["tag-arm64"], architecture: "arm64"))));

ManifestInfo manifestInfo = LoadManifest(manifest, tempFolderContext);
ImageArtifactDetails linkedImageInfo = LoadImageInfo(imageArtifactDetails, manifestInfo, tempFolderContext);

Should.NotThrow(() => ManifestListHelper.ValidateManifestListPlatforms(
manifestInfo, linkedImageInfo, repoPrefix: null));
}

/// <summary>
/// Verifies that no manifest list is returned when an image has shared tags
/// but no platforms exist in image-info.
Expand Down
5 changes: 4 additions & 1 deletion src/ImageBuilder/Commands/CreateManifestListCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
AddPlatformToImageInfo(platformToImport);
}

ManifestListHelper.ValidateManifestListPlatforms(
Manifest, imageArtifactDetails, Options.RepoPrefix);

// Build the manifest-list definitions from the now-complete image-info.
IReadOnlyList<ManifestListInfo> manifestLists =
ManifestListHelper.GetManifestListsForImages(Manifest, imageArtifactDetails, Options.RepoPrefix);
Expand Down Expand Up @@ -192,7 +195,7 @@ imageData.Manifest is not null

ManifestQueryResult result = await _manifestService.Value.GetManifestAsync(sourceName, isDryRun: false);

var platformImportData = new PlatformImportData(
PlatformImportData platformImportData = new(
Repo: imageData.ManifestRepo,
Image: imageData.ManifestImage,
Platform: platform,
Expand Down
Loading
Loading