diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index dbd07ae8e..73f8d7b2d 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -1096,10 +1096,21 @@ def workflow_update( wf_file = wf_dir / "workflow.yml" backup = wf_file.read_bytes() if wf_file.is_file() else None _install_workflow_from_catalog(project_root, registry, workflows_dir, update["id"]) - except typer.Exit: + except (typer.Exit, OSError) as exc: if backup is not None and wf_dir is not None and wf_file is not None: - wf_dir.mkdir(parents=True, exist_ok=True) - wf_file.write_bytes(backup) + try: + wf_dir.mkdir(parents=True, exist_ok=True) + wf_file.write_bytes(backup) + except OSError as restore_exc: + console.print( + f"[yellow]Warning:[/yellow] Could not restore backup for " + f"'{_escape_markup(update['id'])}': {_escape_markup(str(restore_exc))}" + ) + if isinstance(exc, OSError): + console.print( + f"[red]Error:[/red] Filesystem error updating " + f"'{_escape_markup(update['id'])}': {_escape_markup(str(exc))}" + ) failed.append(update["id"]) if failed: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 97e535084..1ea8a1d0e 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -7608,6 +7608,56 @@ steps: assert result.exit_code != 0 assert "Failed to update" in result.output + def test_update_survives_oserror_from_backup_read(self, project_dir, monkeypatch): + """OSError while reading the backup for one workflow must not abort the whole update.""" + import json + from pathlib import Path + from typer.testing import CliRunner + from specify_cli import app + from specify_cli.workflows.catalog import WorkflowRegistry, WorkflowCatalog + + monkeypatch.chdir(project_dir) + registry_path = WorkflowRegistry(project_dir).registry_path + registry_path.parent.mkdir(parents=True, exist_ok=True) + registry_path.write_text( + json.dumps( + { + "schema_version": "1.0", + "workflows": { + "wobbly": { + "name": "Wobbly", + "version": "0.0.1", + "source": "catalog", + "url": "https://example.com/wobbly.yml", + }, + }, + } + ), + encoding="utf-8", + ) + # An existing installed file whose read_bytes will raise OSError. + wf_file = project_dir / ".specify" / "workflows" / "wobbly" / "workflow.yml" + wf_file.parent.mkdir(parents=True, exist_ok=True) + wf_file.write_bytes(b"schema_version: '1.0'\nworkflow:\n id: wobbly\n name: Wobbly\n version: 0.0.1\nsteps: []\n") + monkeypatch.setattr( + WorkflowCatalog, + "get_workflow_info", + lambda self, wid: {"version": "9.9.9", "url": "https://example.com/wobbly.yml", "_install_allowed": True}, + ) + real_read = Path.read_bytes + + def _boom(self, *args, **kwargs): + if self.name == "workflow.yml" and "wobbly" in str(self): + raise OSError("simulated permission denied") + return real_read(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_bytes", _boom) + runner = CliRunner() + result = runner.invoke(app, ["workflow", "update"], input="y\n") + assert result.exit_code != 0, result.output + assert "Filesystem error" in result.output + assert "Failed to update" in result.output + def test_enable_disable_corrupted_registry_entry_errors(self, project_dir, monkeypatch): import json from typer.testing import CliRunner