mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix: escape workflow step metadata (#3863)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user