From c67fff8801dda8c228c1eda0749b88972fdf9721 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 03:53:34 +0000 Subject: [PATCH 1/6] Initial plan From 2ac1644896c6b735a0a3b125a5a513e644821ee9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:04:25 +0000 Subject: [PATCH 2/6] Fix flex consumption functionapp deployment: handle Kudu restart (status 6) as transient state Agent-Logs-Url: https://github.com/Azure/azure-cli/sessions/d22a7f98-aa7f-43a9-9e3e-a45c36c058b2 Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 28 ++++-- .../test_functionapp_commands_thru_mock.py | 97 +++++++++++++++++++ 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 5da17f08f94..55139d1d386 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -9652,6 +9652,9 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, # Indicates whether the status has been non empty in previous calls has_response = False has_partial_success = False + # Indicates Kudu is restarting after package deployment (transient state for flex consumption apps) + kudu_restart_in_progress = False + res_dict = {} while num_trials < total_trials: time.sleep(1) response = requests.get(deployment_status_url, headers=headers, @@ -9659,14 +9662,19 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, try: if response.status_code == 202 and not has_partial_success: has_partial_success = True - if response.status_code == 404 and has_partial_success: + if response.status_code == 404 and has_partial_success and not kudu_restart_in_progress: break - if (response.status_code == 404 or response.json().get('status') is None) and has_response: + if response.status_code == 404 and kudu_restart_in_progress: + # Kudu is restarting after package deployment — continue polling until it comes back + logger.warning("Deployment status endpoint returned 404. " + "Kudu may be restarting after package deployment. Retrying...") + res_dict = {} + elif (response.status_code == 404 or response.json().get('status') is None) and has_response: raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") - if (response.status_code != 404 and response.json().get('status') is not None) and not has_response: - has_response = True - - res_dict = response.json() + else: + if (response.status_code != 404 and response.json().get('status') is not None) and not has_response: + has_response = True + res_dict = response.json() except json.decoder.JSONDecodeError: logger.warning("Deployment status endpoint %s returns malformed data. Retrying...", deployment_status_url) res_dict = {} @@ -9685,12 +9693,14 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, if status == 5: raise CLIError("Deployment was cancelled and another deployment is in progress.") if status == 6: - raise CLIError("Deployment was partially successful. These are the deployment logs:\n{}".format( - json.dumps(show_deployment_log(cmd, rg_name, name)))) + if not kudu_restart_in_progress: + kudu_restart_in_progress = True + logger.warning("Deployment is partially complete. Kudu may be restarting after package " + "deployment. Continuing to poll for completion...") if 'progress' in res_dict: logger.info(res_dict['progress']) # show only in debug mode, customers seem to find this confusing # if the deployment is taking longer than expected - if res_dict.get('status', 0) != 4 and not has_partial_success: + if res_dict.get('status', 0) != 4 and not has_partial_success and not kudu_restart_in_progress: raise CLIError("""Timeout reached by the command, however, the deployment operation is still on-going. Navigate to your scm site to check the deployment status""") return res_dict diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands_thru_mock.py index 9f766ab8e2b..407c5165732 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands_thru_mock.py @@ -12,6 +12,7 @@ enable_zip_deploy_functionapp, enable_zip_deploy, enable_zip_deploy_flex, + _check_zip_deployment_status_flex, add_remote_build_app_settings, remove_remote_build_app_settings, config_source_control, @@ -681,3 +682,99 @@ def test_config_source_control(self, # assert self.assertEqual(response.git_hub_action_configuration.container_configuration.password, None) + + +class TestCheckZipDeploymentStatusFlex(unittest.TestCase): + """Tests for _check_zip_deployment_status_flex handling of Kudu restart (status 6) scenario.""" + + def _make_response(self, status_code, json_body=None): + """Create a mock HTTP response.""" + resp = mock.MagicMock() + resp.status_code = status_code + if json_body is not None: + resp.json.return_value = json_body + else: + resp.json.side_effect = Exception("No JSON body") + return resp + + @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={}) + @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False) + @mock.patch('time.sleep', return_value=None) + @mock.patch('requests.get') + def test_status_6_then_status_4_succeeds(self, requests_get_mock, sleep_mock, + should_disable_mock, get_headers_mock): + """Status 6 (Kudu restart) followed by status 4 (success) should not raise an error.""" + cmd_mock = _get_test_cmd() + + # Simulate: first poll returns status 6 (partial success / Kudu restarting), + # second poll returns status 4 (success) + requests_get_mock.side_effect = [ + self._make_response(200, {'status': 6, 'progress': ''}), + self._make_response(200, {'status': 4, 'complete': True}), + ] + + result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name', + 'https://mock-scm/api/deployments/latest', + timeout=None) + self.assertEqual(result.get('status'), 4) + + @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={}) + @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False) + @mock.patch('time.sleep', return_value=None) + @mock.patch('requests.get') + def test_status_6_then_404_then_status_4_succeeds(self, requests_get_mock, sleep_mock, + should_disable_mock, get_headers_mock): + """Status 6 followed by 404 (Kudu restarting) followed by status 4 should succeed.""" + cmd_mock = _get_test_cmd() + + requests_get_mock.side_effect = [ + self._make_response(200, {'status': 6, 'progress': ''}), + self._make_response(404), + self._make_response(200, {'status': 4, 'complete': True}), + ] + + result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name', + 'https://mock-scm/api/deployments/latest', + timeout=None) + self.assertEqual(result.get('status'), 4) + + @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={}) + @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False) + @mock.patch('time.sleep', return_value=None) + @mock.patch('requests.get') + def test_status_6_multiple_404_then_status_4_succeeds(self, requests_get_mock, sleep_mock, + should_disable_mock, get_headers_mock): + """Multiple 404 responses during Kudu restart should continue polling until success.""" + cmd_mock = _get_test_cmd() + + requests_get_mock.side_effect = [ + self._make_response(200, {'status': 6, 'progress': ''}), + self._make_response(404), + self._make_response(404), + self._make_response(200, {'status': 4, 'complete': True}), + ] + + result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name', + 'https://mock-scm/api/deployments/latest', + timeout=None) + self.assertEqual(result.get('status'), 4) + + @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={}) + @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False) + @mock.patch('time.sleep', return_value=None) + @mock.patch('requests.get') + def test_status_3_still_raises_error(self, requests_get_mock, sleep_mock, + should_disable_mock, get_headers_mock): + """Status 3 (deployment failed) should still raise an error.""" + cmd_mock = _get_test_cmd() + + with mock.patch('azure.cli.command_modules.appservice.custom.show_deployment_log', return_value=[]): + requests_get_mock.side_effect = [ + self._make_response(200, {'status': 3}), + ] + + with self.assertRaises(CLIError) as cm: + _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name', + 'https://mock-scm/api/deployments/latest', + timeout=None) + self.assertIn("Zip deployment failed", str(cm.exception)) From 38e3c16176ceff24ff2256df8cb146ab3a4ecfcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:06:37 +0000 Subject: [PATCH 3/6] Refactor: simplify 404 handling in _check_zip_deployment_status_flex Agent-Logs-Url: https://github.com/Azure/azure-cli/sessions/d22a7f98-aa7f-43a9-9e3e-a45c36c058b2 Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 55139d1d386..39b0bd9eb57 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -9662,17 +9662,19 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, try: if response.status_code == 202 and not has_partial_success: has_partial_success = True - if response.status_code == 404 and has_partial_success and not kudu_restart_in_progress: - break - if response.status_code == 404 and kudu_restart_in_progress: - # Kudu is restarting after package deployment — continue polling until it comes back - logger.warning("Deployment status endpoint returned 404. " - "Kudu may be restarting after package deployment. Retrying...") - res_dict = {} - elif (response.status_code == 404 or response.json().get('status') is None) and has_response: - raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") + if response.status_code == 404: + if has_partial_success and not kudu_restart_in_progress: + break + if kudu_restart_in_progress: + # Kudu is restarting after package deployment — continue polling until it comes back + logger.warning("Deployment status endpoint returned 404. " + "Kudu may be restarting after package deployment. Retrying...") + elif has_response: + raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") else: - if (response.status_code != 404 and response.json().get('status') is not None) and not has_response: + if response.json().get('status') is None and has_response: + raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") + if response.json().get('status') is not None and not has_response: has_response = True res_dict = response.json() except json.decoder.JSONDecodeError: From cdc90563b7ab917433caa661c4aec2d691087e54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:07:58 +0000 Subject: [PATCH 4/6] Refactor: use if-elif chain and cache response.json() in flex deployment status check Agent-Logs-Url: https://github.com/Azure/azure-cli/sessions/d22a7f98-aa7f-43a9-9e3e-a45c36c058b2 Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/command_modules/appservice/custom.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 39b0bd9eb57..26d38cb14fc 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -9665,18 +9665,19 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, if response.status_code == 404: if has_partial_success and not kudu_restart_in_progress: break - if kudu_restart_in_progress: + elif kudu_restart_in_progress: # Kudu is restarting after package deployment — continue polling until it comes back logger.warning("Deployment status endpoint returned 404. " "Kudu may be restarting after package deployment. Retrying...") elif has_response: raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") else: - if response.json().get('status') is None and has_response: + res_json = response.json() + if res_json.get('status') is None and has_response: raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") - if response.json().get('status') is not None and not has_response: + if res_json.get('status') is not None and not has_response: has_response = True - res_dict = response.json() + res_dict = res_json except json.decoder.JSONDecodeError: logger.warning("Deployment status endpoint %s returns malformed data. Retrying...", deployment_status_url) res_dict = {} From 683c111544fa8b00ba5cdd0a51caae73b0102d38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:09:31 +0000 Subject: [PATCH 5/6] Refine: add res_dict comment, limit 404 warning log to first occurrence only Agent-Logs-Url: https://github.com/Azure/azure-cli/sessions/d22a7f98-aa7f-43a9-9e3e-a45c36c058b2 Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/command_modules/appservice/custom.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 26d38cb14fc..fdd46c19d8d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -9654,6 +9654,9 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, has_partial_success = False # Indicates Kudu is restarting after package deployment (transient state for flex consumption apps) kudu_restart_in_progress = False + # Tracks whether the Kudu-restart 404 warning has already been emitted once + kudu_restart_404_warned = False + # Default return value; overwritten on each successful JSON response res_dict = {} while num_trials < total_trials: time.sleep(1) @@ -9667,8 +9670,12 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, break elif kudu_restart_in_progress: # Kudu is restarting after package deployment — continue polling until it comes back - logger.warning("Deployment status endpoint returned 404. " - "Kudu may be restarting after package deployment. Retrying...") + if not kudu_restart_404_warned: + kudu_restart_404_warned = True + logger.warning("Deployment status endpoint returned 404. " + "Kudu may be restarting after package deployment. Retrying...") + else: + logger.info("Deployment status endpoint still returning 404 during Kudu restart. Retrying...") elif has_response: raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") else: From d54aa4fb0e2781e18f98b73ae5c81a647ac3c9a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:11:03 +0000 Subject: [PATCH 6/6] Refine: cache res_json.get('status') in res_status variable Agent-Logs-Url: https://github.com/Azure/azure-cli/sessions/d22a7f98-aa7f-43a9-9e3e-a45c36c058b2 Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli/azure/cli/command_modules/appservice/custom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index fdd46c19d8d..b0390313934 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -9680,9 +9680,10 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url, raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") else: res_json = response.json() - if res_json.get('status') is None and has_response: + res_status = res_json.get('status') + if res_status is None and has_response: raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.") - if res_json.get('status') is not None and not has_response: + if res_status is not None and not has_response: has_response = True res_dict = res_json except json.decoder.JSONDecodeError: