From cdc1ce0dc888b46fcd0ffbb74210052a44a02f47 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 6 Mar 2026 23:39:27 -0500 Subject: [PATCH 1/7] feat: add localFallback option for Docker remote build When remoteBuild is true and localFallback is true in azure.yaml, azd automatically falls back to a local Docker build if the remote ACR build fails. Displays a WARNING message when fallback triggers. This helps users on subscriptions that don't support ACR Tasks (e.g., free trial) by gracefully degrading to local Docker builds instead of failing outright. Changes: - Add localFallback field to DockerProjectOptions struct - Add fallback logic in ContainerHelper.Publish() - Update proto definition and generated code - Update azure.yaml JSON schema (service-level and docker-level) - Add mapper registry mappings - Add unit test for fallback behavior Fixes #4618 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/grpc/proto/models.proto | 1 + cli/azd/pkg/azdext/models.pb.go | 8 ++ cli/azd/pkg/project/container_helper.go | 8 ++ cli/azd/pkg/project/container_helper_test.go | 73 +++++++++++++++++++ .../pkg/project/framework_service_docker.go | 19 ++--- cli/azd/pkg/project/mapper_registry.go | 53 +++++++------- schemas/v1.0/azure.yaml.json | 10 +++ 7 files changed, 138 insertions(+), 34 deletions(-) diff --git a/cli/azd/grpc/proto/models.proto b/cli/azd/grpc/proto/models.proto index f3c292ad983..eb1465f21b5 100644 --- a/cli/azd/grpc/proto/models.proto +++ b/cli/azd/grpc/proto/models.proto @@ -116,6 +116,7 @@ message DockerProjectOptions { string image = 6; string tag = 7; bool remote_build = 8; + bool local_fallback = 10; repeated string build_args = 9; } diff --git a/cli/azd/pkg/azdext/models.pb.go b/cli/azd/pkg/azdext/models.pb.go index 731e5fd2227..ee60aa0eea8 100644 --- a/cli/azd/pkg/azdext/models.pb.go +++ b/cli/azd/pkg/azdext/models.pb.go @@ -1063,6 +1063,7 @@ type DockerProjectOptions struct { Image string `protobuf:"bytes,6,opt,name=image,proto3" json:"image,omitempty"` Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` RemoteBuild bool `protobuf:"varint,8,opt,name=remote_build,json=remoteBuild,proto3" json:"remote_build,omitempty"` + LocalFallback bool `protobuf:"varint,10,opt,name=local_fallback,json=localFallback,proto3" json:"local_fallback,omitempty"` BuildArgs []string `protobuf:"bytes,9,rep,name=build_args,json=buildArgs,proto3" json:"build_args,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1154,6 +1155,13 @@ func (x *DockerProjectOptions) GetRemoteBuild() bool { return false } +func (x *DockerProjectOptions) GetLocalFallback() bool { + if x != nil { + return x.LocalFallback + } + return false +} + func (x *DockerProjectOptions) GetBuildArgs() []string { if x != nil { return x.BuildArgs diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index 2923a0cb4d7..aec3dbd76ff 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -32,6 +32,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/azure/azure-dev/cli/azd/pkg/output/ux" "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" @@ -599,6 +600,13 @@ func (ch *ContainerHelper) Publish( if serviceConfig.Docker.RemoteBuild { remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, env, progress, imageOverride) + if err != nil && serviceConfig.Docker.LocalFallback { + ch.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: "Remote build failed, falling back to local Docker build.", + HidePrefix: false, + }) + remoteImage, err = ch.publishLocalImage(ctx, serviceConfig, serviceContext, env, progress, imageOverride) + } } else if useDotnetPublishForDockerBuild(serviceConfig) { remoteImage, err = ch.runDotnetPublish(ctx, serviceConfig, targetResource, env, progress) } else { diff --git a/cli/azd/pkg/project/container_helper_test.go b/cli/azd/pkg/project/container_helper_test.go index 435acb51c47..9ff8d54f692 100644 --- a/cli/azd/pkg/project/container_helper_test.go +++ b/cli/azd/pkg/project/container_helper_test.go @@ -1271,3 +1271,76 @@ func Test_ContainerHelper_Publish(t *testing.T) { }) } } + +func Test_ContainerHelper_Publish_RemoteBuildLocalFallback(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + mockResults := setupDockerMocks(mockContext) + env := environment.NewWithValues("dev", map[string]string{}) + dockerCli := docker.NewCli(mockContext.CommandRunner) + dotnetCli := dotnet.NewCli(mockContext.CommandRunner) + + mockContainerRegistryService := &mockContainerRegistryService{} + setupContainerRegistryMocks(mockContext, &mockContainerRegistryService.Mock) + + containerHelper := NewContainerHelper( + clock.NewMock(), + mockContainerRegistryService, + nil, + mockContext.CommandRunner, + dockerCli, + dotnetCli, + mockContext.Console, + cloud.AzurePublic(), + ) + + serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript) + serviceConfig.Docker.Registry = osutil.NewExpandableString("contoso.azurecr.io") + serviceConfig.Docker.RemoteBuild = true + serviceConfig.Docker.LocalFallback = true + serviceConfig.Docker.Platform = "linux/arm64" + + dockerArtifact := &Artifact{ + Kind: ArtifactKindContainer, + Location: "my-project/my-service:azd-deploy-0", + LocationKind: LocationKindLocal, + Metadata: map[string]string{ + "imageHash": "IMAGE_ID", + "sourceImage": "", + "targetImage": "my-project/my-service:azd-deploy-0", + }, + } + + serviceContext := &ServiceContext{ + Package: ArtifactCollection{dockerArtifact}, + } + + targetResource := environment.NewTargetResource( + "SUBSCRIPTION_ID", + "RESOURCE_GROUP", + "CONTAINER_APP", + "Microsoft.App/containerApps", + ) + + publishResult, err := logProgress( + t, func(progress *async.Progress[ServiceProgress]) (*ServicePublishResult, error) { + return containerHelper.Publish( + *mockContext.Context, serviceConfig, serviceContext, targetResource, env, progress, &PublishOptions{}) + }, + ) + + require.NoError(t, err) + require.Len(t, publishResult.Artifacts, 1) + require.Equal(t, "contoso.azurecr.io/my-project/my-service:azd-deploy-0", publishResult.Artifacts[0].Metadata["remoteImage"]) + + _, dockerPushCalled := mockResults["docker-push"] + require.True(t, dockerPushCalled) + + warningFound := false + for _, line := range mockContext.Console.Output() { + if strings.Contains(line, "Remote build failed, falling back to local Docker build.") { + warningFound = true + break + } + } + require.True(t, warningFound) +} diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index a1317535ca4..58309fabfd2 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -20,15 +20,16 @@ import ( ) type DockerProjectOptions struct { - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Context string `yaml:"context,omitempty" json:"context,omitempty"` - Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` - Target string `yaml:"target,omitempty" json:"target,omitempty"` - Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` - Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` - Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` - RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` - BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Context string `yaml:"context,omitempty" json:"context,omitempty"` + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` + Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` + Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` + RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` + LocalFallback bool `yaml:"localFallback,omitempty" json:"localFallback,omitempty"` + BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` // not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially. // Aspire would pass the secret keys, which are env vars that azd will set just to run docker build. BuildSecrets []string `yaml:"-" json:"-"` diff --git a/cli/azd/pkg/project/mapper_registry.go b/cli/azd/pkg/project/mapper_registry.go index 11d6fcf21af..ced0579349a 100644 --- a/cli/azd/pkg/project/mapper_registry.go +++ b/cli/azd/pkg/project/mapper_registry.go @@ -191,15 +191,16 @@ func registerProjectMappings() { } return &azdext.DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: registry, - Image: image, - Tag: tag, - RemoteBuild: src.RemoteBuild, - BuildArgs: buildArgs, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: registry, + Image: image, + Tag: tag, + RemoteBuild: src.RemoteBuild, + LocalFallback: src.LocalFallback, + BuildArgs: buildArgs, }, nil }) @@ -405,14 +406,15 @@ func registerProjectMappings() { } result := DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: osutil.NewExpandableString(src.Registry), - Image: osutil.NewExpandableString(src.Image), - Tag: osutil.NewExpandableString(src.Tag), - RemoteBuild: src.RemoteBuild, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: osutil.NewExpandableString(src.Registry), + Image: osutil.NewExpandableString(src.Image), + Tag: osutil.NewExpandableString(src.Tag), + RemoteBuild: src.RemoteBuild, + LocalFallback: src.LocalFallback, } if len(src.BuildArgs) > 0 { @@ -432,14 +434,15 @@ func registerProjectMappings() { } result := &DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: osutil.NewExpandableString(src.Registry), - Image: osutil.NewExpandableString(src.Image), - Tag: osutil.NewExpandableString(src.Tag), - RemoteBuild: src.RemoteBuild, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: osutil.NewExpandableString(src.Registry), + Image: osutil.NewExpandableString(src.Image), + Tag: osutil.NewExpandableString(src.Tag), + RemoteBuild: src.RemoteBuild, + LocalFallback: src.LocalFallback, } if len(src.BuildArgs) > 0 { diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 92cd9a2f864..7db43503b8a 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -139,6 +139,11 @@ "title": "Optional. Whether to use remote build for function app deployment", "description": "When set to true, the deployment package will be built remotely using Oryx. When set to false, the package is deployed as-is. If omitted, defaults to true for JavaScript, TypeScript, and Python function apps." }, + "localFallback": { + "type": "boolean", + "title": "Optional. Whether to fall back to a local Docker build when a remote build fails", + "description": "When set to true and remoteBuild is also true for a container-based service, azd will automatically attempt a local Docker build if the remote Azure Container Registry build fails. This is useful for subscriptions that do not support ACR Tasks, such as free trial subscriptions." + }, "docker": { "$ref": "#/definitions/docker" }, @@ -865,6 +870,11 @@ "type": "boolean", "title": "Optional. Whether to build the image remotely", "description": "If set to true, the image will be built remotely using the Azure Container Registry remote build feature. If set to false, the image will be built locally using Docker." + }, + "localFallback": { + "type": "boolean", + "title": "Optional. Whether to fall back to local Docker build on remote build failure", + "description": "When set to true and remoteBuild is also true, azd falls back to building the container image locally if the remote Azure Container Registry build fails. This is helpful for subscriptions that do not support ACR Tasks, such as free trial subscriptions." } } }, From cbe553531648dde6327790d0d61ddb3a7204444c Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 6 Mar 2026 23:54:05 -0500 Subject: [PATCH 2/7] address PR review feedback - Include original error in fallback warning message - Fix proto field ordering (local_fallback after build_args) - Align struct tag spacing for LocalFallback - Remove service-level localFallback from schema (only docker-level) - Fix lint: break long test line under 125 chars Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/grpc/proto/models.proto | 2 +- cli/azd/pkg/project/container_helper.go | 8 +++++--- cli/azd/pkg/project/container_helper_test.go | 7 +++++-- schemas/v1.0/azure.yaml.json | 5 ----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/azd/grpc/proto/models.proto b/cli/azd/grpc/proto/models.proto index eb1465f21b5..ef9ca92e1f2 100644 --- a/cli/azd/grpc/proto/models.proto +++ b/cli/azd/grpc/proto/models.proto @@ -116,8 +116,8 @@ message DockerProjectOptions { string image = 6; string tag = 7; bool remote_build = 8; - bool local_fallback = 10; repeated string build_args = 9; + bool local_fallback = 10; } // ServiceContext defines the shared pipeline state across all phases of the service lifecycle diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index aec3dbd76ff..0ef48d512fa 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -602,10 +602,12 @@ func (ch *ContainerHelper) Publish( remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, env, progress, imageOverride) if err != nil && serviceConfig.Docker.LocalFallback { ch.console.MessageUxItem(ctx, &ux.WarningMessage{ - Description: "Remote build failed, falling back to local Docker build.", - HidePrefix: false, + Description: fmt.Sprintf( + "Remote build failed: %s\nFalling back to local Docker build.", err), + HidePrefix: false, }) - remoteImage, err = ch.publishLocalImage(ctx, serviceConfig, serviceContext, env, progress, imageOverride) + remoteImage, err = ch.publishLocalImage( + ctx, serviceConfig, serviceContext, env, progress, imageOverride) } } else if useDotnetPublishForDockerBuild(serviceConfig) { remoteImage, err = ch.runDotnetPublish(ctx, serviceConfig, targetResource, env, progress) diff --git a/cli/azd/pkg/project/container_helper_test.go b/cli/azd/pkg/project/container_helper_test.go index 9ff8d54f692..9f25c619422 100644 --- a/cli/azd/pkg/project/container_helper_test.go +++ b/cli/azd/pkg/project/container_helper_test.go @@ -1330,14 +1330,17 @@ func Test_ContainerHelper_Publish_RemoteBuildLocalFallback(t *testing.T) { require.NoError(t, err) require.Len(t, publishResult.Artifacts, 1) - require.Equal(t, "contoso.azurecr.io/my-project/my-service:azd-deploy-0", publishResult.Artifacts[0].Metadata["remoteImage"]) + expectedImage := "contoso.azurecr.io/my-project/my-service:azd-deploy-0" + require.Equal(t, expectedImage, + publishResult.Artifacts[0].Metadata["remoteImage"]) _, dockerPushCalled := mockResults["docker-push"] require.True(t, dockerPushCalled) warningFound := false for _, line := range mockContext.Console.Output() { - if strings.Contains(line, "Remote build failed, falling back to local Docker build.") { + if strings.Contains(line, "Remote build failed:") && + strings.Contains(line, "Falling back to local Docker build.") { warningFound = true break } diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 7db43503b8a..35603dcfa81 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -139,11 +139,6 @@ "title": "Optional. Whether to use remote build for function app deployment", "description": "When set to true, the deployment package will be built remotely using Oryx. When set to false, the package is deployed as-is. If omitted, defaults to true for JavaScript, TypeScript, and Python function apps." }, - "localFallback": { - "type": "boolean", - "title": "Optional. Whether to fall back to a local Docker build when a remote build fails", - "description": "When set to true and remoteBuild is also true for a container-based service, azd will automatically attempt a local Docker build if the remote Azure Container Registry build fails. This is useful for subscriptions that do not support ACR Tasks, such as free trial subscriptions." - }, "docker": { "$ref": "#/definitions/docker" }, From 5afcfd3def8adc0e67980bd8a7358cf2ac12d44d Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Sat, 7 Mar 2026 09:04:35 -0500 Subject: [PATCH 3/7] ci: retrigger pipeline checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 0f77cdf9db38dae6fa6a24c845d3684437c28335 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Sun, 8 Mar 2026 12:00:40 -0400 Subject: [PATCH 4/7] ci: retrigger Windows build check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From dacd2bf225622e85af5723fb7b35a2eab9ddb3eb Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 11 Mar 2026 10:32:29 -0700 Subject: [PATCH 5/7] refactor: make local fallback the default when remoteBuild fails Remove the localFallback field from DockerProjectOptions and make fallback-to-local the default behavior when remoteBuild is true and the remote build fails. Before attempting the local build, azd now checks if Docker or Podman is installed and running via CheckInstalled, providing a clear error if neither is available. Changes: - Remove LocalFallback from DockerProjectOptions struct, proto, schema - Always fall back to local build on remote build failure - Validate Docker/Podman availability before local fallback - Reserve proto field number 10 to prevent reuse - Update remoteBuild schema description to document fallback behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/grpc/proto/models.proto | 3 +- cli/azd/pkg/azdext/models.pb.go | 8 ---- cli/azd/pkg/project/container_helper.go | 9 ++++- cli/azd/pkg/project/container_helper_test.go | 22 ++++++++++- .../pkg/project/framework_service_docker.go | 11 +++--- cli/azd/pkg/project/mapper_registry.go | 39 +++++++++---------- schemas/v1.0/azure.yaml.json | 7 +--- 7 files changed, 55 insertions(+), 44 deletions(-) diff --git a/cli/azd/grpc/proto/models.proto b/cli/azd/grpc/proto/models.proto index ef9ca92e1f2..fe78339d488 100644 --- a/cli/azd/grpc/proto/models.proto +++ b/cli/azd/grpc/proto/models.proto @@ -117,7 +117,8 @@ message DockerProjectOptions { string tag = 7; bool remote_build = 8; repeated string build_args = 9; - bool local_fallback = 10; + reserved 10; + reserved "local_fallback"; } // ServiceContext defines the shared pipeline state across all phases of the service lifecycle diff --git a/cli/azd/pkg/azdext/models.pb.go b/cli/azd/pkg/azdext/models.pb.go index ee60aa0eea8..731e5fd2227 100644 --- a/cli/azd/pkg/azdext/models.pb.go +++ b/cli/azd/pkg/azdext/models.pb.go @@ -1063,7 +1063,6 @@ type DockerProjectOptions struct { Image string `protobuf:"bytes,6,opt,name=image,proto3" json:"image,omitempty"` Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` RemoteBuild bool `protobuf:"varint,8,opt,name=remote_build,json=remoteBuild,proto3" json:"remote_build,omitempty"` - LocalFallback bool `protobuf:"varint,10,opt,name=local_fallback,json=localFallback,proto3" json:"local_fallback,omitempty"` BuildArgs []string `protobuf:"bytes,9,rep,name=build_args,json=buildArgs,proto3" json:"build_args,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1155,13 +1154,6 @@ func (x *DockerProjectOptions) GetRemoteBuild() bool { return false } -func (x *DockerProjectOptions) GetLocalFallback() bool { - if x != nil { - return x.LocalFallback - } - return false -} - func (x *DockerProjectOptions) GetBuildArgs() []string { if x != nil { return x.BuildArgs diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index 0ef48d512fa..2b05ee688b8 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -600,7 +600,14 @@ func (ch *ContainerHelper) Publish( if serviceConfig.Docker.RemoteBuild { remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, env, progress, imageOverride) - if err != nil && serviceConfig.Docker.LocalFallback { + if err != nil { + // Check if a local container runtime (Docker/Podman) is available before falling back + if dockerErr := ch.docker.CheckInstalled(ctx); dockerErr != nil { + return nil, fmt.Errorf( + "remote build failed: %w\n\nLocal fallback unavailable: %s", + err, dockerErr) + } + ch.console.MessageUxItem(ctx, &ux.WarningMessage{ Description: fmt.Sprintf( "Remote build failed: %s\nFalling back to local Docker build.", err), diff --git a/cli/azd/pkg/project/container_helper_test.go b/cli/azd/pkg/project/container_helper_test.go index 9f25c619422..bac6366aea3 100644 --- a/cli/azd/pkg/project/container_helper_test.go +++ b/cli/azd/pkg/project/container_helper_test.go @@ -1279,6 +1279,27 @@ func Test_ContainerHelper_Publish_RemoteBuildLocalFallback(t *testing.T) { dockerCli := docker.NewCli(mockContext.CommandRunner) dotnetCli := dotnet.NewCli(mockContext.CommandRunner) + // Mock Docker availability checks for local fallback + mockContext.CommandRunner.MockToolInPath("docker", nil) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker --version") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.RunResult{ + Stdout: "Docker version 20.10.17, build 100c701", + ExitCode: 0, + }, nil + }) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker ps") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.RunResult{ + Stdout: "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES", + ExitCode: 0, + }, nil + }) + mockContainerRegistryService := &mockContainerRegistryService{} setupContainerRegistryMocks(mockContext, &mockContainerRegistryService.Mock) @@ -1296,7 +1317,6 @@ func Test_ContainerHelper_Publish_RemoteBuildLocalFallback(t *testing.T) { serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript) serviceConfig.Docker.Registry = osutil.NewExpandableString("contoso.azurecr.io") serviceConfig.Docker.RemoteBuild = true - serviceConfig.Docker.LocalFallback = true serviceConfig.Docker.Platform = "linux/arm64" dockerArtifact := &Artifact{ diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index 58309fabfd2..c26423f2a64 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -24,12 +24,11 @@ type DockerProjectOptions struct { Context string `yaml:"context,omitempty" json:"context,omitempty"` Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` Target string `yaml:"target,omitempty" json:"target,omitempty"` - Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` - Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` - Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` - RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` - LocalFallback bool `yaml:"localFallback,omitempty" json:"localFallback,omitempty"` - BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` + Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` + Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` + Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` + RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` + BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` // not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially. // Aspire would pass the secret keys, which are env vars that azd will set just to run docker build. BuildSecrets []string `yaml:"-" json:"-"` diff --git a/cli/azd/pkg/project/mapper_registry.go b/cli/azd/pkg/project/mapper_registry.go index ced0579349a..155879da1b0 100644 --- a/cli/azd/pkg/project/mapper_registry.go +++ b/cli/azd/pkg/project/mapper_registry.go @@ -198,9 +198,8 @@ func registerProjectMappings() { Registry: registry, Image: image, Tag: tag, - RemoteBuild: src.RemoteBuild, - LocalFallback: src.LocalFallback, - BuildArgs: buildArgs, + RemoteBuild: src.RemoteBuild, + BuildArgs: buildArgs, }, nil }) @@ -406,15 +405,14 @@ func registerProjectMappings() { } result := DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: osutil.NewExpandableString(src.Registry), - Image: osutil.NewExpandableString(src.Image), - Tag: osutil.NewExpandableString(src.Tag), - RemoteBuild: src.RemoteBuild, - LocalFallback: src.LocalFallback, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: osutil.NewExpandableString(src.Registry), + Image: osutil.NewExpandableString(src.Image), + Tag: osutil.NewExpandableString(src.Tag), + RemoteBuild: src.RemoteBuild, } if len(src.BuildArgs) > 0 { @@ -434,15 +432,14 @@ func registerProjectMappings() { } result := &DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: osutil.NewExpandableString(src.Registry), - Image: osutil.NewExpandableString(src.Image), - Tag: osutil.NewExpandableString(src.Tag), - RemoteBuild: src.RemoteBuild, - LocalFallback: src.LocalFallback, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: osutil.NewExpandableString(src.Registry), + Image: osutil.NewExpandableString(src.Image), + Tag: osutil.NewExpandableString(src.Tag), + RemoteBuild: src.RemoteBuild, } if len(src.BuildArgs) > 0 { diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 35603dcfa81..50c1076940e 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -864,12 +864,7 @@ "remoteBuild": { "type": "boolean", "title": "Optional. Whether to build the image remotely", - "description": "If set to true, the image will be built remotely using the Azure Container Registry remote build feature. If set to false, the image will be built locally using Docker." - }, - "localFallback": { - "type": "boolean", - "title": "Optional. Whether to fall back to local Docker build on remote build failure", - "description": "When set to true and remoteBuild is also true, azd falls back to building the container image locally if the remote Azure Container Registry build fails. This is helpful for subscriptions that do not support ACR Tasks, such as free trial subscriptions." + "description": "If set to true, the image will be built remotely using the Azure Container Registry remote build feature. If the remote build fails, azd automatically falls back to building locally using Docker or Podman if available. If set to false, the image will be built locally." } } }, From 4a59e5b14180e29a0bb46fba819e5eb1a6be5caf Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 11 Mar 2026 13:17:58 -0700 Subject: [PATCH 6/7] fix: resolve lint errors (errorlint, gofmt) - Use %w instead of %s for dockerErr in container_helper.go (errorlint) - Fix struct field alignment in DockerProjectOptions (gofmt) - Fix struct literal alignment in mapper_registry.go (gofmt) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/project/container_helper.go | 2 +- cli/azd/pkg/project/framework_service_docker.go | 14 +++++++------- cli/azd/pkg/project/mapper_registry.go | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index 2b05ee688b8..91756e673a2 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -604,7 +604,7 @@ func (ch *ContainerHelper) Publish( // Check if a local container runtime (Docker/Podman) is available before falling back if dockerErr := ch.docker.CheckInstalled(ctx); dockerErr != nil { return nil, fmt.Errorf( - "remote build failed: %w\n\nLocal fallback unavailable: %s", + "remote build failed: %w\n\nLocal fallback unavailable: %w", err, dockerErr) } diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index c26423f2a64..fc52bb9cd16 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -20,13 +20,13 @@ import ( ) type DockerProjectOptions struct { - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Context string `yaml:"context,omitempty" json:"context,omitempty"` - Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` - Target string `yaml:"target,omitempty" json:"target,omitempty"` - Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` - Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` - Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Context string `yaml:"context,omitempty" json:"context,omitempty"` + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` + Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` + Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` // not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially. diff --git a/cli/azd/pkg/project/mapper_registry.go b/cli/azd/pkg/project/mapper_registry.go index 155879da1b0..11d6fcf21af 100644 --- a/cli/azd/pkg/project/mapper_registry.go +++ b/cli/azd/pkg/project/mapper_registry.go @@ -191,13 +191,13 @@ func registerProjectMappings() { } return &azdext.DockerProjectOptions{ - Path: src.Path, - Context: src.Context, - Platform: src.Platform, - Target: src.Target, - Registry: registry, - Image: image, - Tag: tag, + Path: src.Path, + Context: src.Context, + Platform: src.Platform, + Target: src.Target, + Registry: registry, + Image: image, + Tag: tag, RemoteBuild: src.RemoteBuild, BuildArgs: buildArgs, }, nil From 34793a364718246c67343d6760ded5183d86a76e Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 11 Mar 2026 18:04:01 -0700 Subject: [PATCH 7/7] fix: correct gofmt struct field alignment after rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/project/framework_service_docker.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index fc52bb9cd16..d2d19409a29 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -20,13 +20,13 @@ import ( ) type DockerProjectOptions struct { - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Context string `yaml:"context,omitempty" json:"context,omitempty"` - Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` - Target string `yaml:"target,omitempty" json:"target,omitempty"` - Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` - Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` - Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Context string `yaml:"context,omitempty" json:"context,omitempty"` + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"` + Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` + Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` // not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially.