Skip to content

Move manifest list creation to Post_Build, copy via ACR import - #2030

Merged
lbussell merged 15 commits into
dotnet:mainfrom
lbussell:move-manifest-lists-src
Mar 24, 2026
Merged

lbussell merged 15 commits into
dotnet:mainfrom
lbussell:move-manifest-lists-src

Conversation

@lbussell

@lbussell lbussell commented Mar 17, 2026 •

Copy link
Copy Markdown
Member

This PR makes the following changes:

  • Add CreateManifestListCommand that is inteded to run in Post_Build after mergeImageInfo to create Docker manifest lists, push them to the staging registry, and record their digests in image-info.json. This enables manifest list signing by ensuring digests are present before the Sign stage.
  • Extend CopyAcrImagesCommand to copy manifest list shared tags alongside platform tags via ACR import.
  • Extract ManifestListHelper from PublishManifestCommand, essentially unchanged. It determines which manifest lists to create based on which platforms were actually built, preventing manifest lists from referencing unbuilt images.
  • Remove PublishManifestCommand - no longer needed. The Publish stage will now only copy images/manifest lists.

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.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into image-info.json.
  • Extend CopyAcrImagesCommand to also import/copy manifest list shared tags (including syndicated tags).
  • Extract manifest list selection logic into ManifestListHelper and remove PublishManifestCommand (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.

Comment thread src/ImageBuilder/ManifestListHelper.cs Resolved
lbussell and others added 4 commits March 18, 2026 10:07
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to image-info.json.
  • Introduce ManifestListHelper (extracted from the old publish command) to compute which manifest lists to create based on actually-built platforms.
  • Extend CopyAcrImagesCommand to copy manifest list shared tags (including syndicated variants) alongside platform tags; remove PublishManifestCommand and 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.

Comment thread src/ImageBuilder/ManifestListHelper.cs Outdated
Comment on lines +134 to +149
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}'.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines 45 to 50
@@ -49,11 +49,11 @@ public override async Task ExecuteAsync()
return;
}
@lbussell
lbussell marked this pull request as ready for review March 19, 2026 16:23
@lbussell
lbussell requested a review from a team as a code owner March 19, 2026 16:23
Comment thread src/ImageBuilder/ManifestListHelper.cs Outdated
Comment on lines +134 to +149
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}'.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

lbussell and others added 3 commits March 20, 2026 14:44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lbussell and others added 3 commits March 23, 2026 14:34
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
@lbussell
lbussell enabled auto-merge (squash) March 24, 2026 16:22
@lbussell
lbussell merged commit f6e7c49 into dotnet:main Mar 24, 2026
12 checks passed
@lbussell
lbussell deleted the move-manifest-lists-src branch March 24, 2026 17:29
lbussell added a commit that referenced this pull request Mar 30, 2026
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>
lbussell added a commit to lbussell/docker-tools that referenced this pull request May 20, 2026
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>
lbussell added a commit to lbussell/docker-tools that referenced this pull request May 26, 2026
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>
lbussell added a commit that referenced this pull request Sep 17, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants