Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 3, 2025

Description

Adds pipeline infrastructure for pushing container images to ContainerRegistryResource instances. The registry resource now directly creates push steps for all resources that reference it.

Key changes:

  • Add WellKnownPipelineSteps.Push and WellKnownPipelineSteps.PushPrereq constants
  • ContainerRegistryResource registers PipelineStepAnnotation to create per-resource push steps
  • ContainerRegistryResource registers PipelineConfigurationAnnotation to wire Build → Push dependencies
  • Push unconditionally depends on PushPrereq to ensure annotations are set up before any push operations
  • PushPrereq step automatically adds ContainerRegistryReferenceAnnotation to resources that don't have one when a single registry exists (required for correct remote image tag computation during push)
  • GetResourcesToPush only includes resources with ContainerRegistryReferenceAnnotation matching the target registry
  • Clear error when multiple registries exist without explicit .WithContainerRegistry() annotation

Example usage:

// Single registry - automatic selection, no annotation needed
var dockerHub = builder.AddContainerRegistry("docker-hub", "docker.io", "myusername");
var api = builder.AddProject<Projects.MyApi>("api");
// api will automatically push to docker-hub

// Multiple registries - explicit annotation required
var dockerHub = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser");
var ghcr = builder.AddContainerRegistry("ghcr", "ghcr.io", "myorg");
var api = builder.AddProject<Projects.MyApi>("api")
    .WithContainerRegistry(dockerHub);  // Required when multiple registries exist

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?
Original prompt

Container Registry Push Pipeline Step Plan

Overview

This plan proposes adding support for pushing container images to ContainerRegistryResource instances through a well-known pipeline step. The ContainerRegistryResource will directly register pipeline steps for all resources that reference it.

Motivation

Currently, the ContainerRegistryResource serves as a reference for container registries but doesn't participate in the deployment pipeline. To enable deployment scenarios where containers need to be pushed to external registries (Docker Hub, GHCR, private registries), we need to:

  1. Add pipeline step infrastructure to push images to the registry
  2. Create a well-known pipeline step constant for image push operations
  3. Wire up dependencies so push happens after build and before deploy

The reference implementation from https://github.com/captainsafia/aspire-image-push demonstrates this need with a custom pipeline step that pushes images to a parameterized container registry.

Goals

  1. Add WellKnownPipelineSteps.Push as a well-known pipeline step
  2. Enable ContainerRegistryResource to directly create push steps for all resources that require image push
  3. Provide automatic registry selection when only one registry exists (no annotation required)
  4. Enforce explicit annotation requirement when multiple registries exist with clear error messages
  5. Ensure proper dependency ordering: Build → Push → Deploy
  6. Follow the same error handling pattern as DeploymentTargetAnnotation for consistency

API Design

1. Add Well-Known Pipeline Step

Add a new constant to WellKnownPipelineSteps.cs:

public static class WellKnownPipelineSteps
{
    // ... existing steps ...

    /// <summary>
    /// The meta-step that coordinates all push operations.
    /// All push steps should be required by this step.
    /// </summary>
    public const string Push = "push";

    /// <summary>
    /// The prerequisite step that runs before any push operations.
    /// </summary>
    public const string PushPrereq = "push-prereq";
}

2. ContainerRegistryResource Pipeline Integration

Update ContainerRegistryResource to directly create push steps for all resources that reference it:

public class ContainerRegistryResource : Resource, IContainerRegistry
{
    public ContainerRegistryResource(string name, ReferenceExpression endpoint, ReferenceExpression? repository = null)
        : base(name)
    {
        ArgumentNullException.ThrowIfNull(endpoint);

        _registryName = ReferenceExpression.Create($"{name}");
        _endpoint = endpoint;
        _repository = repository;

        Annotations.Add(new PipelineStepAnnotation(factoryContext =>
        {
            var model = factoryContext.PipelineContext.Model;
            var steps = new List<PipelineStep>();

            var allRegistries = model.Resources.OfType<IContainerRegistry>().ToArray();
            var hasMultipleRegistries = allRegistries.Length > 1;

            foreach (var resource in model.Resources)
            {
                if (!resource.RequiresImageBuildAndPush())
                {
                    continue;
                }

                var targetRegistry = GetTargetRegistryForResource(resource, this, allRegistries, hasMultipleRegistries);
                if (targetRegistry is null || !ReferenceEquals(targetRegistry, this))
                {
                    continue;
                }

                var pushStep = new PipelineStep
                {
                    Name = $"push-{resource.Name}",
                    Action = async ctx =>
                    {
                        var containerImageManager = ctx.Services.GetRequiredService<IResourceContainerImageManager>();
                        await containerImageManager.PushImageAsync(resource, ctx.CancellationToken).ConfigureAwait(false);
                    },
                    Tags = [WellKnownPipelineTags.PushContainerImage],
                    RequiredBySteps = [WellKnownPipelineSteps.Push],
                    Resource = resource
                };

                steps.Add(pushStep);
            }

            return steps;
        }));

        Annotations.Add(new PipelineConfigurationAnnotation(context =>
        {
            foreach (var resource in context.Model.Resources)
            {
                if (!resource.RequiresImageBuildAndPush())
                {
                    continue;
                }

                var allRegistries = context.Model.Resources.OfType<IContainerRegistry>().ToArray();
                var hasMultipleRegistries = allRegistries.Length > 1;

                var targetRegistry = GetTargetRegistryForResource(resource, this, allRegistries, hasMultipleRegistries);
                if (targetRegistry is null || !ReferenceEquals(targetRegistry, this))
                {
                    continue;
                }

                var buildSteps = context.GetSteps(resour...

</details>



<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/dotnet/aspire/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add support for pushing container images to registry Add Push and PushPrereq pipeline steps for ContainerRegistryResource Dec 3, 2025
Copilot finished work on behalf of captainsafia December 3, 2025 01:29
Copilot AI requested a review from captainsafia December 3, 2025 01:29
@github-actions
Copy link
Contributor

github-actions bot commented Dec 3, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 13306

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 13306"

@captainsafia
Copy link
Member

@copilot Bleh, we need to have the ContainerRegistryReferenceAnnotation on all resources so that we can correctly compute the remote image tag during the push phase. Add a PushPrereq step that will add the ContainerRegistryReferenceAnnotation to resources that don't already have it when a single container registry exists in the app model. When multiple registries exist, throw an exception that the user must call WithContainerRegistry explicitly.

Copy link
Contributor Author

Copilot AI commented Dec 3, 2025

@copilot Bleh, we need to have the ContainerRegistryReferenceAnnotation on all resources so that we can correctly compute the remote image tag during the push phase. Add a PushPrereq step that will ad...

Done in ed1e587. The PushPrereq step now adds ContainerRegistryReferenceAnnotation to resources that don't have it when a single container registry exists. When multiple registries exist, it throws an InvalidOperationException requiring the user to call WithContainerRegistry explicitly.

Copilot finished work on behalf of captainsafia December 3, 2025 02:56
}

// When multiple registries exist, require explicit WithContainerRegistry call
if (allRegistries.Length > 1)
Copy link
Member

Choose a reason for hiding this comment

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

Pushing to all doesn't seem like bad behavior though I understand its different from compute environments (at the moment).

Copy link
Member

Choose a reason for hiding this comment

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

Yep, I modeled this based on the patterns we have for compute environments. IMO, I think pushing towards a 1:1 relationship between registries and images is probably better for push (and eventually pull). Pushing to multiple registries increases the overhead associated with a single push call. We'd have to authenticate/tag/push N times instead of 1. I also think it's the kind of thing where we'd want the user to be more intentional about where images gets pushed given the security implications.

@davidfowl
Copy link
Member

Does this break when you have 2 compute environments now?

@captainsafia
Copy link
Member

Does this break when you have 2 compute environments now?

Probably, lol. TBH, I haven't gotten that far. I'm just trying to get the end-to-end over in aspire-image-push working before layering on things.

My intuition tells me that as a fall out of this PR we'll need to model the Azure Container Registry that is implicitly in the compute environments as an explicit thing and always model the push steps on a resource that implements IContainerRegistry instead of within the compute environments.

@captainsafia
Copy link
Member

@copilot We need to make sure that the PushPrereq runs unconditionally on compute resources that need a push. Currently, GetResourcesToPush introduces a gap in the implementation because we will never wire up the PipelineStepConfiguration for the PushPrereq if there isn't already a ContainerRegistryReferenceAnnotation set (which is done by the PushPrereq).

Copy link
Contributor Author

Copilot AI commented Dec 3, 2025

@copilot We need to make sure that the PushPrereq runs unconditionally on compute resources that need a push. Currently, GetResourcesToPush introduces a gap in the implementation because we will never...

Fixed in 335a826. Refactored GetResourcesToPush to include resources without ContainerRegistryReferenceAnnotation when a single registry exists, ensuring PipelineConfigurationAnnotation correctly wires up the PushPrereq dependency even before the annotation is added at runtime.

Copilot finished work on behalf of captainsafia December 3, 2025 05:18
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the container registry resource.</param>
/// <returns>The created <see cref="AzureContainerRegistryResource"/>.</returns>
public static AzureContainerRegistryResource CreateDefaultContainerRegistry(IDistributedApplicationBuilder builder, string name)
Copy link
Member

Choose a reason for hiding this comment

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

CreateDefaultAzureContainerRegistry

Copilot AI and others added 5 commits December 5, 2025 13:11
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
…meters

Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
…e registry

Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
@captainsafia captainsafia force-pushed the copilot/add-container-image-push-step branch from e9c206e to b56d325 Compare December 5, 2025 21:16
@captainsafia captainsafia force-pushed the copilot/add-container-image-push-step branch from 9485950 to 6cdddc1 Compare December 5, 2025 22:47
@captainsafia captainsafia marked this pull request as ready for review December 5, 2025 23:10
Copilot AI review requested due to automatic review settings December 5, 2025 23:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds container image push pipeline infrastructure to ContainerRegistryResource instances. The key changes include new well-known pipeline steps (Push and PushPrereq), refactoring of push logic from environment resources to individual container resources, and automatic registry selection logic.

Key Changes

  • Added WellKnownPipelineSteps.Push and PushPrereq constants for pipeline coordination
  • Refactored push step creation from environment resources to individual container/project resources
  • Implemented automatic registry annotation in PushPrereq step with clear error messages for multiple registries
  • Created PipelineStepHelpers.PushImageToRegistryAsync for shared push logic
  • Updated Azure environments to create default ACR resources
  • Extensive snapshot test updates reflecting new pipeline structure

Reviewed changes

Copilot reviewed 65 out of 65 changed files in this pull request and generated no comments.

Show a summary per file
File Description
WellKnownPipelineSteps.cs Added Push and PushPrereq constants
PipelineStepHelpers.cs New helper for push operations
DistributedApplicationPipeline.cs Added PushPrereq step with automatic registry annotation logic
ContainerResourceBuilderExtensions.cs Renamed method to include push, added push step generation
ProjectResource.cs Added push step generation alongside build steps
AzureContainerRegistryResource.cs Added login step and pipeline configuration
AzureContainerRegistryHelpers.cs Refactored helper methods
Azure environment resources Removed push/login logic, delegated to registry resources
Test files Updated assertions for new step counts and dependencies
Snapshot files Updated Bicep generation for separate ACR resources

Comment on lines +15 to +17
<InternalsVisibleTo Include="Aspire.Hosting.Azure.AppContainers" />
<InternalsVisibleTo Include="Aspire.Hosting.Azure.AppService" />
<InternalsVisibleTo Include="Aspire.Hosting.Azure.Tests" />
Copy link
Member

Choose a reason for hiding this comment

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

File an issue for this one.

Comment on lines +94 to +101
foreach (var pushStep in pushSteps)
{
foreach (var buildStep in projectBuildSteps)
{
pushStep.DependsOn(buildStep);
}
pushStep.DependsOn(WellKnownPipelineSteps.PushPrereq);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
foreach (var pushStep in pushSteps)
{
foreach (var buildStep in projectBuildSteps)
{
pushStep.DependsOn(buildStep);
}
pushStep.DependsOn(WellKnownPipelineSteps.PushPrereq);
}
pushSteps.DependsOn(projectBuildSteps);
pushSteps.DependsOn(WellKnownPipelineSteps.PushPrereq);


return builder.WithAnnotation(new PipelineConfigurationAnnotation(context =>
{
var buildSteps = context.GetSteps(builder.Resource, WellKnownPipelineTags.BuildCompute);
Copy link
Member

Choose a reason for hiding this comment

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

See above comment for simplification of this.

// Single registry - automatically add the annotation
if (allRegistries.Length == 1)
{
resource.Annotations.Add(new ContainerRegistryReferenceAnnotation(allRegistries[0]));
Copy link
Member

Choose a reason for hiding this comment

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

A little bit worrying that we do this so late. The annotation list is not threadsafe.

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