From a5307256ca9c1406f787e99eb7f277cb9f6cfd91 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:04:29 +0200 Subject: [PATCH] fix(workflows): fail cleanly on unparseable catalog install URLs urlparse raises ValueError on e.g. an unbalanced IPv6 literal before the invalid-URL branch is reached; on workflow update that also bypassed the per-workflow handler and aborted the whole command. Convert the parse failure into a clean error so add fails cleanly and update skips just the affected workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/workflows/_commands.py | 10 +++++-- tests/test_workflows.py | 41 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index 36fb1b135..062b85116 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -825,8 +825,14 @@ def _install_workflow_from_catalog( from ipaddress import ip_address from urllib.parse import urlparse - parsed_url = urlparse(workflow_url) - url_host = parsed_url.hostname or "" + try: + parsed_url = urlparse(workflow_url) + url_host = parsed_url.hostname or "" + except ValueError: + console.print( + f"[red]Error:[/red] Workflow '{safe_wf_id}' has a malformed install URL." + ) + raise typer.Exit(1) is_loopback = False if url_host == "localhost": is_loopback = True diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 96578d1e0..96c2449d5 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -7619,6 +7619,47 @@ steps: # The previously installed workflow must survive a failed update. assert "1.0.0" in (wf_dir / "workflow.yml").read_text(encoding="utf-8") + def test_update_malformed_catalog_url_fails_cleanly(self, project_dir, monkeypatch): + """An unparseable catalog URL (unbalanced IPv6 literal) must not abort the whole update.""" + from typer.testing import CliRunner + from specify_cli import app + from specify_cli.workflows.catalog import WorkflowCatalog, WorkflowRegistry + + monkeypatch.chdir(project_dir) + registry = WorkflowRegistry(project_dir) + registry.add("align-wf", { + "name": "Align Workflow", + "version": "1.0.0", + "description": "CLI alignment test workflow", + "source": "catalog", + "catalog_name": "test-catalog", + "url": "https://[::1/workflow.yml", + }) + wf_dir = project_dir / ".specify" / "workflows" / "align-wf" + wf_dir.mkdir(parents=True) + (wf_dir / "workflow.yml").write_text( + self.WORKFLOW_YAML.format(version="1.0.0"), encoding="utf-8" + ) + + monkeypatch.setattr( + WorkflowCatalog, + "get_workflow_info", + lambda self, wid: { + "id": wid, + "name": "Align Workflow", + "version": "2.0.0", + "url": "https://[::1/workflow.yml", + "_install_allowed": True, + "_catalog_name": "test-catalog", + }, + ) + runner = CliRunner() + result = runner.invoke(app, ["workflow", "update"], input="y\n") + assert "malformed install URL" in result.output + assert "Failed to update" in result.output + # The previously installed workflow must survive. + assert "1.0.0" in (wf_dir / "workflow.yml").read_text(encoding="utf-8") + def test_update_preserves_disabled_state(self, project_dir, monkeypatch): from unittest.mock import patch from typer.testing import CliRunner