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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,11 @@ cmd_coverage/*

# Ignore test results
test_results.xml

# Squad agent framework (local only — never commit)
.squad/
.squad-workstream
.github/agents/squad.agent.md
.github/workflows/squad-*.yml
.github/workflows/sync-squad-labels.yml
.copilot/
103 changes: 94 additions & 9 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9781,6 +9781,43 @@ def _warmup_kudu_and_get_cookie_internal(params):
return None


def _send_deploy_request(cli_ctx, deploy_url, body):
"""Wrapper around send_raw_request for --src-url deployments that provides
actionable error messages instead of bare HTTP status codes."""
from azure.cli.core.azclierror import HTTPError
try:
return send_raw_request(cli_ctx, "PUT", deploy_url, body=body)
except HTTPError as ex:
resp = ex.response
status_code = resp.status_code if resp is not None else None
response_text = resp.text if resp is not None else ""
reason = getattr(resp, 'reason', None) or "Unknown"
error_detail = f" Details: {response_text}" if response_text else ""
if status_code == 400:
raise CLIError(
f"Deployment from URL failed with status 400 ({reason}).{error_detail}\n"
"Possible causes:\n"
" - The source URL is not publicly accessible or the SAS token has expired\n"
" - The URL does not point to a valid deployment artifact\n"
" - The artifact type does not match the file content (e.g., --type zip for a non-zip file)\n"
"Please verify the URL is accessible and the artifact type is correct."
) from ex
if status_code == 404:
raise ResourceNotFoundError(
f"Deployment from URL failed with status 404 ({reason}).{error_detail}\n"
"The target app or OneDeploy endpoint could not be found. "
"Verify that the resource group, app name, and slot (if any) are correct, "
"and that the app still exists."
) from ex
if status_code == 409:
raise ValidationError(
f"Deployment from URL failed with status 409 ({reason}).{error_detail}\n"
"Another deployment is currently in progress. Please wait for the existing "
"deployment to complete before starting a new one."
) from ex
raise


def _make_onedeploy_request(params):
import requests
from azure.cli.core.util import should_disable_connection_verify
Expand Down Expand Up @@ -9828,16 +9865,16 @@ def _make_onedeploy_request(params):
if cookies is None:
logger.info("Failed to fetch affinity cookie for Kudu. "
"Deployment will proceed without pre-warming a Kudu instance.")
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
else:
deploy_arm_url = _build_onedeploy_url(params, cookies.get("ARRAffinity"))
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_arm_url, body=body)
response = _send_deploy_request(params.cmd.cli_ctx, deploy_arm_url, body)
except Exception as ex: # pylint: disable=broad-except
logger.info("Failed to deploy using instances endpoint. "
"Deployment will proceed without pre-warming a Kudu instance. Ex: %s", ex)
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
else:
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
poll_async_deployment_for_debugging = False

# check the status of deployment
Expand All @@ -9856,12 +9893,60 @@ def _make_onedeploy_request(params):
deployment_status_url, params.slot, params.timeout)
logger.info('Server response: %s', response_body)
else:
# For --src-url deployments using ARM endpoint
if 'application/json' in response.headers.get('content-type', ""):
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
logger.warning("Deployment has completed successfully")
# Check if we should poll for completion (default is sync to match --src-path)
if params.is_async_deployment is not True:
# Try to extract deployment ID from ARM response
deployment_id = None
try:
response_json = response.json()
# Check for deployment ID in response
if 'id' in response_json:
deployment_id = response_json['id'].split('/')[-1]
elif 'properties' in response_json and 'deploymentId' in response_json['properties']:
deployment_id = response_json['properties']['deploymentId']
except Exception as ex: # pylint: disable=broad-except
logger.info("Failed to parse ARM response for deployment ID: %s", ex)

# If we have a deployment ID, poll for completion
if deployment_id:
logger.info("Tracking deployment ID: %s", deployment_id)
try:
deploymentstatusapi_url = _build_deploymentstatus_url(
params.cmd, params.resource_group_name, params.webapp_name,
params.slot, deployment_id
)
# Poll deployment status using the ARM deployment status API
logger.warning('Polling the status of sync deployment. Start Time: %s UTC',
datetime.datetime.now(datetime.timezone.utc))
response_body = _poll_deployment_runtime_status(
params.cmd, params.resource_group_name, params.webapp_name,
params.slot, deploymentstatusapi_url, deployment_id, params.timeout
)
except Exception as ex: # pylint: disable=broad-except
logger.warning("Failed to track deployment status: %s. "
"Deployment may still be in progress.", ex)
# Fallback to immediate response
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
else:
# No deployment ID found, return immediate response
logger.info("Could not extract deployment ID from ARM response, returning immediate status")
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
else:
# Async mode: return immediately with current state
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
if params.is_async_deployment is not True:
logger.warning("Deployment has completed successfully")
logger.warning("You can visit your app at: %s", _get_url(params.cmd, params.resource_group_name,
params.webapp_name, params.slot))
return response_body
Expand Down
Loading
Loading