Repository navigation
Move manifest list creation to Post_Build, copy via ACR import - #2030
Conversation
Add createManifestList command that runs in Post_Build after mergeImageInfo to create Docker manifest lists, push them, and record their digests in image-info.json. This enables manifest list signing by ensuring digests are present before the Sign stage. Extract ManifestListHelper to determine which manifest lists to create based on which platforms were actually built, preventing manifest lists from referencing unbuilt images. Extend CopyAcrImagesCommand to copy manifest list shared tags alongside platform tags via ACR import, making Publish a pure copy stage. Remove PublishManifestCommand - no longer needed. Fixes: dotnet#2002 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Moves Docker manifest list creation earlier in the pipeline (Post_Build) so manifest list digests are available in image-info.json before the Sign stage, and updates publish copying to include those manifest lists via ACR import.
Changes:
- Add
CreateManifestListCommand(Post_Build) to create/push manifest lists and write their digests intoimage-info.json. - Extend
CopyAcrImagesCommandto also import/copy manifest list shared tags (including syndicated tags). - Extract manifest list selection logic into
ManifestListHelperand removePublishManifestCommand(and its options/tests).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ImageBuilder/ManifestListHelper.cs | New helper to compute which manifest lists to create based on built/changed platforms. |
| src/ImageBuilder/ImageBuilder.cs | Registers CreateManifestListCommand and removes PublishManifestCommand from DI. |
| src/ImageBuilder/Commands/PublishManifestOptions.cs | Removed (no longer needed after moving manifest list creation). |
| src/ImageBuilder/Commands/PublishManifestCommand.cs | Removed (publish stage no longer creates manifests). |
| src/ImageBuilder/Commands/CreateManifestListOptions.cs | New CLI options for the Post_Build manifest list creation command. |
| src/ImageBuilder/Commands/CreateManifestListCommand.cs | New command to create/push manifest lists and persist digests to image-info.json. |
| src/ImageBuilder/Commands/CopyAcrImagesCommand.cs | Adds ACR import of manifest list tags in addition to platform tags. |
| src/ImageBuilder.Tests/PublishManifestCommandTests.cs | Removed tests for the deleted command. |
| src/ImageBuilder.Tests/ManifestListHelperTests.cs | New unit tests for manifest list selection logic. |
| src/ImageBuilder.Tests/CreateManifestListCommandTests.cs | New unit tests for manifest list creation + digest recording. |
| src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs | Adds coverage for copying manifest list tags (including syndicated tags). |
You can also share your feedback on Copilot code review. Take the survey.
The source tag for syndicated manifest list copies was incorrectly reusing the primary repo's source tag. Recompute it from the syndicated destination tag via GetSourceTag. Also tighten test assertions to verify exact source tags instead of It.IsAny<string>(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…foFileAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Moves Docker manifest list creation earlier in the pipeline (Post_Build) so manifest list digests exist in image-info.json before the Sign stage, and updates publish-time copy behavior to include manifest list shared tags via ACR import.
Changes:
- Add
CreateManifestListCommand(Post_Build) to create/push manifest lists and write their digests back toimage-info.json. - Introduce
ManifestListHelper(extracted from the old publish command) to compute which manifest lists to create based on actually-built platforms. - Extend
CopyAcrImagesCommandto copy manifest list shared tags (including syndicated variants) alongside platform tags; removePublishManifestCommandand its options/tests, replacing with new targeted tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ImageBuilder/ManifestListHelper.cs | New helper to compute manifest lists to create based on manifest + image-info (built platforms). |
| src/ImageBuilder/ImageBuilder.cs | Registers CreateManifestListCommand and removes PublishManifestCommand. |
| src/ImageBuilder/Commands/CreateManifestListOptions.cs | CLI options/args for the new Post_Build manifest list creation command. |
| src/ImageBuilder/Commands/CreateManifestListCommand.cs | Creates/pushes manifest lists and records their digests into image-info.json. |
| src/ImageBuilder/Commands/CopyAcrImagesCommand.cs | Copies manifest list shared tags (and syndicated tags) via ACR import in addition to platform tags. |
| src/ImageBuilder.Tests/ManifestListHelperTests.cs | Adds unit tests for manifest list selection logic (built platforms, syndication, prefixes, etc.). |
| src/ImageBuilder.Tests/CreateManifestListCommandTests.cs | Adds tests for end-to-end manifest list creation/push + digest recording. |
| src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs | Adds coverage ensuring manifest list tags (incl. syndicated) are imported/copied. |
| src/ImageBuilder/Commands/PublishManifestCommand.cs | Removed (manifest list creation no longer happens in Publish stage). |
| src/ImageBuilder/Commands/PublishManifestOptions.cs | Removed (options tied to removed command). |
| src/ImageBuilder.Tests/PublishManifestCommandTests.cs | Removed and replaced by new helper/command-focused test coverage. |
You can also share your feedback on Copilot code review. Take the survey.
| PlatformInfo platformInfo = repo.AllImages | ||
| .SelectMany(img => | ||
| img.AllPlatforms | ||
| .Select(p => (Image: img, Platform: p)) | ||
| .Where(imagePlatform => platform != imagePlatform.Platform && | ||
| PlatformInfo.AreMatchingPlatforms(image, platform, imagePlatform.Image, imagePlatform.Platform) && | ||
| imagePlatform.Platform.Tags.Any())) | ||
| .FirstOrDefault() | ||
| .Platform; | ||
|
|
||
| if (platformInfo is null) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"Could not find a platform with concrete tags for '{platform.DockerfilePathRelativeToManifest}'."); | ||
| } | ||
|
|
| @@ -49,11 +49,11 @@ public override async Task ExecuteAsync() | |||
| return; | |||
| } | |||
| PlatformInfo platformInfo = repo.AllImages | ||
| .SelectMany(img => | ||
| img.AllPlatforms | ||
| .Select(p => (Image: img, Platform: p)) | ||
| .Where(imagePlatform => platform != imagePlatform.Platform && | ||
| PlatformInfo.AreMatchingPlatforms(image, platform, imagePlatform.Image, imagePlatform.Platform) && | ||
| imagePlatform.Platform.Tags.Any())) | ||
| .FirstOrDefault() | ||
| .Platform; | ||
|
|
||
| if (platformInfo is null) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"Could not find a platform with concrete tags for '{platform.DockerfilePathRelativeToManifest}'."); | ||
| } | ||
|
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…/docker-tools into move-manifest-lists-src
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use Cast<nullable>().FirstOrDefault() instead of FirstOrDefault().Platform to properly handle the no-match case with nullable types rather than relying on a non-nullable variable silently holding null. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add string.IsNullOrEmpty check before File.Exists to explicitly handle the case where ImageInfoPath is null, rather than relying on File.Exists behavior with null input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-src # Conflicts: # src/ImageBuilder/LifecycleMetadataService.cs
The `Post Publish Notification` step in the Publish stage fails with a `NullReferenceException` on PR builds ([example](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1358504)). **Root cause:** The `Create Manifest Lists` step in `post-build.yml` is skipped for PR builds (`ne(variables['Build.Reason'], 'PullRequest')`) because it requires registry access. Without that step, manifest digests in `image-info.json` are null. The `postPublishNotification` command then crashes when it tries to read those digests. This was introduced by #2030 (moved manifest list creation to Post_Build) and activated by #2038 (updated the ImageBuilder tag to include that change). **Fix:** Add `ne(variables['Build.Reason'], 'PullRequest')` to the `Post Publish Notification` condition, consistent with how `Create Manifest Lists` handles PR builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Path-filtered builds (e.g., --path 'samples/*/Dockerfile.nanoserver') produce image-info containing only the rebuilt platforms. Combined with PR dotnet#2030's move of manifest-list creation from the prod repo into the scoped staging repo, the shared-tag manifest list created from that image-info only references the rebuilt subset. The subsequent copyAcrImages step then overwrites the previously complete shared tags in the prod repo with this degraded list. Add an ISharedTagPortingService that runs before manifest-list creation in CreateManifestListCommand. For each image with shared tags that has manifest-declared platforms missing from image-info, it: 1. Looks up the platform's representative tag in the source registry (the manifest's model registry, before --registry-override). 2. Imports the resolved image (by digest, copying referrers) into the scoped staging repo at the platform's declared simple tags. 3. Synthesizes a PlatformData entry in image-info so the existing ManifestListHelper logic and the downstream copyAcrImages step pick the ported platform up naturally. Platforms with no representative tag at the source (genuinely new) are skipped with a warning, preserving today's behavior for net-new shared tags. Fixes dotnet#2107. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add manifest list property tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Clarify manifest list property tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Remove unnecessary TestPlatform class WIP this is totally useless Remove csharp Copilot instructions Remove junk tests Add comments Update test to be more accurate Port unrebuilt platforms forward for shared-tag manifest lists Path-filtered builds (e.g., --path 'samples/*/Dockerfile.nanoserver') produce image-info containing only the rebuilt platforms. Combined with PR dotnet#2030's move of manifest-list creation from the prod repo into the scoped staging repo, the shared-tag manifest list created from that image-info only references the rebuilt subset. The subsequent copyAcrImages step then overwrites the previously complete shared tags in the prod repo with this degraded list. Add an ISharedTagPortingService that runs before manifest-list creation in CreateManifestListCommand. For each image with shared tags that has manifest-declared platforms missing from image-info, it: 1. Looks up the platform's representative tag in the source registry (the manifest's model registry, before --registry-override). 2. Imports the resolved image (by digest, copying referrers) into the scoped staging repo at the platform's declared simple tags. 3. Synthesizes a PlatformData entry in image-info so the existing ManifestListHelper logic and the downstream copyAcrImages step pick the ported platform up naturally. Platforms with no representative tag at the source (genuinely new) are skipped with a warning, preserving today's behavior for net-new shared tags. Fixes dotnet#2107. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Add edge-case tests and syndicated tag handling for shared-tag porting - Add tests for: genuinely-new platforms (404 at source) skipped with warning; no-op when image-info is complete; dry-run skips source registry but still records import intent. - Extend GetDestinationTagsForPort to import ported platforms into syndicated repo paths too, so syndicated manifest lists can resolve their platform refs in staging. - Minor cleanup in AddPortedPlatformToImageInfo (use repo.FullModelName). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Reference PublishRegistry service connection from post-build createManifestList now queries the PublishRegistry to look up previously-published platform tags for porting (issue dotnet#2107). Reference its service connection from the Post_Build job so OIDC tokens are issued for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Refactor SharedTagPortingService to plan/execute separation The porting service is now a pure planner: it identifies missing platforms, resolves their source digests, and returns a SharedTagPortingPlan describing what should happen. It performs no ACR imports and does not mutate image-info. CreateManifestListCommand owns the side effects: it consumes the plan, performs each ACR import via ICopyImageService, and appends a synthesized PlatformData entry to image-info per ported platform via a new AddPortedPlatformToImageInfo helper. This makes the top-level control flow linear and obvious (plan -> imports -> image-info -> manifest lists), and isolates the porting service to a single responsibility (figuring out what needs to move) that can be tested without ICopyImageService. In dry-run the planner now returns an empty plan, so no imports run - simpler and matches what we want operationally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Revert "Reference PublishRegistry service connection from post-build" This reverts commit 734e1e2. Document MCR source registry tradeoff for shared-tag porting Explains why CreateManifestListCommand reads previously-published platform digests from Manifest.Model.Registry (the public MCR mirror) rather than from PublishRegistry: keeping the Post_Build stage's managed identity / service connection scoped to BuildRegistry only avoids expanding its trust boundary, at the cost of potentially porting stale digests during MCR's mirror lag window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Remove unused manifest list test generator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Trim manifest list command comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Clarify dry-run porting test comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Remove redundant syndicated tag cast Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Remove extra interface EOF blank line Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Preserve syndicated tag tuple names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aspire Dashboard images [will be syndicated](dotnet/dotnet-docker#7337) for a period of time. When attempting to publish syndicated aspire dashboard images, the [post-build stage failed](https://dev.azure.com/dnceng/internal/_build/results?buildId=3076999&view=logs&j=d0655b31-4114-5ece-d5b2-aae266c14734&t=9161a68a-f78e-59c2-d387-7e8be1fa9623) with: ``` Failed to execute docker manifest create --amend msdotnetcontainersstaging.azurecr.io/build-staging/3076999/dotnet/nightly/aspire-dashboard:13.6.0 msdotnetcontainersstaging.azurecr.io/build-staging/3076999/dotnet/nightly/aspire-dashboard:13.6.0-amd64 msdotnetcontainersstaging.azurecr.io/build-staging/3076999/dotnet/nightly/aspire-dashboard:13.6.0-arm64v8 no such manifest: msdotnetcontainersstaging.azurecr.io/build-staging/3076999/dotnet/nightly/aspire-dashboard:13.6.0-amd64 ``` In this instance, `dotnet/nightly/aspire-dashboard` is the syndicated version of the image. The manifest list creation task attempts to create the syndicated manifest lists, but since the syndicated platform-specific tags haven't been created yet, it fails. Historically, here is how syndication worked: **Prior to #2030** (aka the last time we had working syndication): - Build stage: - builds images - tags built images with primary platform-specific tags - tags built images with primary multi-platform tags - Post-build stage: - does nothing with tags or syndication - Publish stage: - creates and publishes manifest lists - creates all syndicated tags and manifest lists **After #2030:** - Build stage: - builds images - tags built images with primary platform-specific tags - tags built images with primary multi-platform tags - Post-build stage: - creates manifest lists - creates syndicated manifest lists (this is what is breaking today) - Publish stage: - copies all tags (primary + syndicated) to publish destination **With the changes from this PR:** - Build stage: - builds images - tags built images with **all** platform-specific tags - tags built images with **all** multi-platform tags - Post-build stage: - creates manifest lists - creates syndicated manifest lists (will now succeed) - Publish stage: - copies all tags (primary + syndicated) to publish destination --- You may be wondering, "Why not have the Build/Post-Build stages operate only on *primary* tags, and just create syndicated tags as part of publishing?" This approach is appealing, but the current manifest/syndication model is on a tag-specific basis. This means that platforms may participate selectively in syndication. If an image has platforms `[A, B, C]`, but only platforms `[A, B]` are syndicated with shared syndicated tags, then that necessitates a new manifest list `[A, B]` be created. Thus, we must create it during Build/Post-Build and not during Publishing (so that it can get signed, etc.). I would like to simplify the syndication model to **image-level** instead of **tag-level**, but [I will leave that as follow-up work](#2240). This PR is required to unblock syndication of Aspire Dashboard images. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ef3e1f6f-f17d-4cc9-8466-aa0b5e21dbdf
This PR makes the following changes:
This is part 1/2 of #2002
The move-manifest-list-pipelines branch is companion to this one, and contains the pipeline changes necessary for these changes.
I validated these changes internally using the imagebuilder unofficial pipeline (build#2928726), and confirmed that manifest lists were signed (with test signatures) in the staging (test) ACR and that the signatures were copied across to the prod (test) ACR.