Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ for each environment.
| `AZURE_APP_SERVICE_DASHBOARD_URI` | The URI for the .NET Aspire dashboard hosted on Azure App Service. |
| `AZURE_AKS_CLUSTER_NAME` | The name of the Azure Kubernetes Service cluster. |

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

Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
## Dev Center Variables

Variables for [Azure Dev Center](https://learn.microsoft.com/azure/dev-box/overview-what-is-microsoft-dev-box)
Expand Down
31 changes: 31 additions & 0 deletions cli/azd/pkg/project/service_target_appservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package project
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"

"github.com/azure/azure-dev/cli/azd/internal/mapper"
Expand Down Expand Up @@ -245,6 +247,25 @@ 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 ignoreSlots, err := strconv.ParseBool(
st.env.Getenv(ignoreSlotsEnvVar),
); err == nil && 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 != "" {
log.Printf(
"WARNING: %s is set but will be ignored because %s is enabled",
slotEnvVar, ignoreSlotsEnvVar,
)
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
}

progress.SetProgress(NewServiceProgress(
"Skipping slot deployment (deploying to main app)"))
return []deploymentTarget{{SlotName: ""}}, nil
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
}
Comment thread
vhvb1989 marked this conversation as resolved.

progress.SetProgress(NewServiceProgress("Checking deployment history"))

// Check if there are previous deployments
Expand Down Expand Up @@ -339,6 +360,16 @@ func slotEnvVarNameForService(serviceName string) string {
return fmt.Sprintf("AZD_DEPLOY_%s_SLOT_NAME", normalizedName)
}

// 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 {
normalizedName := strings.ToUpper(strings.ReplaceAll(serviceName, "-", "_"))
Comment thread
vhvb1989 marked this conversation as resolved.
Outdated
return fmt.Sprintf("AZD_DEPLOY_%s_IGNORE_SLOTS", normalizedName)
}

// Gets the exposed endpoints for the App Service, including any deployment slots
func (st *appServiceTarget) Endpoints(
ctx context.Context,
Expand Down
220 changes: 220 additions & 0 deletions cli/azd/pkg/project/service_target_appservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
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"
)

Expand Down Expand Up @@ -53,3 +60,216 @@ 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
}{
"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{""},
},
"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{""},
},
}

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))
Comment thread
vhvb1989 marked this conversation as resolved.
for i, expected := range tc.expectedSlots {
require.Equal(t, expected, targets[i].SlotName)
}
})
}
}
Loading