fix: escape workflow step metadata (#3863)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marsel Safin
2026-07-30 18:10:22 +02:00
committed by GitHub
parent 81bf741b92
commit 0f3f2aa45e
2 changed files with 133 additions and 18 deletions

View File

@@ -2576,9 +2576,10 @@ def workflow_step_list():
console.print(" [bold]Custom (installed):[/bold]")
for key in sorted(installed):
meta = installed[key] or {}
name = meta.get("name", key)
version = meta.get("version", "?")
console.print(f" • [bold]{name}[/bold] ({key}) v{version}")
name = _escape_markup(str(meta.get("name", key)))
safe_key = _escape_markup(str(key))
version = _escape_markup(str(meta.get("version", "?")))
console.print(f" • [bold]{name}[/bold] ({safe_key}) v{version}")
console.print()
if not built_in and not installed:
@@ -3122,13 +3123,15 @@ def workflow_step_search(
install_note = (
"" if step.get("_install_allowed", True) else " [dim](discovery only)[/dim]"
)
name = _escape_markup(str(step.get("name", step.get("id", "?"))))
step_id = _escape_markup(str(step.get("id", "?")))
version = _escape_markup(str(step.get("version", "?")))
console.print(
f" [bold]{step.get('name', step.get('id', '?'))}[/bold]"
f" ({step.get('id', '?')}) v{step.get('version', '?')}{install_note}"
f" [bold]{name}[/bold] ({step_id}) v{version}{install_note}"
)
desc = step.get("description", "")
if desc:
console.print(f" {desc}")
console.print(f" {_escape_markup(str(desc))}")
console.print()
@@ -3141,6 +3144,7 @@ def workflow_step_info(
from .catalog import StepCatalog, StepCatalogError, StepRegistry
project_root = _require_specify_project()
safe_step_id = _escape_markup(str(step_id))
registry = StepRegistry(project_root)
installed_meta = registry.get(step_id)
@@ -3150,20 +3154,27 @@ def workflow_step_info(
is_builtin = builtin_step is not None and not installed_meta
if is_builtin:
console.print(f"\n[bold cyan]{step_id}[/bold cyan] [dim](built-in)[/dim]")
console.print(f" Type key: {step_id}")
console.print(f"\n[bold cyan]{safe_step_id}[/bold cyan] [dim](built-in)[/dim]")
console.print(f" Type key: {safe_step_id}")
console.print(" [green]Built-in step type[/green]")
return
if installed_meta:
name = _escape_markup(str(installed_meta.get("name", step_id)))
version = _escape_markup(str(installed_meta.get("version", "?")))
console.print(
f"\n[bold cyan]{installed_meta.get('name', step_id)}[/bold cyan] ({step_id})"
f"\n[bold cyan]{name}[/bold cyan] ({safe_step_id})"
)
console.print(f" Version: {installed_meta.get('version', '?')}")
console.print(f" Version: {version}")
if installed_meta.get("author"):
console.print(f" Author: {installed_meta['author']}")
console.print(
f" Author: {_escape_markup(str(installed_meta['author']))}"
)
if installed_meta.get("description"):
console.print(f" Description: {installed_meta['description']}")
console.print(
f" Description: "
f"{_escape_markup(str(installed_meta['description']))}"
)
console.print(" [green]Installed[/green]")
return
@@ -3175,20 +3186,24 @@ def workflow_step_info(
info = None
if info:
name = _escape_markup(str(info.get("name", step_id)))
version = _escape_markup(str(info.get("version", "?")))
console.print(
f"\n[bold cyan]{info.get('name', step_id)}[/bold cyan] ({step_id})"
f"\n[bold cyan]{name}[/bold cyan] ({safe_step_id})"
)
console.print(f" Version: {info.get('version', '?')}")
console.print(f" Version: {version}")
if info.get("author"):
console.print(f" Author: {info['author']}")
console.print(f" Author: {_escape_markup(str(info['author']))}")
if info.get("description"):
console.print(f" Description: {info['description']}")
console.print(
f" Description: {_escape_markup(str(info['description']))}"
)
console.print(" [yellow]Not installed[/yellow]")
console.print(
f"\n Install with: [cyan]specify workflow step add {step_id}[/cyan]"
f"\n Install with: [cyan]specify workflow step add {safe_step_id}[/cyan]"
)
else:
console.print(f"[red]Error:[/red] Step type '{step_id}' not found")
console.print(f"[red]Error:[/red] Step type '{safe_step_id}' not found")
raise typer.Exit(1)

View File

@@ -9416,6 +9416,106 @@ steps:
assert "symlinked .specify/workflows/runs" in result.output
class TestWorkflowStepRichMarkup:
"""Step discovery commands render metadata as literal text."""
METADATA = {
"id": "[magenta]step-id[/magenta]",
"name": "[red]Step Name[/red]",
"version": "[green]1.0.0[/green]",
"author": "[yellow]Author[/yellow]",
"description": "[blue]Description[/blue]",
}
def test_search_escapes_catalog_metadata(
self, project_dir, monkeypatch
):
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import StepCatalog
metadata = dict(self.METADATA)
monkeypatch.chdir(project_dir)
monkeypatch.setattr(
StepCatalog, "search", lambda _catalog, query=None: [metadata]
)
result = CliRunner().invoke(app, ["workflow", "step", "search"])
assert result.exit_code == 0, result.output
assert metadata["name"] in result.output
assert metadata["id"] in result.output
assert metadata["version"] in result.output
assert metadata["description"] in result.output
def test_info_escapes_catalog_metadata(
self, project_dir, monkeypatch
):
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import StepCatalog, StepRegistry
metadata = dict(self.METADATA)
monkeypatch.chdir(project_dir)
monkeypatch.setattr(StepRegistry, "get", lambda _registry, step_id: None)
monkeypatch.setattr(
StepCatalog,
"get_step_info",
lambda _catalog, step_id: metadata,
)
result = CliRunner().invoke(
app, ["workflow", "step", "info", metadata["id"]]
)
assert result.exit_code == 0, result.output
for value in metadata.values():
assert value in result.output
def test_info_escapes_missing_step_id(self, project_dir, monkeypatch):
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import StepCatalog, StepRegistry
step_id = "[red]missing[/red]"
monkeypatch.chdir(project_dir)
monkeypatch.setattr(StepRegistry, "get", lambda _registry, step_id: None)
monkeypatch.setattr(
StepCatalog,
"get_step_info",
lambda _catalog, step_id: None,
)
result = CliRunner().invoke(
app, ["workflow", "step", "info", step_id]
)
assert result.exit_code == 1, result.output
assert step_id in result.output
def test_list_escapes_installed_metadata(
self, project_dir, monkeypatch
):
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import StepRegistry
metadata = dict(self.METADATA)
monkeypatch.chdir(project_dir)
monkeypatch.setattr(
StepRegistry,
"list",
lambda _registry: {metadata["id"]: metadata},
)
result = CliRunner().invoke(app, ["workflow", "step", "list"])
assert result.exit_code == 0, result.output
assert metadata["name"] in result.output
assert metadata["id"] in result.output
assert metadata["version"] in result.output
class TestWorkflowStepAddCLI:
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="symlinks are unavailable")
def test_add_rejects_symlinked_steps_base_dir(self, project_dir, monkeypatch):