From 130bdb7b38cd4497457ab4975fdf5ae9f5f2ae19 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Thu, 19 Mar 2026 16:21:09 -0700 Subject: [PATCH 1/4] functions: use language switch for remoteBuild resolution --- .../pkg/project/service_target_functionapp.go | 73 +++++++++-- .../service_target_functionapp_test.go | 113 ++++++++++++++++++ 2 files changed, 178 insertions(+), 8 deletions(-) diff --git a/cli/azd/pkg/project/service_target_functionapp.go b/cli/azd/pkg/project/service_target_functionapp.go index 0cb893356cb..2cc96d21fa8 100644 --- a/cli/azd/pkg/project/service_target_functionapp.go +++ b/cli/azd/pkg/project/service_target_functionapp.go @@ -5,7 +5,9 @@ package project import ( "context" + "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -16,9 +18,67 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools" + "github.com/denormal/go-gitignore" ) +const functionAppRemoteBuildDocURL = "https://aka.ms/azd-functionapp-remote-build" + +// resolveFunctionAppRemoteBuild returns the appropriate remote build setting for function apps. +func resolveFunctionAppRemoteBuild(serviceConfig *ServiceConfig) (remoteBuild bool, err error) { + switch serviceConfig.Language { + case ServiceLanguageJavaScript, ServiceLanguageTypeScript: + ignore, err := gitignore.NewFromFile(filepath.Join(serviceConfig.Path(), serviceConfig.Host.IgnoreFile())) + if errors.Is(err, fs.ErrNotExist) { + // no ignore file, default to true + return true, nil + } + + if err != nil { + return false, fmt.Errorf("reading ignore file: %w", err) + } + + nodeModulesExcluded := false + if match := ignore.Relative("node_modules", true); match != nil && match.Ignore() { + nodeModulesExcluded = true + } + + if serviceConfig.RemoteBuild == nil { // remoteBuild option unset + // enable remote build only if 'node_modules' is excluded + return nodeModulesExcluded, nil + } + + if *serviceConfig.RemoteBuild && !nodeModulesExcluded { + return false, &internal.ErrorWithSuggestion{ + Err: fmt.Errorf("'remoteBuild: true' requires '.funcignore' to exclude node_modules"), + Suggestion: fmt.Sprintf( + "Update '.funcignore' to exclude node_modules, or set 'remoteBuild: false'. Learn more: %s", + output.WithLinkFormat(functionAppRemoteBuildDocURL), + ), + } + } + + if !*serviceConfig.RemoteBuild && nodeModulesExcluded { + return false, &internal.ErrorWithSuggestion{ + Err: fmt.Errorf("'remoteBuild: false' cannot be used when '.funcignore' excludes node_modules"), + Suggestion: fmt.Sprintf( + "Set 'remoteBuild: true', or remove node_modules from '.funcignore'. Learn more: %s", + output.WithLinkFormat(functionAppRemoteBuildDocURL), + ), + } + } + + return *serviceConfig.RemoteBuild, nil + default: + if serviceConfig.RemoteBuild != nil { + return *serviceConfig.RemoteBuild, nil + } + + return serviceConfig.Language == ServiceLanguagePython, nil + } +} + // functionAppTarget specifies an Azure Function to deploy to. // Implements `project.ServiceTarget` type functionAppTarget struct { @@ -156,17 +216,14 @@ func (f *functionAppTarget) Deploy( } progress.SetProgress(NewServiceProgress("Uploading deployment package")) - var remoteBuild bool - if serviceConfig.RemoteBuild != nil { - remoteBuild = *serviceConfig.RemoteBuild - } else { - remoteBuild = serviceConfig.Language == ServiceLanguageJavaScript || - serviceConfig.Language == ServiceLanguageTypeScript || - serviceConfig.Language == ServiceLanguagePython - } // Deploy to appropriate plan type if isFlexConsumption { + remoteBuild, buildErr := resolveFunctionAppRemoteBuild(serviceConfig) + if buildErr != nil { + return nil, buildErr + } + _, err = f.cli.DeployFunctionAppUsingZipFileFlexConsumption( ctx, targetResource.SubscriptionId(), diff --git a/cli/azd/pkg/project/service_target_functionapp_test.go b/cli/azd/pkg/project/service_target_functionapp_test.go index 81669d550c6..b7edecbf92b 100644 --- a/cli/azd/pkg/project/service_target_functionapp_test.go +++ b/cli/azd/pkg/project/service_target_functionapp_test.go @@ -4,14 +4,21 @@ package project import ( + "os" + "path/filepath" "strings" "testing" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/stretchr/testify/require" ) +func boolPtr(value bool) *bool { + return &value +} + func TestNewFunctionAppTargetTypeValidation(t *testing.T) { t.Parallel() @@ -45,3 +52,109 @@ func TestNewFunctionAppTargetTypeValidation(t *testing.T) { }) } } + +func TestResolveFunctionAppRemoteBuild_JavaScriptMatrix(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + remoteBuild *bool + funcIgnoreContent string + expectRemoteBuild bool + expectError string + }{ + { + name: "NoRemoteBuildAndFuncIgnoreExcludesNodeModules_RemoteBuildEnabled", + remoteBuild: nil, + funcIgnoreContent: "node_modules\n", + expectRemoteBuild: true, + }, + { + name: "NoRemoteBuildAndFuncIgnoreDoesNotExcludeNodeModules_RemoteBuildDisabled", + remoteBuild: nil, + funcIgnoreContent: "dist\n", + expectRemoteBuild: false, + }, + { + name: "NoRemoteBuildAndMissingFuncIgnore_RemoteBuildEnabled", + remoteBuild: nil, + funcIgnoreContent: "", + expectRemoteBuild: true, + }, + { + name: "RemoteBuildFalseAndFuncIgnoreExcludesNodeModules_Errors", + remoteBuild: boolPtr(false), + funcIgnoreContent: "node_modules\n", + expectError: "'remoteBuild: false' cannot be used when '.funcignore' excludes node_modules", + }, + { + name: "RemoteBuildFalseAndFuncIgnoreDoesNotExcludeNodeModules_Succeeds", + remoteBuild: boolPtr(false), + funcIgnoreContent: "dist\n", + expectRemoteBuild: false, + }, + { + name: "RemoteBuildTrueAndFuncIgnoreExcludesNodeModules_Succeeds", + remoteBuild: boolPtr(true), + funcIgnoreContent: "node_modules\n", + expectRemoteBuild: true, + }, + { + name: "RemoteBuildTrueAndFuncIgnoreDoesNotExcludeNodeModules_Errors", + remoteBuild: boolPtr(true), + funcIgnoreContent: "dist\n", + expectError: "'remoteBuild: true' requires '.funcignore' to exclude node_modules", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + serviceConfig := createTestServiceConfig(t.TempDir(), AzureFunctionTarget, ServiceLanguageJavaScript) + serviceConfig.RemoteBuild = tt.remoteBuild + + if tt.funcIgnoreContent != "" { + err := os.WriteFile( + filepath.Join(serviceConfig.Path(), ".funcignore"), + []byte(tt.funcIgnoreContent), + 0600, + ) + require.NoError(t, err) + } + + remoteBuild, err := resolveFunctionAppRemoteBuild(serviceConfig) + if tt.expectError != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.expectError) + + var suggestionErr *internal.ErrorWithSuggestion + require.ErrorAs(t, err, &suggestionErr) + require.Contains(t, suggestionErr.Suggestion, functionAppRemoteBuildDocURL) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectRemoteBuild, remoteBuild) + }) + } +} + +func TestResolveFunctionAppRemoteBuild_NonJavaScriptDefaults(t *testing.T) { + t.Parallel() + + pythonConfig := createTestServiceConfig(t.TempDir(), AzureFunctionTarget, ServiceLanguagePython) + remoteBuild, err := resolveFunctionAppRemoteBuild(pythonConfig) + require.NoError(t, err) + require.True(t, remoteBuild) + + pythonConfig.RemoteBuild = boolPtr(false) + remoteBuild, err = resolveFunctionAppRemoteBuild(pythonConfig) + require.NoError(t, err) + require.False(t, remoteBuild) + + csharpConfig := createTestServiceConfig(t.TempDir(), AzureFunctionTarget, ServiceLanguageCsharp) + remoteBuild, err = resolveFunctionAppRemoteBuild(csharpConfig) + require.NoError(t, err) + require.False(t, remoteBuild) +} From 685ad6f2269216c3d9dfb627f92f331bc07905b2 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Fri, 20 Mar 2026 11:41:13 -0700 Subject: [PATCH 2/4] delete boolPtr --- .../pkg/project/service_target_functionapp_test.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cli/azd/pkg/project/service_target_functionapp_test.go b/cli/azd/pkg/project/service_target_functionapp_test.go index b7edecbf92b..8ee1afdb369 100644 --- a/cli/azd/pkg/project/service_target_functionapp_test.go +++ b/cli/azd/pkg/project/service_target_functionapp_test.go @@ -15,10 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -func boolPtr(value bool) *bool { - return &value -} - func TestNewFunctionAppTargetTypeValidation(t *testing.T) { t.Parallel() @@ -83,25 +79,25 @@ func TestResolveFunctionAppRemoteBuild_JavaScriptMatrix(t *testing.T) { }, { name: "RemoteBuildFalseAndFuncIgnoreExcludesNodeModules_Errors", - remoteBuild: boolPtr(false), + remoteBuild: new(false), funcIgnoreContent: "node_modules\n", expectError: "'remoteBuild: false' cannot be used when '.funcignore' excludes node_modules", }, { name: "RemoteBuildFalseAndFuncIgnoreDoesNotExcludeNodeModules_Succeeds", - remoteBuild: boolPtr(false), + remoteBuild: new(false), funcIgnoreContent: "dist\n", expectRemoteBuild: false, }, { name: "RemoteBuildTrueAndFuncIgnoreExcludesNodeModules_Succeeds", - remoteBuild: boolPtr(true), + remoteBuild: new(true), funcIgnoreContent: "node_modules\n", expectRemoteBuild: true, }, { name: "RemoteBuildTrueAndFuncIgnoreDoesNotExcludeNodeModules_Errors", - remoteBuild: boolPtr(true), + remoteBuild: new(true), funcIgnoreContent: "dist\n", expectError: "'remoteBuild: true' requires '.funcignore' to exclude node_modules", }, @@ -148,7 +144,7 @@ func TestResolveFunctionAppRemoteBuild_NonJavaScriptDefaults(t *testing.T) { require.NoError(t, err) require.True(t, remoteBuild) - pythonConfig.RemoteBuild = boolPtr(false) + pythonConfig.RemoteBuild = new(false) remoteBuild, err = resolveFunctionAppRemoteBuild(pythonConfig) require.NoError(t, err) require.False(t, remoteBuild) From eecd855c42fcad4006aba808a6f07556b8b44b5c Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Mon, 23 Mar 2026 10:12:56 -0700 Subject: [PATCH 3/4] address feedback --- .../pkg/project/service_target_functionapp.go | 30 ++++++++++--------- .../service_target_functionapp_test.go | 7 ++++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/project/service_target_functionapp.go b/cli/azd/pkg/project/service_target_functionapp.go index 2cc96d21fa8..3f789da1de4 100644 --- a/cli/azd/pkg/project/service_target_functionapp.go +++ b/cli/azd/pkg/project/service_target_functionapp.go @@ -4,6 +4,7 @@ package project import ( + "bytes" "context" "errors" "fmt" @@ -18,19 +19,23 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/input" - "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/denormal/go-gitignore" ) -const functionAppRemoteBuildDocURL = "https://aka.ms/azd-functionapp-remote-build" - // resolveFunctionAppRemoteBuild returns the appropriate remote build setting for function apps. func resolveFunctionAppRemoteBuild(serviceConfig *ServiceConfig) (remoteBuild bool, err error) { switch serviceConfig.Language { case ServiceLanguageJavaScript, ServiceLanguageTypeScript: - ignore, err := gitignore.NewFromFile(filepath.Join(serviceConfig.Path(), serviceConfig.Host.IgnoreFile())) + ignoreFile := serviceConfig.Host.IgnoreFile() + ignoreFilePath := filepath.Join(serviceConfig.Path(), ignoreFile) + ignoreFileContents, err := os.ReadFile(ignoreFilePath) if errors.Is(err, fs.ErrNotExist) { + if serviceConfig.RemoteBuild != nil { + // no ignore file, nothing to validate -- return true + return *serviceConfig.RemoteBuild, nil + } + // no ignore file, default to true return true, nil } @@ -39,6 +44,9 @@ func resolveFunctionAppRemoteBuild(serviceConfig *ServiceConfig) (remoteBuild bo return false, fmt.Errorf("reading ignore file: %w", err) } + // Parse from in-memory contents so we don't hold an open file handle (important on Windows temp dirs). + ignore := gitignore.New(bytes.NewReader(ignoreFileContents), serviceConfig.Path(), nil) + nodeModulesExcluded := false if match := ignore.Relative("node_modules", true); match != nil && match.Ignore() { nodeModulesExcluded = true @@ -51,21 +59,15 @@ func resolveFunctionAppRemoteBuild(serviceConfig *ServiceConfig) (remoteBuild bo if *serviceConfig.RemoteBuild && !nodeModulesExcluded { return false, &internal.ErrorWithSuggestion{ - Err: fmt.Errorf("'remoteBuild: true' requires '.funcignore' to exclude node_modules"), - Suggestion: fmt.Sprintf( - "Update '.funcignore' to exclude node_modules, or set 'remoteBuild: false'. Learn more: %s", - output.WithLinkFormat(functionAppRemoteBuildDocURL), - ), + Err: fmt.Errorf("'remoteBuild: true' requires '%s' to exclude node_modules", ignoreFile), + Suggestion: fmt.Sprintf("Update '%s' to exclude node_modules, or set 'remoteBuild: false'.", ignoreFile), } } if !*serviceConfig.RemoteBuild && nodeModulesExcluded { return false, &internal.ErrorWithSuggestion{ - Err: fmt.Errorf("'remoteBuild: false' cannot be used when '.funcignore' excludes node_modules"), - Suggestion: fmt.Sprintf( - "Set 'remoteBuild: true', or remove node_modules from '.funcignore'. Learn more: %s", - output.WithLinkFormat(functionAppRemoteBuildDocURL), - ), + Err: fmt.Errorf("'remoteBuild: false' cannot be used when '%s' excludes node_modules", ignoreFile), + Suggestion: fmt.Sprintf("Set 'remoteBuild: true', or remove node_modules from '%s'.", ignoreFile), } } diff --git a/cli/azd/pkg/project/service_target_functionapp_test.go b/cli/azd/pkg/project/service_target_functionapp_test.go index 8ee1afdb369..9ea5b1ea4af 100644 --- a/cli/azd/pkg/project/service_target_functionapp_test.go +++ b/cli/azd/pkg/project/service_target_functionapp_test.go @@ -77,6 +77,12 @@ func TestResolveFunctionAppRemoteBuild_JavaScriptMatrix(t *testing.T) { funcIgnoreContent: "", expectRemoteBuild: true, }, + { + name: "RemoteBuildFalseAndMissingFuncIgnore_RemoteBuildDisabled", + remoteBuild: new(false), + funcIgnoreContent: "", + expectRemoteBuild: false, + }, { name: "RemoteBuildFalseAndFuncIgnoreExcludesNodeModules_Errors", remoteBuild: new(false), @@ -126,7 +132,6 @@ func TestResolveFunctionAppRemoteBuild_JavaScriptMatrix(t *testing.T) { var suggestionErr *internal.ErrorWithSuggestion require.ErrorAs(t, err, &suggestionErr) - require.Contains(t, suggestionErr.Suggestion, functionAppRemoteBuildDocURL) return } From 1810f0845fbecae7219512fb73d4745257470ec6 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Tue, 24 Mar 2026 13:18:37 -0700 Subject: [PATCH 4/4] do not exclude node_modules when remoteBuild: false --- cli/azd/pkg/project/project_utils.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/azd/pkg/project/project_utils.go b/cli/azd/pkg/project/project_utils.go index 82164e7fca7..ad41fddb341 100644 --- a/cli/azd/pkg/project/project_utils.go +++ b/cli/azd/pkg/project/project_utils.go @@ -84,6 +84,11 @@ func createDeployableZip(svc *ServiceConfig, root string) (string, error) { } } else if svc.Language == ServiceLanguageJavaScript || svc.Language == ServiceLanguageTypeScript { if name == "node_modules" && isDir { + if svc.RemoteBuild != nil && !*svc.RemoteBuild { + // if remote build is false, we do not exclude node_modules by default + return true, nil + } + return false, nil } }