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: 1 addition & 1 deletion cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ integration.
| `AZD_DEPLOYMENT_ID_FILE` | Absolute path of a file where `azd` writes ARM deployment IDs in NDJSON format (one JSON line per layer) during `azd provision` or `azd up`. The file is truncated at the start of each provisioning run, and each infrastructure layer appends one line as its ARM deployment starts. Each line has the shape `{"deploymentId":"/subscriptions/.../deployments/<name>","layer":"<layer-name>"}` — the `layer` field is empty for non-layered (single-module) provisioning. Consumers should tail/watch the file and parse each line independently; unknown fields must be ignored for forward compatibility. The path must be absolute (relative paths are ignored); the containing directory must already exist and be writable. Lines are only appended when an ARM deployment is actually started — runs short-circuited by the deployment-state cache or canceled by provision validation do not produce output. A process-wide mutex serializes writes so each line is always complete. If the file cannot be written (for example, the parent directory does not exist, the path is not writable, or the path points to a directory rather than a file), provisioning continues and the failure is recorded via the standard log; that output is only visible when `--debug` or `AZD_DEBUG_LOG` is enabled. On Windows, consumers should use a file-watcher pattern that does not keep a read handle open, otherwise new appends may fail. Only Bicep deployments are supported. |
| `AZD_UP_CONCURRENCY` | Maximum number of steps to run in parallel during `azd up`. Parsed as a positive integer; clamped to a maximum of `64`. Falls back to `AZD_DEPLOY_CONCURRENCY` when unset. When both are unset, concurrency is unlimited. |
| `AZD_DEPLOY_{SERVICE}_SLOT_NAME` | Sets the App Service deployment slot target for a service. Replace `{SERVICE}` with the uppercase service name (hyphens become underscores). Set to `production` to deploy to the main app, or a slot name (e.g., `staging`). When slots exist and this is not set, `--no-prompt` mode fails with an error listing available targets. Applies to `host: appservice` only; Function Apps always deploy to the main site. |
| `AZD_DEPLOY_{SERVICE}_SKIP_STATUS_CHECK` | If `true`, skips runtime deployment status tracking for the named Linux App Service after zip deploy. Useful when the target web app is intentionally stopped. Parsed as a boolean (`true`/`false`/`1`/`0`). `{SERVICE}` follows the same naming rules as `AZD_DEPLOY_{SERVICE}_SLOT_NAME`. |
| `AZD_DEPLOY_{SERVICE}_SKIP_STATUS_CHECK` | If `true`, skips deployment status tracking for the named Linux App Service after the zip deployment request is accepted. By default, azd waits up to five minutes without a deployment status change. Each new status resets the five-minute wait. If the status remains unchanged, azd completes deployment with a warning. Useful when the target web app is intentionally stopped. Parsed as a boolean (`true`/`false`/`1`/`0`). `{SERVICE}` follows the same naming rules as `AZD_DEPLOY_{SERVICE}_SLOT_NAME`. |

## azd exec

Expand Down
4 changes: 4 additions & 0 deletions cli/azd/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ func (da *DeployAction) deployServicesGraph(
return nil, err
}

if da.formatter.Kind() != output.JsonFormat {
displayDeployWarnings(ctx, da.console, stableServices, state)
}

// Display service endpoint artifacts collected during deploy steps.
if da.formatter.Kind() != output.JsonFormat {
for _, svc := range stableServices {
Expand Down
12 changes: 11 additions & 1 deletion cli/azd/internal/cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,9 @@ func TestDeploymentResultJSON(t *testing.T) {
result := DeploymentResult{
Timestamp: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
Services: map[string]*project.ServiceDeployResult{
"api": {},
"api": {
Warnings: []string{"status did not change for 5m0s"},
},
"web": {},
},
}
Expand All @@ -510,6 +512,14 @@ func TestDeploymentResultJSON(t *testing.T) {
services, ok := parsed["services"].(map[string]any)
require.True(t, ok, "services should be a map")
require.Len(t, services, 2)

api, ok := services["api"].(map[string]any)
require.True(t, ok)
require.Equal(t, []any{"status did not change for 5m0s"}, api["warnings"])

web, ok := services["web"].(map[string]any)
require.True(t, ok)
require.NotContains(t, web, "warnings")
}

func TestResolveDAGConcurrency(t *testing.T) {
Expand Down
18 changes: 18 additions & 0 deletions cli/azd/internal/cmd/service_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/exegraph"
"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/project"
)
Expand Down Expand Up @@ -595,6 +596,23 @@ func deployTimeoutWarning(svcName string, timeout time.Duration) *ux.WarningMess
}
}

func displayDeployWarnings(
ctx context.Context,
console input.Console,
services []*project.ServiceConfig,
state *deployGraphState,
) {
for _, svc := range services {
if result := state.GetResult(svc.Name); result != nil {
for _, warning := range result.Warnings {
console.MessageUxItem(ctx, &ux.WarningMessage{
Description: fmt.Sprintf("Service '%s': %s", svc.Name, warning),
})
}
}
}
}

// suggestServiceDeps scans each service's Environment map for references to
// other services' env vars (SERVICE_<OTHER>_*) and logs advisory hints
// suggesting uses: declarations. This does NOT affect execution order — it
Expand Down
17 changes: 17 additions & 0 deletions cli/azd/internal/cmd/service_graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockinput"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -233,6 +234,22 @@ func TestDeployGraphState_ResultsSnapshot(t *testing.T) {
require.Equal(t, r1, state.GetResult("api"), "deleting from snapshot must not affect state")
}

func TestDisplayDeployWarnings(t *testing.T) {
services := []*project.ServiceConfig{{Name: "api"}, {Name: "web"}}
state := newDeployGraphState(services)
state.StoreResult("api", &project.ServiceDeployResult{
Warnings: []string{"deployment status did not change"},
})
state.StoreResult("web", &project.ServiceDeployResult{})
console := mockinput.NewMockConsole()

displayDeployWarnings(t.Context(), console, services, state)

require.Len(t, console.Output(), 1)
require.Contains(t, console.Output()[0], "WARNING:")
require.Contains(t, console.Output()[0], "Service 'api': deployment status did not change")
}

func TestDeployGraphState_StoreLoadContext(t *testing.T) {
t.Parallel()
services := []*project.ServiceConfig{{Name: "svc"}}
Expand Down
20 changes: 16 additions & 4 deletions cli/azd/internal/cmd/up_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,10 +618,22 @@ func (u *UpGraphAction) Run(
return nil, result.Error
}

// Display service endpoint artifacts collected during deploy steps.
for _, svc := range stableServices {
if dr := state.GetResult(svc.Name); dr != nil && len(dr.Artifacts) > 0 {
u.console.MessageUxItem(ctx, dr.Artifacts)
if u.formatter.Kind() != output.JsonFormat {
displayDeployWarnings(ctx, u.console, stableServices, state)

// Display service endpoint artifacts collected during deploy steps.
for _, svc := range stableServices {
if dr := state.GetResult(svc.Name); dr != nil && len(dr.Artifacts) > 0 {
u.console.MessageUxItem(ctx, dr.Artifacts)
}
}
} else {
deployResult := DeploymentResult{
Timestamp: time.Now(),
Services: state.ResultsSnapshot(),
}
if err := u.formatter.Format(deployResult, u.writer, nil); err != nil {
return nil, fmt.Errorf("up result could not be displayed: %w", err)
}
}

Expand Down
17 changes: 17 additions & 0 deletions cli/azd/pkg/azapi/azure_client_linuxwebapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package azapi

import (
"bytes"
"errors"
"net/http"
"strings"
"testing"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2"
Expand All @@ -16,6 +18,21 @@ import (
"github.com/stretchr/testify/require"
)

func TestAppServiceStatusTimeoutResult(t *testing.T) {
result, ok := appServiceStatusTimeoutResult(&azsdk.DeploymentStatusTimeoutError{
Timeout: 5 * time.Minute,
})

require.True(t, ok)
require.Equal(t, "OK", result.Status)
require.Contains(t, result.RuntimeStatusWarning, "Deployment completed")
require.Contains(t, result.RuntimeStatusWarning, "no App Service deployment status change for 5m0s")

result, ok = appServiceStatusTimeoutResult(errors.New("runtime failed"))
require.False(t, ok)
require.Nil(t, result)
}

// Test deployment status api (linux web app only)
func Test_DeployTrackLinuxWebAppStatus(t *testing.T) {
t.Run("Success", func(t *testing.T) {
Expand Down
31 changes: 28 additions & 3 deletions cli/azd/pkg/azapi/webapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ type AzCliAppServiceProperties struct {
HostNames []string
}

// AppServiceZipDeployResult describes the outcome of an App Service zip deployment.
type AppServiceZipDeployResult struct {
Status string
RuntimeStatusWarning string
}

func appServiceStatusTimeoutResult(err error) (*AppServiceZipDeployResult, bool) {
timeoutErr, ok := errors.AsType[*azsdk.DeploymentStatusTimeoutError](err)
if !ok {
return nil, false
}

return &AppServiceZipDeployResult{
Status: "OK",
RuntimeStatusWarning: fmt.Sprintf(
"Deployment completed, but azd observed no App Service deployment status change for %s. "+
"Check the app's runtime status and startup logs in the Azure Portal.",
timeoutErr.Timeout,
),
}, true
}

func (cli *AzureClient) GetAppServiceProperties(
ctx context.Context,
subscriptionId string,
Expand Down Expand Up @@ -169,7 +191,7 @@ func (cli *AzureClient) DeployAppServiceZip(
deployZipFile io.ReadSeeker,
progressLog func(string),
skipStatusCheck bool,
) (_ *string, err error) {
) (_ *AppServiceZipDeployResult, err error) {
ctx, span := tracing.Start(ctx, events.DeployAppServiceZipEvent)
defer func() { span.EndWithStatus(err) }()

Expand Down Expand Up @@ -234,6 +256,9 @@ func (cli *AzureClient) DeployAppServiceZip(
err = client.DeployTrackStatus(
ctx, deployZipFile, subscriptionId, resourceGroup, appName, progressLog)
if err != nil {
if timeoutResult, ok := appServiceStatusTimeoutResult(err); ok {
return timeoutResult, nil
}
if isBuildFailure(err) && attempt < maxBuildRetries {
progressLog("Build process failed — will retry after SCM stabilizes")
continue
Expand All @@ -243,7 +268,7 @@ func (cli *AzureClient) DeployAppServiceZip(
}
} else {
// Deployment is successful
return new("OK"), nil
return &AppServiceZipDeployResult{Status: "OK"}, nil
}
break
}
Expand All @@ -263,7 +288,7 @@ func (cli *AzureClient) DeployAppServiceZip(
return nil, err
}

return &response.StatusText, nil
return &AppServiceZipDeployResult{Status: response.StatusText}, nil
}

// isBuildFailure returns true when the deployment error indicates a transient
Expand Down
Loading
Loading