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 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.

@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.

/// <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

@captainsafia captainsafia force-pushed the copilot/add-container-image-push-step branch 2 times, most recently 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
@captainsafia captainsafia force-pushed the copilot/add-container-image-push-step branch from 19a0685 to fb319be Compare December 8, 2025 19:34
@captainsafia captainsafia force-pushed the copilot/add-container-image-push-step branch from fb319be to 19f38c8 Compare December 8, 2025 19:49
@captainsafia
Copy link
Member

OK, I tested the changes in 9c45f96 against the following scenarios.

  1. Clean aspire do push with a single registry
  2. Clean aspire deploy with a single ACA compute environment
  3. aspire do push on deployment state from Step 2
  4. Clean aspire do push with a single ACA compute environment
  5. Clean aspire do push with multiple registries
  6. Clean aspire deploy with multiple compute environments

@davidfowl
Copy link
Member

21:58:04 (push-prereq) → Starting push-prereq...
21:58:04 (push-prereq) ✗ [ERR] Step 'push-prereq' failed.
21:58:04 (push-prereq) ✗ Step 'push-prereq' failed: Resource 'server' requires image push but no container registry is available. Please add a container
registry using 'builder.AddContainerRegistry(...)' or specify one with '.WithContainerRegistry(registryBuilder)'.
21:58:04 (pipeline-execution) ✗ [ERR] Step 'push-prereq' failed: Resource 'server' requires image push but no container registry is available. Please add a
container registry using 'builder.AddContainerRegistry(...)' or specify one with '.WithContainerRegistry(registryBuilder)'.
21:58:04 (pipeline-execution) ✗ Failed

@davidfowl davidfowl merged commit 77a1176 into main Dec 9, 2025
562 of 565 checks passed
@davidfowl davidfowl deleted the copilot/add-container-image-push-step branch December 9, 2025 06:09
@davidfowl
Copy link
Member

This fails without .WithContainerRegistry(acr);

var builder = DistributedApplication.CreateBuilder(args);

var acr = builder.AddAzureContainerRegistry("acr");

var cache = builder.AddRedis("cache");

#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

var server = builder.AddProject<Projects.FeBackend_Server>("server")
    .WithReference(cache)
    .WaitFor(cache)
    .WithHttpHealthCheck("/health");

var webfrontend = builder.AddViteApp("webfrontend", "../frontend")
    .WithReference(server)
    .WaitFor(server)
    .WithExternalHttpEndpoints();

server.PublishWithContainerFiles(webfrontend, "wwwroot");

builder.Build().Run();

@dotnet-policy-service dotnet-policy-service bot added this to the 13.1 milestone Dec 9, 2025
@captainsafia
Copy link
Member

@davidfowl Yeah, we're currently only setting the RegistryTargetAnnotation when you call AddContainerRegistry. Should be easy to fix though...

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