Skip to content

Commit 0f35993

Browse files
author
Shikha Jha
committed
added detailed failure prompt
1 parent d01027d commit 0f35993

4 files changed

Lines changed: 1089 additions & 18 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
"""
7+
Context-enriched error builder for az webapp deploy / az functionapp deploy.
8+
9+
Instead of raising a bare "Status Code: 504" error, this module builds a structured
10+
diagnostic context block that includes the error code, deployment stage, runtime info,
11+
common causes, suggested fixes, and a ready-to-use Copilot prompt.
12+
"""
13+
14+
import yaml
15+
from knack.log import get_logger
16+
from knack.util import CLIError
17+
18+
from ._deployment_failure_patterns import match_failure_pattern
19+
20+
logger = get_logger(__name__)
21+
22+
23+
def _safe_yaml_dump(data):
24+
"""Dump dict to YAML string, falling back to repr on error."""
25+
try:
26+
return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True).rstrip()
27+
except Exception: # pylint: disable=broad-except
28+
return repr(data)
29+
30+
31+
def _get_app_runtime(cmd, resource_group_name, webapp_name, slot=None):
32+
"""Fetch the runtime name/version from the webapp config."""
33+
try:
34+
from ._client_factory import web_client_factory
35+
client = web_client_factory(cmd.cli_ctx)
36+
if slot:
37+
config = client.web_apps.get_configuration_slot(resource_group_name, webapp_name, slot)
38+
else:
39+
config = client.web_apps.get_configuration(resource_group_name, webapp_name)
40+
# Linux apps store runtime in linux_fx_version (e.g. "PYTHON|3.11")
41+
if config.linux_fx_version:
42+
return config.linux_fx_version
43+
# Windows apps: check e.g. net_framework_version, java_version, python_version, etc.
44+
for attr in ('net_framework_version', 'java_version', 'python_version',
45+
'php_version', 'node_version', 'power_shell_version'):
46+
val = getattr(config, attr, None)
47+
if val:
48+
return f"{attr.replace('_version', '').replace('_', ' ').title()} {val}"
49+
return "Unknown"
50+
except Exception: # pylint: disable=broad-except
51+
return "Unknown"
52+
53+
54+
def _get_app_region(cmd, resource_group_name, webapp_name):
55+
"""Fetch the Azure region of the web app."""
56+
try:
57+
from ._client_factory import web_client_factory
58+
client = web_client_factory(cmd.cli_ctx)
59+
app = client.web_apps.get(resource_group_name, webapp_name)
60+
return app.location if app else "Unknown"
61+
except Exception: # pylint: disable=broad-except
62+
return "Unknown"
63+
64+
65+
def _get_app_plan_sku(cmd, resource_group_name, webapp_name):
66+
"""Fetch the App Service plan SKU (e.g. B1, P1V2)."""
67+
try:
68+
from ._client_factory import web_client_factory
69+
from azure.mgmt.core.tools import parse_resource_id
70+
client = web_client_factory(cmd.cli_ctx)
71+
app = client.web_apps.get(resource_group_name, webapp_name)
72+
if app and app.server_farm_id:
73+
plan_parts = parse_resource_id(app.server_farm_id)
74+
plan = client.app_service_plans.get(plan_parts['resource_group'], plan_parts['name'])
75+
if plan and plan.sku:
76+
return plan.sku.name
77+
return "Unknown"
78+
except Exception: # pylint: disable=broad-except
79+
return "Unknown"
80+
81+
82+
def _determine_deployment_type(params):
83+
"""Infer the deployment mechanism from the params."""
84+
if params.src_url:
85+
return "OneDeploy (URL-based)"
86+
artifact = getattr(params, 'artifact_type', None)
87+
if artifact == 'zip':
88+
return "ZipDeploy"
89+
if artifact == 'war':
90+
return "WarDeploy"
91+
if artifact == 'jar':
92+
return "JarDeploy"
93+
if artifact == 'ear':
94+
return "EarDeploy"
95+
if artifact == 'startup':
96+
return "StartupFile"
97+
if artifact == 'static':
98+
return "StaticDeploy"
99+
return "OneDeploy"
100+
101+
102+
def build_enriched_error_context(params, status_code=None, error_message=None,
103+
deployment_status=None, deployment_properties=None,
104+
last_known_step=None, kudu_status=None):
105+
"""
106+
Build a structured context-enriched error dict for a deployment failure.
107+
108+
Parameters
109+
----------
110+
params : OneDeployParams
111+
The deployment parameters object.
112+
status_code : int, optional
113+
HTTP status code of the failed response.
114+
error_message : str, optional
115+
Raw error message / response body text.
116+
deployment_status : str, optional
117+
Deployment status string (e.g. RuntimeFailed, BuildFailed).
118+
deployment_properties : dict, optional
119+
Full deployment properties dict from the status API.
120+
last_known_step : str, optional
121+
The last step that completed successfully.
122+
kudu_status : str, optional
123+
The SCM/Kudu HTTP status if available.
124+
125+
Returns
126+
-------
127+
dict
128+
Structured error context ready for display.
129+
"""
130+
pattern = match_failure_pattern(
131+
status_code=status_code,
132+
error_message=error_message,
133+
deployment_status=deployment_status
134+
)
135+
136+
# Build base context
137+
context = {}
138+
139+
if pattern:
140+
context["errorCode"] = pattern["errorCode"]
141+
context["stage"] = pattern["stage"]
142+
else:
143+
context["errorCode"] = f"HTTP_{status_code}" if status_code else "UnknownDeploymentError"
144+
context["stage"] = deployment_status or "Unknown"
145+
146+
# App metadata (best-effort)
147+
context["runtime"] = _get_app_runtime(params.cmd, params.resource_group_name,
148+
params.webapp_name, params.slot)
149+
context["deploymentType"] = _determine_deployment_type(params)
150+
context["region"] = _get_app_region(params.cmd, params.resource_group_name, params.webapp_name)
151+
context["planSku"] = _get_app_plan_sku(params.cmd, params.resource_group_name, params.webapp_name)
152+
153+
# Causes and fixes
154+
if pattern:
155+
context["commonCauses"] = pattern["commonCauses"]
156+
context["suggestedFixes"] = pattern["suggestedFixes"]
157+
else:
158+
context["commonCauses"] = ["Unrecognised failure — see error details below"]
159+
context["suggestedFixes"] = [
160+
"Check deployment logs: 'az webapp log deployment show -n {} -g {}'".format(
161+
params.webapp_name, params.resource_group_name),
162+
"Check runtime logs: 'az webapp log tail -n {} -g {}'".format(
163+
params.webapp_name, params.resource_group_name)
164+
]
165+
166+
# Extra diagnostics
167+
if last_known_step:
168+
context["lastKnownStep"] = last_known_step
169+
if kudu_status:
170+
context["kuduStatus"] = str(kudu_status)
171+
172+
# Instance counts from deployment properties
173+
if deployment_properties:
174+
for key in ('numberOfInstancesInProgress', 'numberOfInstancesSuccessful',
175+
'numberOfInstancesFailed'):
176+
val = deployment_properties.get(key)
177+
if val is not None:
178+
context.setdefault("instanceStatus", {})[key] = int(val)
179+
errors = deployment_properties.get('errors')
180+
if errors:
181+
context["deploymentErrors"] = [
182+
{"code": e.get('extendedCode', ''), "message": e.get('message', '')}
183+
for e in errors[:3] # cap at 3
184+
]
185+
logs = deployment_properties.get('failedInstancesLogs')
186+
if logs:
187+
context["failedInstanceLogs"] = logs[0] if len(logs) == 1 else logs
188+
189+
# Raw details
190+
if error_message:
191+
context["rawError"] = error_message[:500] # truncate long bodies
192+
193+
return context
194+
195+
196+
def format_enriched_error_message(context):
197+
"""
198+
Format the structured context dict into a human-readable error message.
199+
200+
The output includes the YAML context block and a ready-to-use Copilot prompt.
201+
"""
202+
lines = []
203+
lines.append("")
204+
lines.append("=" * 72)
205+
lines.append("DEPLOYMENT FAILED — Context-Enriched Diagnostics")
206+
lines.append("=" * 72)
207+
lines.append("")
208+
209+
# YAML context block
210+
lines.append("--- COPILOT CONTEXT ---")
211+
lines.append(_safe_yaml_dump(context))
212+
lines.append("--- END CONTEXT ---")
213+
lines.append("")
214+
215+
# Human-readable summary
216+
lines.append(f"Error Code : {context.get('errorCode', 'Unknown')}")
217+
lines.append(f"Stage : {context.get('stage', 'Unknown')}")
218+
lines.append(f"Runtime : {context.get('runtime', 'Unknown')}")
219+
lines.append(f"Deploy Type : {context.get('deploymentType', 'Unknown')}")
220+
lines.append(f"Region : {context.get('region', 'Unknown')}")
221+
lines.append(f"Plan SKU : {context.get('planSku', 'Unknown')}")
222+
lines.append("")
223+
224+
causes = context.get("commonCauses", [])
225+
if causes:
226+
lines.append("Common Causes:")
227+
for c in causes:
228+
lines.append(f" - {c}")
229+
lines.append("")
230+
231+
fixes = context.get("suggestedFixes", [])
232+
if fixes:
233+
lines.append("Suggested Fixes:")
234+
for f in fixes:
235+
lines.append(f" - {f}")
236+
lines.append("")
237+
238+
if context.get("rawError"):
239+
lines.append(f"Raw Error : {context['rawError']}")
240+
lines.append("")
241+
242+
# Copilot prompt
243+
lines.append("-" * 72)
244+
lines.append("Ask Copilot:")
245+
lines.append(' Copy-paste the COPILOT CONTEXT block above into GitHub Copilot Chat,')
246+
lines.append(' or run:')
247+
lines.append(' gh copilot explain "Paste the COPILOT CONTEXT above and explain')
248+
lines.append(' why this deployment failed and what I should do"')
249+
lines.append("-" * 72)
250+
251+
return "\n".join(lines)
252+
253+
254+
def raise_enriched_deployment_error(params, status_code=None, error_message=None,
255+
deployment_status=None, deployment_properties=None,
256+
last_known_step=None, kudu_status=None):
257+
"""
258+
Build context-enriched diagnostics and raise a CLIError.
259+
260+
This is the main entry-point called from the deployment code paths.
261+
"""
262+
context = build_enriched_error_context(
263+
params=params,
264+
status_code=status_code,
265+
error_message=error_message,
266+
deployment_status=deployment_status,
267+
deployment_properties=deployment_properties,
268+
last_known_step=last_known_step,
269+
kudu_status=kudu_status
270+
)
271+
272+
logger.info("Deployment failure context: %s", context)
273+
274+
message = format_enriched_error_message(context)
275+
raise CLIError(message)

0 commit comments

Comments
 (0)