Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions cli/azd/pkg/project/service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 40 additions & 9 deletions cli/azd/pkg/project/service_target_staticwebapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <environment> 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
}
Comment on lines +39 to +47

type staticWebAppTarget struct {
env *environment.Environment
cli *azapi.AzureClient
Expand Down Expand Up @@ -177,13 +201,16 @@ func (at *staticWebAppTarget) Deploy(
dOptions.OutputRelativeFolderPath = packagePath
cwd = serviceConfig.Project.Path
}
// 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(),
DefaultStaticWebAppEnvironmentName,
serviceConfig.StaticWebApp.Environment,
*deploymentToken,
Comment on lines 207 to 214
dOptions,
at.env.Environ())
Expand All @@ -193,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
}

Expand Down Expand Up @@ -235,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 {
Expand All @@ -264,17 +290,22 @@ 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(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
DefaultStaticWebAppEnvironmentName,
apiEnvName,
)
if err != nil {
return fmt.Errorf("failed verifying static web app deployment: %w", err)
Expand Down
58 changes: 58 additions & 0 deletions cli/azd/pkg/project/service_target_staticwebapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
}
5 changes: 4 additions & 1 deletion cli/azd/pkg/tools/swa/swa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
47 changes: 41 additions & 6 deletions cli/azd/pkg/tools/swa/swa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -141,7 +140,7 @@ func Test_SwaDeploy(t *testing.T) {
"subscriptionID",
"resourceGroupID",
"appName",
"default",
"",
"deploymentToken",
DeployOptions{},
nil,
Expand All @@ -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",
Expand All @@ -191,7 +189,7 @@ func Test_SwaDeploy(t *testing.T) {
"subscriptionID",
"resourceGroupID",
"appName",
"default",
"",
"deploymentToken",
DeployOptions{
AppFolderPath: "appFolderPath",
Expand Down Expand Up @@ -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)
Expand All @@ -239,7 +236,7 @@ func Test_SwaDeploy(t *testing.T) {
"subscriptionID",
"resourceGroupID",
"appName",
"default",
"",
"deploymentToken",
DeployOptions{},
nil,
Expand All @@ -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)
}
29 changes: 29 additions & 0 deletions docs/reference/azure-yaml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,42 @@ 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 |
| `env` | map | Environment variables passed to the service |
| `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:
Expand Down
Loading