diff --git a/cli/azd/docs/environment-variables.md b/cli/azd/docs/environment-variables.md index 9a22784c156..37bfa1ac9be 100644 --- a/cli/azd/docs/environment-variables.md +++ b/cli/azd/docs/environment-variables.md @@ -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/","layer":""}` — 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 diff --git a/cli/azd/internal/cmd/deploy.go b/cli/azd/internal/cmd/deploy.go index e1c5648bd7e..933fde1302e 100644 --- a/cli/azd/internal/cmd/deploy.go +++ b/cli/azd/internal/cmd/deploy.go @@ -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 { diff --git a/cli/azd/internal/cmd/deploy_test.go b/cli/azd/internal/cmd/deploy_test.go index 118a9dda3cf..83272f1b309 100644 --- a/cli/azd/internal/cmd/deploy_test.go +++ b/cli/azd/internal/cmd/deploy_test.go @@ -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": {}, }, } @@ -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) { diff --git a/cli/azd/internal/cmd/service_graph.go b/cli/azd/internal/cmd/service_graph.go index f8adcbfa3aa..9b97eb8d0f7 100644 --- a/cli/azd/internal/cmd/service_graph.go +++ b/cli/azd/internal/cmd/service_graph.go @@ -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" ) @@ -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__*) and logs advisory hints // suggesting uses: declarations. This does NOT affect execution order — it diff --git a/cli/azd/internal/cmd/service_graph_test.go b/cli/azd/internal/cmd/service_graph_test.go index e56145d3ebe..a8a56cd6bf3 100644 --- a/cli/azd/internal/cmd/service_graph_test.go +++ b/cli/azd/internal/cmd/service_graph_test.go @@ -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" ) @@ -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"}} diff --git a/cli/azd/internal/cmd/up_graph.go b/cli/azd/internal/cmd/up_graph.go index 2822adfb46b..eb4165bab10 100644 --- a/cli/azd/internal/cmd/up_graph.go +++ b/cli/azd/internal/cmd/up_graph.go @@ -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) } } diff --git a/cli/azd/pkg/azapi/azure_client_linuxwebapp_test.go b/cli/azd/pkg/azapi/azure_client_linuxwebapp_test.go index 2e20f93f5ec..871f63155d2 100644 --- a/cli/azd/pkg/azapi/azure_client_linuxwebapp_test.go +++ b/cli/azd/pkg/azapi/azure_client_linuxwebapp_test.go @@ -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" @@ -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) { diff --git a/cli/azd/pkg/azapi/webapp.go b/cli/azd/pkg/azapi/webapp.go index aed9a4d708c..ad30403890a 100644 --- a/cli/azd/pkg/azapi/webapp.go +++ b/cli/azd/pkg/azapi/webapp.go @@ -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, @@ -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) }() @@ -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 @@ -243,7 +268,7 @@ func (cli *AzureClient) DeployAppServiceZip( } } else { // Deployment is successful - return new("OK"), nil + return &AppServiceZipDeployResult{Status: "OK"}, nil } break } @@ -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 diff --git a/cli/azd/pkg/azsdk/zip_deploy_client.go b/cli/azd/pkg/azsdk/zip_deploy_client.go index fb1709fad03..ed18d4fba30 100644 --- a/cli/azd/pkg/azsdk/zip_deploy_client.go +++ b/cli/azd/pkg/azsdk/zip_deploy_client.go @@ -25,9 +25,20 @@ import ( ) const ( - deployStatusInterval = 10 * time.Second + deployStatusInterval = 10 * time.Second + deployRuntimeStatusTimeout = 5 * time.Minute ) +// DeploymentStatusTimeoutError indicates that App Service did not report a terminal deployment +// status within the verification timeout. +type DeploymentStatusTimeoutError struct { + Timeout time.Duration +} + +func (e *DeploymentStatusTimeoutError) Error() string { + return fmt.Sprintf("app service did not report a terminal deployment status within %s", e.Timeout) +} + // ZipDeployClient wraps usage of app service zip deploy used for application deployments // More info can be found at the following: // https://github.com/MicrosoftDocs/azure-docs/blob/main/includes/app-service-deploy-zip-push-rest.md @@ -135,33 +146,55 @@ func (c *ZipDeployClient) BeginDeployTrackStatus( resourceGroup, appName string, ) (*runtime.Poller[armappservice.WebAppsClientGetProductionSiteDeploymentStatusResponse], error) { - request, err := c.createDeployRequest(ctx, zipFile) + client, deploymentStatusId, err := c.beginDeployTrackStatusRequest(ctx, zipFile, subscriptionId) if err != nil { return nil, err } + return beginProductionSiteDeploymentStatus(ctx, client, resourceGroup, appName, deploymentStatusId) +} + +func (c *ZipDeployClient) beginDeployTrackStatusRequest( + ctx context.Context, + zipFile io.ReadSeeker, + subscriptionId string, +) (*armappservice.WebAppsClient, string, error) { + request, err := c.createDeployRequest(ctx, zipFile) + if err != nil { + return nil, "", err + } + response, err := c.pipeline.Do(request) if err != nil { - return nil, err + return nil, "", err } defer response.Body.Close() if !runtime.HasStatusCode(response, http.StatusAccepted) { - return nil, runtime.NewResponseError(response) + return nil, "", runtime.NewResponseError(response) } client, err := armappservice.NewWebAppsClient(subscriptionId, c.cred, c.armClientOptions) - if err != nil { - return nil, fmt.Errorf("creating web app client: %w", err) + return nil, "", fmt.Errorf("creating web app client: %w", err) } deploymentStatusId := response.Header.Get("Scm-Deployment-Id") if deploymentStatusId == "" { - return nil, fmt.Errorf("empty deployment status id") + return nil, "", fmt.Errorf("empty deployment status id") } + return client, deploymentStatusId, nil +} + +func beginProductionSiteDeploymentStatus( + ctx context.Context, + client *armappservice.WebAppsClient, + resourceGroup string, + appName string, + deploymentStatusId string, +) (*runtime.Poller[armappservice.WebAppsClientGetProductionSiteDeploymentStatusResponse], error) { // Add 404 to default retry errors in azure-sdk-for-go. We get temporary 404s when the KUDO API received the request // and created a temp deployment id as a intermediate step before deployed with actual deployment id retryCtx := policy.WithRetryOptions(ctx, policy.RetryOptions{ @@ -286,20 +319,72 @@ func (c *ZipDeployClient) DeployTrackStatus( resourceGroup string, appName string, progressLog func(string)) error { + return c.deployTrackStatus( + ctx, + zipFile, + subscriptionId, + resourceGroup, + appName, + deployRuntimeStatusTimeout, + 3*time.Second, + progressLog, + ) +} + +func (c *ZipDeployClient) deployTrackStatus( + ctx context.Context, + zipFile io.ReadSeeker, + subscriptionId string, + resourceGroup string, + appName string, + statusTrackingTimeout time.Duration, + pollInterval time.Duration, + progressLog func(string), +) error { var response armappservice.WebAppsClientGetProductionSiteDeploymentStatusResponse - poller, err := c.BeginDeployTrackStatus(ctx, zipFile, subscriptionId, resourceGroup, appName) + client, deploymentStatusId, err := c.beginDeployTrackStatusRequest(ctx, zipFile, subscriptionId) if err != nil { return err } - delay := 3 * time.Second + delay := pollInterval pollCount := 0 + lastStatus := armappservice.DeploymentBuildStatus("") + statusTrackingDeadline := time.Now().Add(statusTrackingTimeout) + + beginCtx, cancelBegin := context.WithDeadline(ctx, statusTrackingDeadline) + poller, err := beginProductionSiteDeploymentStatus( + beginCtx, + client, + resourceGroup, + appName, + deploymentStatusId, + ) + cancelBegin() + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + if !time.Now().Before(statusTrackingDeadline) { + return &DeploymentStatusTimeoutError{Timeout: statusTrackingTimeout} + } + return err + } + for { var resp *http.Response - resp, err = poller.Poll(ctx) + pollCtx, cancelPoll := context.WithDeadline(ctx, statusTrackingDeadline) + resp, err = poller.Poll(pollCtx) + cancelPoll() if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + if !time.Now().Before(statusTrackingDeadline) { + return &DeploymentStatusTimeoutError{Timeout: statusTrackingTimeout} + } return err } @@ -311,6 +396,13 @@ func (c *ZipDeployClient) DeployTrackStatus( return err } + if response.Properties != nil && + response.Properties.Status != nil && + *response.Properties.Status != lastStatus { + lastStatus = *response.Properties.Status + statusTrackingDeadline = time.Now().Add(statusTrackingTimeout) + } + if poller.Done() { if response.Properties == nil || response.Properties.Status == nil { return fmt.Errorf("response or its properties are empty") @@ -342,12 +434,23 @@ func (c *ZipDeployClient) DeployTrackStatus( delay = 20 * time.Second } + remaining := time.Until(statusTrackingDeadline) + if remaining <= 0 { + return &DeploymentStatusTimeoutError{Timeout: statusTrackingTimeout} + } + + timer := time.NewTimer(min(delay, remaining)) select { case <-ctx.Done(): + timer.Stop() return ctx.Err() - case <-time.After(delay): + case <-timer.C: pollCount++ } + + if !time.Now().Before(statusTrackingDeadline) { + return &DeploymentStatusTimeoutError{Timeout: statusTrackingTimeout} + } } return nil diff --git a/cli/azd/pkg/azsdk/zip_deploy_client_test.go b/cli/azd/pkg/azsdk/zip_deploy_client_test.go index 81f3b550365..608a80af6f7 100644 --- a/cli/azd/pkg/azsdk/zip_deploy_client_test.go +++ b/cli/azd/pkg/azsdk/zip_deploy_client_test.go @@ -6,6 +6,7 @@ package azsdk import ( "bytes" "context" + "errors" "fmt" "net" "net/http" @@ -459,3 +460,140 @@ func TestLogWebAppDeploymentStatus(t *testing.T) { require.NoError(t, result.err) }) } + +func TestDeployTrackStatus_StatusTrackingTimeout(t *testing.T) { + mockContext := mocks.NewMockContext(t.Context()) + registerTrackedDeployMocks(mockContext) + + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && + strings.Contains(request.URL.Path, "/deploymentStatus/") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + response, err := mocks.CreateHttpResponseWithBody( + request, + http.StatusAccepted, + map[string]any{ + "status": "InProgress", + "properties": map[string]any{ + "status": armappservice.DeploymentBuildStatusBuildInProgress, + "numberOfInstancesSuccessful": 0, + "numberOfInstancesFailed": 0, + "numberOfInstancesInProgress": 1, + }, + }, + ) + response.Header.Set("Azure-AsyncOperation", request.URL.String()) + return response, err + }) + + client, err := NewZipDeployClient("HOSTNAME", &mocks.MockCredentials{}, mockContext.ArmClientOptions) + require.NoError(t, err) + + err = client.deployTrackStatus( + *mockContext.Context, + bytes.NewReader(nil), + "SUBSCRIPTION_ID", + "RESOURCE_GROUP_ID", + "APP_NAME", + time.Millisecond, + time.Millisecond, + func(string) {}, + ) + + timeoutErr, ok := errors.AsType[*DeploymentStatusTimeoutError](err) + require.True(t, ok) + require.Equal(t, time.Millisecond, timeoutErr.Timeout) +} + +func TestDeployTrackStatus_InitialStatusRequestTimeout(t *testing.T) { + mockContext := mocks.NewMockContext(t.Context()) + registerTrackedDeployMocks(mockContext) + + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && + strings.Contains(request.URL.Path, "/deploymentStatus/") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + <-request.Context().Done() + return nil, request.Context().Err() + }) + + client, err := NewZipDeployClient("HOSTNAME", &mocks.MockCredentials{}, mockContext.ArmClientOptions) + require.NoError(t, err) + + err = client.deployTrackStatus( + *mockContext.Context, + bytes.NewReader(nil), + "SUBSCRIPTION_ID", + "RESOURCE_GROUP_ID", + "APP_NAME", + time.Millisecond, + time.Millisecond, + func(string) {}, + ) + + timeoutErr, ok := errors.AsType[*DeploymentStatusTimeoutError](err) + require.True(t, ok) + require.Equal(t, time.Millisecond, timeoutErr.Timeout) +} + +func TestDeployTrackStatus_StatusChangeResetsTimeout(t *testing.T) { + mockContext := mocks.NewMockContext(t.Context()) + registerTrackedDeployMocks(mockContext) + + pollCount := 0 + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && + strings.Contains(request.URL.Path, "/deploymentStatus/") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + pollCount++ + status := armappservice.DeploymentBuildStatusBuildInProgress + if pollCount > 1 { + status = armappservice.DeploymentBuildStatusRuntimeStarting + } + + response, err := mocks.CreateHttpResponseWithBody( + request, + http.StatusAccepted, + map[string]any{ + "status": "InProgress", + "properties": map[string]any{ + "status": status, + "numberOfInstancesSuccessful": 0, + "numberOfInstancesFailed": 0, + "numberOfInstancesInProgress": 1, + }, + }, + ) + response.Header.Set("Azure-AsyncOperation", request.URL.String()) + return response, err + }) + + client, err := NewZipDeployClient("HOSTNAME", &mocks.MockCredentials{}, mockContext.ArmClientOptions) + require.NoError(t, err) + + err = client.deployTrackStatus( + *mockContext.Context, + bytes.NewReader(nil), + "SUBSCRIPTION_ID", + "RESOURCE_GROUP_ID", + "APP_NAME", + 40*time.Millisecond, + 20*time.Millisecond, + func(string) {}, + ) + + timeoutErr, ok := errors.AsType[*DeploymentStatusTimeoutError](err) + require.True(t, ok) + require.Equal(t, 40*time.Millisecond, timeoutErr.Timeout) + require.GreaterOrEqual(t, pollCount, 3) +} + +func registerTrackedDeployMocks(mockContext *mocks.MockContext) { + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodPost && strings.Contains(request.URL.Path, "/api/zipdeploy") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + response, _ := mocks.CreateEmptyHttpResponse(request, http.StatusAccepted) + response.Header.Set("Scm-Deployment-Id", "00000000-0000-0000-0000-000000000000") + return response, nil + }) +} diff --git a/cli/azd/pkg/project/service_models.go b/cli/azd/pkg/project/service_models.go index 03f48d7ead1..ea4e29507e9 100644 --- a/cli/azd/pkg/project/service_models.go +++ b/cli/azd/pkg/project/service_models.go @@ -81,4 +81,5 @@ type ServicePublishResult struct { // ServiceDeployResult is the result of a successful Deploy operation type ServiceDeployResult struct { Artifacts ArtifactCollection `json:"artifacts"` + Warnings []string `json:"warnings,omitempty"` } diff --git a/cli/azd/pkg/project/service_target_appservice.go b/cli/azd/pkg/project/service_target_appservice.go index 18ff767e527..deeb1094c21 100644 --- a/cli/azd/pkg/project/service_target_appservice.go +++ b/cli/azd/pkg/project/service_target_appservice.go @@ -326,6 +326,7 @@ func (st *appServiceTarget) zipDeploy( // Deploy to each target hasSlots := len(deployTargets) > 1 || (len(deployTargets) == 1 && deployTargets[0].SlotName != "") + var warnings []string for _, target := range deployTargets { zipFile, err := os.Open(zipFilePath) @@ -346,7 +347,8 @@ func (st *appServiceTarget) zipDeploy( progressMsg = "Uploading deployment package" } progress.SetProgress(NewServiceProgress(progressMsg)) - _, deployErr = st.cli.DeployAppServiceZip( + var deployResult *azapi.AppServiceZipDeployResult + deployResult, deployErr = st.cli.DeployAppServiceZip( ctx, targetResource.SubscriptionId(), targetResource.ResourceGroupName(), @@ -355,6 +357,9 @@ func (st *appServiceTarget) zipDeploy( func(logProgress string) { progress.SetProgress(NewServiceProgress(logProgress)) }, skipStatusCheck, ) + if deployErr == nil && deployResult.RuntimeStatusWarning != "" { + warnings = append(warnings, deployResult.RuntimeStatusWarning) + } } else { progressMsg := fmt.Sprintf("Uploading deployment package to slot '%s'", target.SlotName) progress.SetProgress(NewServiceProgress(progressMsg)) @@ -397,6 +402,7 @@ func (st *appServiceTarget) zipDeploy( return &ServiceDeployResult{ Artifacts: artifacts, + Warnings: warnings, }, nil }