From 4fda59146b3aefa273605bb643ac212fe938277f Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Tue, 20 Oct 2026 23:41:31 -0400 Subject: [PATCH 1/2] Exit non-zero when 'system caches delete' task fails The runtime cache deletion runs as a Connect task that is waited on with raise_on_error=False, and the CLI discarded the returned task, so a task that finished with a non-zero code or an error still produced exit code 0. Raise an RSConnectException in that case so cli_feedback exits with 1. Dry runs return no task and are unaffected. Co-Authored-By: Claude Sonnet 5.5 --- rsconnect/main.py | 5 ++- tests/test_main_system_caches.py | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 7cb64dc54..c7864fed0 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -5921,7 +5921,10 @@ def system_caches_delete( cacert=cacert, logger=None, ).validate_server() - ce.delete_runtime_cache(language, version, image_name, dry_run) + _, task = ce.delete_runtime_cache(language, version, image_name, dry_run) + if task is not None and (task["code"] != 0 or task.get("error")): + reason = task.get("error") or "task exited with status %d" % task["code"] + raise RSConnectException("Failed to delete the runtime cache: %s" % reason) def _parse_installations(values: tuple[str, ...]) -> list[EnvironmentInstallation] | None: diff --git a/tests/test_main_system_caches.py b/tests/test_main_system_caches.py index 7cc58b891..9823b8a8d 100644 --- a/tests/test_main_system_caches.py +++ b/tests/test_main_system_caches.py @@ -117,6 +117,66 @@ def test_system_caches_delete_happy_path(self): self.assertEqual(body["version"], "1.2.3") self.assertEqual(body["image_name"], "Local") + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_system_caches_delete_task_failed(self): + """A task that finishes with a non-zero code is surfaced as exit code 1.""" + register_server_validation_uris(CONNECT_SERVER) + httpretty.register_uri( + httpretty.DELETE, + f"{CONNECT_SERVER}/__api__/v1/system/caches/runtime", + status=200, + body=json.dumps( + {"language": "Python", "version": "1.2.3", "image_name": "Local", "dry_run": False, "task_id": "abc123"} + ), + adding_headers={"Content-Type": "application/json"}, + ) + httpretty.register_uri( + httpretty.GET, + f"{CONNECT_SERVER}/__api__/v1/tasks/abc123", + body=json.dumps( + { + "id": "abc123", + "output": [], + "result": {"type": "", "data": ""}, + "finished": True, + "code": 1, + "error": "failed to delete cache", + "last": 0, + } + ), + adding_headers={"Content-Type": "application/json"}, + ) + + runner = CliRunner() + args = ["system", "caches", "delete", "--language", "Python", "--version", "1.2.3", "--image-name", "Local"] + apply_common_args(args, server=CONNECT_SERVER, key=API_KEY) + result = runner.invoke(cli, args) + + self.assertEqual(result.exit_code, 1, result.output) + self.assertRegex(result.output, "Failed to delete the runtime cache: failed to delete cache") + + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_system_caches_delete_dry_run(self): + """A dry run has no task to wait for and exits 0.""" + register_server_validation_uris(CONNECT_SERVER) + httpretty.register_uri( + httpretty.DELETE, + f"{CONNECT_SERVER}/__api__/v1/system/caches/runtime", + status=200, + body=json.dumps( + {"language": "Python", "version": "1.2.3", "image_name": "Local", "dry_run": True, "task_id": None} + ), + adding_headers={"Content-Type": "application/json"}, + ) + + runner = CliRunner() + args = ["system", "caches", "delete", "--language", "Python", "--version", "1.2.3", "--image-name", "Local"] + args.append("--dry-run") + apply_common_args(args, server=CONNECT_SERVER, key=API_KEY) + result = runner.invoke(cli, args) + + self.assertEqual(result.exit_code, 0, result.output) + def test_system_caches_delete_missing_all_flags(self): """Omitting both --language and --version yields exit code 2 (Click validation).""" runner = CliRunner() From 354e2ce131a7daf2e72b5304430f20cde44b29df Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Thu, 22 Oct 2026 17:22:20 -0400 Subject: [PATCH 2/2] fix: raise on failed task in delete_runtime_cache instead of in the CLI Remove raise_on_error=False so wait_for_task raises on a non-zero exit code, and drop the duplicate check from system_caches_delete. Co-Authored-By: Claude Sonnet 5.5 --- rsconnect/api.py | 2 +- rsconnect/main.py | 5 +---- tests/test_main_system_caches.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 33661da80..606489d06 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -3000,7 +3000,7 @@ def delete_runtime_cache(self, language: str, version: str, image_name: str, dry print("Dry run finished") return result, None else: - (_, task) = self.client.wait_for_task(result["task_id"], connect_logger.info, raise_on_error=False) + (_, task) = self.client.wait_for_task(result["task_id"], connect_logger.info) return result, task diff --git a/rsconnect/main.py b/rsconnect/main.py index c7864fed0..7cb64dc54 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -5921,10 +5921,7 @@ def system_caches_delete( cacert=cacert, logger=None, ).validate_server() - _, task = ce.delete_runtime_cache(language, version, image_name, dry_run) - if task is not None and (task["code"] != 0 or task.get("error")): - reason = task.get("error") or "task exited with status %d" % task["code"] - raise RSConnectException("Failed to delete the runtime cache: %s" % reason) + ce.delete_runtime_cache(language, version, image_name, dry_run) def _parse_installations(values: tuple[str, ...]) -> list[EnvironmentInstallation] | None: diff --git a/tests/test_main_system_caches.py b/tests/test_main_system_caches.py index 9823b8a8d..5425a208a 100644 --- a/tests/test_main_system_caches.py +++ b/tests/test_main_system_caches.py @@ -153,7 +153,7 @@ def test_system_caches_delete_task_failed(self): result = runner.invoke(cli, args) self.assertEqual(result.exit_code, 1, result.output) - self.assertRegex(result.output, "Failed to delete the runtime cache: failed to delete cache") + self.assertRegex(result.output, "Task exited with status 1.") @httpretty.activate(verbose=True, allow_net_connect=False) def test_system_caches_delete_dry_run(self):