diff --git a/cli/azd/docs/environment-variables.md b/cli/azd/docs/environment-variables.md index 3af740096e2..f85fcdc540f 100644 --- a/cli/azd/docs/environment-variables.md +++ b/cli/azd/docs/environment-variables.md @@ -52,17 +52,6 @@ integration. | `AZD_BUILDER_IMAGE` | The builder docker image used to perform Dockerfile-less builds. | | `AZD_DEPLOY_TIMEOUT` | Timeout for deployment operations, parsed as an integer number of seconds (for example, `1200`). Defaults to `1200` seconds (20 minutes). | -### App Service Slot Deployments - -These variables control deployment slot behavior for Azure App Service targets. In all variable names, -`{SERVICE}` is the uppercase service name from `azure.yaml` with hyphens replaced by underscores -(e.g., service `my-api` → `MY_API`). - -| Variable | Description | -| --- | --- | -| `AZD_DEPLOY_{SERVICE}_SLOT_NAME` | When multiple deployment slots exist, auto-selects the named slot instead of prompting. The value must match an existing slot name. | -| `AZD_DEPLOY_{SERVICE}_IGNORE_SLOTS` | If true, bypasses all slot detection logic and deploys directly to the main app, even when deployment slots exist. Takes precedence over `AZD_DEPLOY_{SERVICE}_SLOT_NAME`. | - ## Extension Variables These variables are set and consumed by azd extension hosts (for example, IDE/editor integrations) diff --git a/cli/azd/pkg/project/service_target_appservice.go b/cli/azd/pkg/project/service_target_appservice.go index 5064b42bc13..03a3454c027 100644 --- a/cli/azd/pkg/project/service_target_appservice.go +++ b/cli/azd/pkg/project/service_target_appservice.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "os" - "strconv" "strings" "github.com/azure/azure-dev/cli/azd/internal/mapper" @@ -15,7 +14,6 @@ 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/ux" "github.com/azure/azure-dev/cli/azd/pkg/tools" ) @@ -230,10 +228,6 @@ type deploymentTarget struct { // based on deployment history and available slots. // // Deployment Strategy: -// - Override — AZD_DEPLOY_{SERVICE_NAME}_IGNORE_SLOTS (highest precedence): -// When set to a truthy boolean value, bypasses all slot detection and deploys directly -// to the main app. If AZD_DEPLOY_{SERVICE_NAME}_SLOT_NAME is also set, it is ignored -// and a warning is emitted. // - First deployment (no history): // Deploy to main app AND all slots to ensure consistency across all environments. // This prevents configuration drift and ensures all slots start with the same baseline. @@ -251,33 +245,6 @@ func (st *appServiceTarget) determineDeploymentTargets( targetResource *environment.TargetResource, progress *async.Progress[ServiceProgress], ) ([]deploymentTarget, error) { - // Check if slot deployment is explicitly disabled for this service - ignoreSlotsEnvVar := ignoreSlotsEnvVarNameForService(serviceConfig.Name) - if ignoreSlotsValue, hasIgnoreSlots := st.env.LookupEnv(ignoreSlotsEnvVar); hasIgnoreSlots { - ignoreSlots, err := strconv.ParseBool(ignoreSlotsValue) - if err != nil { - st.console.MessageUxItem(ctx, &ux.WarningMessage{ - Description: fmt.Sprintf( - "Ignoring invalid value %q for %s; expected a boolean value", - ignoreSlotsValue, ignoreSlotsEnvVar), - }) - } else if ignoreSlots { - // Warn if SLOT_NAME env var is also set, since it will be ignored - slotEnvVar := slotEnvVarNameForService(serviceConfig.Name) - if slotName := st.env.Getenv(slotEnvVar); slotName != "" { - st.console.MessageUxItem(ctx, &ux.WarningMessage{ - Description: fmt.Sprintf( - "%s is set but will be ignored because %s is enabled", - slotEnvVar, ignoreSlotsEnvVar), - }) - } - - progress.SetProgress(NewServiceProgress( - "Skipping slot deployment (deploying to main app)")) - return []deploymentTarget{{SlotName: ""}}, nil - } - } - progress.SetProgress(NewServiceProgress("Checking deployment history")) // Check if there are previous deployments @@ -368,16 +335,8 @@ func (st *appServiceTarget) determineDeploymentTargets( // for a given service. The format is AZD_DEPLOY_{SERVICE_NAME}_SLOT_NAME where the service name // is uppercase and any hyphens are replaced with underscores. func slotEnvVarNameForService(serviceName string) string { - return fmt.Sprintf("AZD_DEPLOY_%s_SLOT_NAME", environment.Key(serviceName)) -} - -// ignoreSlotsEnvVarNameForService returns the environment variable name for opting out of -// automatic slot deployment for a given service. The format is AZD_DEPLOY_{SERVICE_NAME}_IGNORE_SLOTS -// where the service name is uppercase and any hyphens are replaced with underscores. -// When set to a truthy boolean value, azd deploys directly to the main app, ignoring any -// configured deployment slots. -func ignoreSlotsEnvVarNameForService(serviceName string) string { - return fmt.Sprintf("AZD_DEPLOY_%s_IGNORE_SLOTS", environment.Key(serviceName)) + normalizedName := strings.ToUpper(strings.ReplaceAll(serviceName, "-", "_")) + return fmt.Sprintf("AZD_DEPLOY_%s_SLOT_NAME", normalizedName) } // Gets the exposed endpoints for the App Service, including any deployment slots diff --git a/cli/azd/pkg/project/service_target_appservice_test.go b/cli/azd/pkg/project/service_target_appservice_test.go index 666326cedeb..65a438217c6 100644 --- a/cli/azd/pkg/project/service_target_appservice_test.go +++ b/cli/azd/pkg/project/service_target_appservice_test.go @@ -4,18 +4,11 @@ package project import ( - "context" - "net/http" "strings" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" - "github.com/azure/azure-dev/cli/azd/pkg/async" "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/test/mocks" - "github.com/azure/azure-dev/cli/azd/test/mocks/mockaccount" "github.com/stretchr/testify/require" ) @@ -60,234 +53,3 @@ func TestNewAppServiceTargetTypeValidation(t *testing.T) { }) } } - -func TestSlotEnvVarNameForService(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - serviceName string - expected string - }{ - "SimpleService": { - serviceName: "api", - expected: "AZD_DEPLOY_API_SLOT_NAME", - }, - "HyphenatedService": { - serviceName: "my-web-app", - expected: "AZD_DEPLOY_MY_WEB_APP_SLOT_NAME", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := slotEnvVarNameForService(tc.serviceName) - require.Equal(t, tc.expected, result) - }) - } -} - -func TestIgnoreSlotsEnvVarNameForService(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - serviceName string - expected string - }{ - "SimpleService": { - serviceName: "api", - expected: "AZD_DEPLOY_API_IGNORE_SLOTS", - }, - "HyphenatedService": { - serviceName: "my-web-app", - expected: "AZD_DEPLOY_MY_WEB_APP_IGNORE_SLOTS", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := ignoreSlotsEnvVarNameForService(tc.serviceName) - require.Equal(t, tc.expected, result) - }) - } -} - -func TestDetermineDeploymentTargets_IgnoreSlots(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - envVars map[string]string - hasDeployments bool - slots []string - expectedSlots []string // empty string = main app - expectedWarning string // substring expected in console output, empty = no warning - }{ - "IgnoreSlots_True_NoSlots": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "true"}, - hasDeployments: true, - slots: []string{}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_True_OneSlot": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "true"}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_True_MultipleSlots": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "true"}, - hasDeployments: true, - slots: []string{"staging", "preview"}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_True_FirstDeployment_WithSlots": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "true"}, - hasDeployments: false, - slots: []string{"staging"}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_True_OverridesSlotName": { - envVars: map[string]string{ - "AZD_DEPLOY_API_IGNORE_SLOTS": "true", - "AZD_DEPLOY_API_SLOT_NAME": "staging", - }, - hasDeployments: true, - slots: []string{"staging", "preview"}, - expectedSlots: []string{""}, - expectedWarning: "AZD_DEPLOY_API_SLOT_NAME is set but will be ignored", - }, - "IgnoreSlots_False_OneSlot_SubsequentDeploy": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "false"}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{"staging"}, - }, - "IgnoreSlots_Unset_OneSlot_SubsequentDeploy": { - envVars: map[string]string{}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{"staging"}, - }, - "IgnoreSlots_Unset_NoSlots_SubsequentDeploy": { - envVars: map[string]string{}, - hasDeployments: true, - slots: []string{}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_Unset_FirstDeploy_WithSlots": { - envVars: map[string]string{}, - hasDeployments: false, - slots: []string{"staging"}, - expectedSlots: []string{"", "staging"}, - }, - "IgnoreSlots_TrueNumeric": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "1"}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_TrueUppercase": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "TRUE"}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{""}, - }, - "IgnoreSlots_InvalidValue_FallsBackToSlotLogic": { - envVars: map[string]string{"AZD_DEPLOY_API_IGNORE_SLOTS": "notabool"}, - hasDeployments: true, - slots: []string{"staging"}, - expectedSlots: []string{"staging"}, - expectedWarning: "Ignoring invalid value", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - t.Parallel() - - mockContext := mocks.NewMockContext(t.Context()) - azCli := azapi.NewAzureClient( - mockaccount.SubscriptionCredentialProviderFunc( - func(_ context.Context, _ string) (azcore.TokenCredential, error) { - return mockContext.Credentials, nil - }, - ), - mockContext.ArmClientOptions, - ) - - // Mock deployment history - mockContext.HttpClient.When(func(request *http.Request) bool { - return request.Method == http.MethodGet && - strings.Contains(request.URL.Path, "/deployments") - }).RespondFn(func(request *http.Request) (*http.Response, error) { - var deployments []*armappservice.Deployment - if tc.hasDeployments { - deployments = []*armappservice.Deployment{ - {ID: new("dep-1"), Name: new("dep-1")}, - } - } - response := armappservice.WebAppsClientListDeploymentsResponse{ - DeploymentCollection: armappservice.DeploymentCollection{ - Value: deployments, - }, - } - return mocks.CreateHttpResponseWithBody( - request, http.StatusOK, response) - }) - - // Mock slots - mockContext.HttpClient.When(func(request *http.Request) bool { - return request.Method == http.MethodGet && - strings.Contains(request.URL.Path, "/slots") - }).RespondFn(func(request *http.Request) (*http.Response, error) { - sites := make([]*armappservice.Site, len(tc.slots)) - for i, slot := range tc.slots { - fullName := "WEB_APP_NAME/" + slot - sites[i] = &armappservice.Site{Name: &fullName} - } - response := armappservice.WebAppsClientListSlotsResponse{ - WebAppCollection: armappservice.WebAppCollection{ - Value: sites, - }, - } - return mocks.CreateHttpResponseWithBody( - request, http.StatusOK, response) - }) - - env := environment.NewWithValues("test", tc.envVars) - target := &appServiceTarget{ - env: env, - cli: azCli, - console: mockContext.Console, - } - - serviceConfig := &ServiceConfig{Name: "api"} - targetResource := environment.NewTargetResource( - "SUB_ID", "RG_ID", "WEB_APP_NAME", - string(azapi.AzureResourceTypeWebSite), - ) - progress := async.NewNoopProgress[ServiceProgress]() - - targets, err := target.determineDeploymentTargets( - *mockContext.Context, - serviceConfig, - targetResource, - progress, - ) - - require.NoError(t, err) - require.Len(t, targets, len(tc.expectedSlots)) - for i, expected := range tc.expectedSlots { - require.Equal(t, expected, targets[i].SlotName) - } - - // Verify warning messages when expected - consoleOutput := strings.Join(mockContext.Console.Output(), "\n") - if tc.expectedWarning != "" { - require.Contains(t, consoleOutput, tc.expectedWarning) - } else { - require.Empty(t, mockContext.Console.Output(), - "unexpected warning in console output: %s", consoleOutput) - } - }) - } -}