From 10f9770f0cf97f4a14589adbd8e60f93af3a82a0 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:58:14 +0200 Subject: [PATCH] fix: escape workflow validation errors before Rich output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/workflows/_commands.py | 6 +++--- tests/test_workflows.py | 28 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index e73fdf1a0..fd83bebdc 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -374,7 +374,7 @@ def workflow_run( if errors: err.print("[red]Workflow validation failed:[/red]") for verr in errors: - err.print(f" • {verr}") + err.print(f" • {_escape_markup(str(verr))}") raise typer.Exit(1) # Parse inputs @@ -645,7 +645,7 @@ def workflow_add( if errors: console.print("[red]Error:[/red] Workflow validation failed:") for err in errors: - console.print(f" \u2022 {err}") + console.print(f" \u2022 {_escape_markup(str(err))}") raise typer.Exit(1) if expected_id is not None and definition.id != expected_id: @@ -903,7 +903,7 @@ def _install_workflow_from_catalog( shutil.rmtree(workflow_dir, ignore_errors=True) console.print("[red]Error:[/red] Downloaded workflow validation failed:") for err in errors: - console.print(f" \u2022 {err}") + console.print(f" \u2022 {_escape_markup(str(err))}") raise typer.Exit(1) # Enforce that the workflow's internal ID matches the catalog key diff --git a/tests/test_workflows.py b/tests/test_workflows.py index dc3cd0ec2..4e18557ed 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -5905,6 +5905,34 @@ class TestWorkflowAddSymlinkGuard: assert result.exit_code != 0 assert "symlinked .specify/workflows" in result.output + def test_add_escapes_rich_markup_in_validation_errors(self, temp_dir, monkeypatch): + """User-controlled YAML values in validation errors must not be parsed as Rich markup.""" + from typer.testing import CliRunner + from specify_cli import app + + (temp_dir / ".specify" / "workflows").mkdir(parents=True) + src = temp_dir / "incoming.yml" + src.write_text( + """ +schema_version: "1.0" +workflow: + id: "markup-wf" + name: "Markup" + version: "[bold]bad[/bold]" + +steps: + - id: step-one + command: speckit.specify +""", + encoding="utf-8", + ) + + monkeypatch.chdir(temp_dir) + result = CliRunner().invoke(app, ["workflow", "add", str(src)]) + + assert result.exit_code != 0 + assert "[bold]bad[/bold]" in result.output + @pytest.mark.skipif(not hasattr(os, "symlink"), reason="symlinks are unavailable") def test_add_refuses_symlinked_id_dir(self, temp_dir, monkeypatch, sample_workflow_yaml): """A symlinked install dir must not let a copy escape the project root."""