From 6e024dd67fda699bc5799ff4131a2328071da8ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:35:46 +0000 Subject: [PATCH 1/3] Initial plan From c982224912ddc6eba4bfb6a10bd1db79249e9602 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:42:11 +0000 Subject: [PATCH 2/3] fix: omit --env flag for SWA production deployments to fix BadRequest error Co-authored-by: jongio <2163001+jongio@users.noreply.github.com> --- .../project/service_target_staticwebapp.go | 5 +- cli/azd/pkg/tools/swa/swa.go | 5 +- cli/azd/pkg/tools/swa/swa_test.go | 47 ++++++++++++++++--- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index 291f3124a42..d6de2413bef 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -177,13 +177,16 @@ func (at *staticWebAppTarget) Deploy( dOptions.OutputRelativeFolderPath = packagePath cwd = serviceConfig.Project.Path } + // Pass an empty environment name to the SWA CLI deploy command so that it deploys + // to the production environment. The SWA service rejects "default" as an explicit + // environment name; production deployments are made by omitting the --env flag. _, err = at.swa.Deploy(ctx, cwd, at.env.GetTenantId(), targetResource.SubscriptionId(), targetResource.ResourceGroupName(), targetResource.ResourceName(), - DefaultStaticWebAppEnvironmentName, + "", *deploymentToken, dOptions, at.env.Environ()) diff --git a/cli/azd/pkg/tools/swa/swa.go b/cli/azd/pkg/tools/swa/swa.go index bf56653affb..934e255137d 100644 --- a/cli/azd/pkg/tools/swa/swa.go +++ b/cli/azd/pkg/tools/swa/swa.go @@ -84,10 +84,13 @@ func (cli *Cli) Deploy( "--subscription-id", subscriptionId, "--resource-group", resourceGroup, "--app-name", appName, - "--env", environment, "--no-use-keychain", "--deployment-token", deploymentToken} + if environment != "" { + args = append(args, "--env", environment) + } + if options.AppFolderPath != "" { args = append(args, "--app-location", options.AppFolderPath) } diff --git a/cli/azd/pkg/tools/swa/swa_test.go b/cli/azd/pkg/tools/swa/swa_test.go index 8e9f807646b..e0cf7e2a8e8 100644 --- a/cli/azd/pkg/tools/swa/swa_test.go +++ b/cli/azd/pkg/tools/swa/swa_test.go @@ -119,7 +119,6 @@ func Test_SwaDeploy(t *testing.T) { "--subscription-id", "subscriptionID", "--resource-group", "resourceGroupID", "--app-name", "appName", - "--env", "default", "--no-use-keychain", "--deployment-token", "deploymentToken", }, args.Args) @@ -141,7 +140,7 @@ func Test_SwaDeploy(t *testing.T) { "subscriptionID", "resourceGroupID", "appName", - "default", + "", "deploymentToken", DeployOptions{}, nil, @@ -167,7 +166,6 @@ func Test_SwaDeploy(t *testing.T) { "--subscription-id", "subscriptionID", "--resource-group", "resourceGroupID", "--app-name", "appName", - "--env", "default", "--no-use-keychain", "--deployment-token", "deploymentToken", "--app-location", "appFolderPath", @@ -191,7 +189,7 @@ func Test_SwaDeploy(t *testing.T) { "subscriptionID", "resourceGroupID", "appName", - "default", + "", "deploymentToken", DeployOptions{ AppFolderPath: "appFolderPath", @@ -220,7 +218,6 @@ func Test_SwaDeploy(t *testing.T) { "--subscription-id", "subscriptionID", "--resource-group", "resourceGroupID", "--app-name", "appName", - "--env", "default", "--no-use-keychain", "--deployment-token", "deploymentToken", }, args.Args) @@ -239,7 +236,7 @@ func Test_SwaDeploy(t *testing.T) { "subscriptionID", "resourceGroupID", "appName", - "default", + "", "deploymentToken", DeployOptions{}, nil, @@ -252,3 +249,41 @@ func Test_SwaDeploy(t *testing.T) { ) }) } + +func Test_SwaDeploy_WithEnvironment(t *testing.T) { + mockContext := mocks.NewMockContext(t.Context()) + swacli := NewCli(mockContext.CommandRunner) + + var capturedArgs exec.RunArgs + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "npx") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + capturedArgs = args + return exec.RunResult{}, nil + }) + + _, err := swacli.Deploy( + t.Context(), + testPath, + "tenantID", + "subscriptionID", + "resourceGroupID", + "appName", + "staging", + "deploymentToken", + DeployOptions{}, + nil, + ) + require.NoError(t, err) + require.Equal(t, []string{ + "-y", swaCliPackage, + "deploy", + "--tenant-id", "tenantID", + "--subscription-id", "subscriptionID", + "--resource-group", "resourceGroupID", + "--app-name", "appName", + "--no-use-keychain", + "--deployment-token", "deploymentToken", + "--env", "staging", + }, capturedArgs.Args) +} From 485a3e21d0c99f1551997d4f5ea8d1359c9b3a63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:53:37 +0000 Subject: [PATCH 3/3] feat: add StaticWebAppOptions to support configurable SWA environments Co-authored-by: jongio <2163001+jongio@users.noreply.github.com> --- cli/azd/pkg/project/service_config.go | 2 + .../project/service_target_staticwebapp.go | 52 +++++++++++++---- .../service_target_staticwebapp_test.go | 58 +++++++++++++++++++ docs/reference/azure-yaml-schema.md | 29 ++++++++++ 4 files changed, 129 insertions(+), 12 deletions(-) diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 15287519157..de466a2b5c4 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -46,6 +46,8 @@ type ServiceConfig struct { Image osutil.ExpandableString `yaml:"image,omitempty"` // The optional docker options for configuring the output image Docker DockerProjectOptions `yaml:"docker,omitempty"` + // The optional static web app options for configuring Azure Static Web App deployments + StaticWebApp StaticWebAppOptions `yaml:"staticwebapp,omitempty"` // The optional K8S / AKS options K8s AksOptions `yaml:"k8s,omitempty"` // Infrastructure module path relative to the root infra folder diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index d6de2413bef..fcb1693ecc2 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -18,10 +18,34 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools/swa" ) -// TODO: Enhance for multi-environment support -// https://github.com/Azure/azure-dev/issues/1152 +// DefaultStaticWebAppEnvironmentName is the identifier used by the Azure REST API +// to refer to the production environment of a Static Web App. +// This value is distinct from the --env flag accepted by the SWA CLI: +// the SWA CLI uses an empty/omitted --env for production deploys. const DefaultStaticWebAppEnvironmentName = "default" +// StaticWebAppOptions contains configuration for deploying to an Azure Static Web App. +// These options can be specified in azure.yaml under the service's staticwebapp key. +type StaticWebAppOptions struct { + // The SWA environment to deploy to. When empty (the default), azd deploys to + // the production environment by omitting the --env flag from the SWA CLI command. + // Set this to a named preview environment (e.g. "staging") to deploy a + // non-production environment; the value is passed as --env to the + // SWA CLI and is also used when querying the Azure REST API for deployment status + // and endpoints. + Environment string `yaml:"environment,omitempty"` +} + +// apiEnvironmentName returns the environment identifier to use for Azure REST API +// calls against the Static Web Apps service. The production environment is identified +// as "default" in the API; named preview environments use their configured name. +func (o *StaticWebAppOptions) apiEnvironmentName() string { + if o.Environment != "" { + return o.Environment + } + return DefaultStaticWebAppEnvironmentName +} + type staticWebAppTarget struct { env *environment.Environment cli *azapi.AzureClient @@ -177,16 +201,16 @@ func (at *staticWebAppTarget) Deploy( dOptions.OutputRelativeFolderPath = packagePath cwd = serviceConfig.Project.Path } - // Pass an empty environment name to the SWA CLI deploy command so that it deploys - // to the production environment. The SWA service rejects "default" as an explicit - // environment name; production deployments are made by omitting the --env flag. + // Pass the configured environment name to the SWA CLI. When empty (the default) + // the SWA CLI omits --env entirely, which targets the production environment. + // Providing a non-empty name (e.g. "staging") deploys to a named preview environment. _, err = at.swa.Deploy(ctx, cwd, at.env.GetTenantId(), targetResource.SubscriptionId(), targetResource.ResourceGroupName(), targetResource.ResourceName(), - "", + serviceConfig.StaticWebApp.Environment, *deploymentToken, dOptions, at.env.Environ()) @@ -196,7 +220,7 @@ func (at *staticWebAppTarget) Deploy( } progress.SetProgress(NewServiceProgress("Verifying deployment")) - if err := at.verifyDeployment(ctx, targetResource); err != nil { + if err := at.verifyDeployment(ctx, serviceConfig, targetResource); err != nil { return nil, err } @@ -238,14 +262,13 @@ func (at *staticWebAppTarget) Endpoints( serviceConfig *ServiceConfig, targetResource *environment.TargetResource, ) ([]string, error) { - // TODO: Enhance for multi-environment support - // https://github.com/Azure/azure-dev/issues/1152 + apiEnvName := serviceConfig.StaticWebApp.apiEnvironmentName() if envProps, err := at.cli.GetStaticWebAppEnvironmentProperties( ctx, targetResource.SubscriptionId(), targetResource.ResourceGroupName(), targetResource.ResourceName(), - DefaultStaticWebAppEnvironmentName, + apiEnvName, ); err != nil { return nil, fmt.Errorf("fetching service properties: %w", err) } else { @@ -267,9 +290,14 @@ func (at *staticWebAppTarget) validateTargetResource( return nil } -func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, targetResource *environment.TargetResource) error { +func (at *staticWebAppTarget) verifyDeployment( + ctx context.Context, + serviceConfig *ServiceConfig, + targetResource *environment.TargetResource, +) error { retries := 0 const maxRetries = 10 + apiEnvName := serviceConfig.StaticWebApp.apiEnvironmentName() for { envProps, err := at.cli.GetStaticWebAppEnvironmentProperties( @@ -277,7 +305,7 @@ func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, targetResour targetResource.SubscriptionId(), targetResource.ResourceGroupName(), targetResource.ResourceName(), - DefaultStaticWebAppEnvironmentName, + apiEnvName, ) if err != nil { return fmt.Errorf("failed verifying static web app deployment: %w", err) diff --git a/cli/azd/pkg/project/service_target_staticwebapp_test.go b/cli/azd/pkg/project/service_target_staticwebapp_test.go index cda46d14669..b3d6473aa89 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp_test.go +++ b/cli/azd/pkg/project/service_target_staticwebapp_test.go @@ -10,6 +10,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestNewStaticWebAppTargetTypeValidation(t *testing.T) { @@ -53,3 +54,60 @@ func TestNewStaticWebAppTargetTypeValidation(t *testing.T) { }) } } + +func TestStaticWebAppOptions_ApiEnvironmentName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + opts StaticWebAppOptions + expected string + }{ + { + name: "DefaultsToProductionBuildId", + opts: StaticWebAppOptions{}, + expected: DefaultStaticWebAppEnvironmentName, + }, + { + name: "UsesConfiguredEnvironment", + opts: StaticWebAppOptions{Environment: "staging"}, + expected: "staging", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.opts.apiEnvironmentName()) + }) + } +} + +func TestStaticWebAppOptions_YamlRoundTrip(t *testing.T) { + t.Parallel() + + yamlInput := ` +host: staticwebapp +project: ./src/web +staticwebapp: + environment: staging +` + var svc ServiceConfig + err := yaml.Unmarshal([]byte(yamlInput), &svc) + require.NoError(t, err) + require.Equal(t, "staging", svc.StaticWebApp.Environment) +} + +func TestStaticWebAppOptions_YamlRoundTripNoEnvironment(t *testing.T) { + t.Parallel() + + // When staticwebapp key is absent, Environment should be empty and production is used. + yamlInput := ` +host: staticwebapp +project: ./src/web +` + var svc ServiceConfig + err := yaml.Unmarshal([]byte(yamlInput), &svc) + require.NoError(t, err) + require.Equal(t, "", svc.StaticWebApp.Environment) + require.Equal(t, DefaultStaticWebAppEnvironmentName, svc.StaticWebApp.apiEnvironmentName()) +} diff --git a/docs/reference/azure-yaml-schema.md b/docs/reference/azure-yaml-schema.md index e4f4337555a..8efca974f7d 100644 --- a/docs/reference/azure-yaml-schema.md +++ b/docs/reference/azure-yaml-schema.md @@ -51,6 +51,7 @@ services: | `dist` | string | Path to pre-built distribution directory | | `resourceName` | string | Override the Azure resource name | | `k8s` | object | Kubernetes-specific configuration | +| `staticwebapp` | object | Azure Static Web App-specific configuration (see below) | | `config` | object | Service-specific configuration | | `resourceGroup` | string | Override the resource group for this service | | `apiVersion` | string | API version for the hosting target | @@ -58,6 +59,34 @@ services: | `uses` | list | Service dependencies | | `remoteBuild` | boolean | Enable remote build (e.g., for Azure Functions) | +## Static Web App Options (`staticwebapp`) + +The `staticwebapp` key configures Azure Static Web Apps deployments. These options are only applicable when `host: staticwebapp`. + +| Property | Type | Description | +|---|---|---| +| `environment` | string | The named SWA preview environment to deploy to. When omitted (the default), azd deploys to the **production** environment by omitting `--env` from the SWA CLI command. Set to a custom name (e.g. `"staging"`) to target a named preview environment. | + +### Example: production deployment (default) + +```yaml +services: + web: + project: ./src/web + host: staticwebapp +``` + +### Example: named preview environment + +```yaml +services: + web: + project: ./src/web + host: staticwebapp + staticwebapp: + environment: staging +``` + ## Hooks Hooks run user-defined scripts at lifecycle points: