Skip to content

Commit 4fcfd1a

Browse files
marcelsafinCopilot
andcommitted
fix(workflows): escape rich markup in id-mismatch errors and validate --from source early
The two id-mismatch error paths interpolated repr() into Rich markup, so a stray bracket in a user typo could be parsed as markup. Route both through rich.markup.escape. `workflow add <source> --from <url>` also validated the source only after downloading. Validate it up front so a URL/path/typo fails without a network fetch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a2ef4a0 commit 4fcfd1a

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

src/specify_cli/workflows/_commands.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ def workflow_add(
606606
project_root = _require_specify_project()
607607
registry = WorkflowRegistry(project_root)
608608
workflows_dir = project_root / ".specify" / "workflows"
609+
# With --from, source names the expected workflow ID: validate it up
610+
# front so a URL/path/typo fails without a network fetch.
611+
if from_url is not None and not dev:
612+
_validate_workflow_id_or_exit(source)
609613
# Reject a symlinked .specify / .specify/workflows before any write so an
610614
# install can't escape the project root (covers the local, URL, and
611615
# catalog branches below — all write beneath workflows_dir).
@@ -635,8 +639,8 @@ def _validate_and_install_local(
635639

636640
if expected_id is not None and definition.id != expected_id:
637641
console.print(
638-
f"[red]Error:[/red] Workflow ID in YAML ({definition.id!r}) "
639-
f"does not match the requested workflow ID ({expected_id!r})."
642+
f"[red]Error:[/red] Workflow ID in YAML ({_escape_markup(repr(definition.id))}) "
643+
f"does not match the requested workflow ID ({_escape_markup(repr(expected_id))})."
640644
)
641645
raise typer.Exit(1)
642646

@@ -894,8 +898,8 @@ def _install_workflow_from_catalog(
894898
import shutil
895899
shutil.rmtree(workflow_dir, ignore_errors=True)
896900
console.print(
897-
f"[red]Error:[/red] Workflow ID in YAML ({definition.id!r}) "
898-
f"does not match catalog key ({workflow_id!r}). "
901+
f"[red]Error:[/red] Workflow ID in YAML ({_escape_markup(repr(definition.id))}) "
902+
f"does not match catalog key ({_escape_markup(repr(workflow_id))}). "
899903
f"The catalog entry may be misconfigured."
900904
)
901905
raise typer.Exit(1)

tests/test_workflows.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7020,6 +7020,30 @@ def test_add_from_url_id_mismatch_errors(self, project_dir, monkeypatch):
70207020
assert "does not match" in result.output
70217021
assert not WorkflowRegistry(project_dir).is_installed("align-wf")
70227022

7023+
def test_add_from_rejects_invalid_source_id_without_fetch(self, project_dir, monkeypatch):
7024+
"""--from with a non-workflow-id source (URL, path, uppercase) fails before any network fetch."""
7025+
from unittest.mock import patch
7026+
from typer.testing import CliRunner
7027+
from specify_cli import app
7028+
7029+
monkeypatch.chdir(project_dir)
7030+
calls: list[str] = []
7031+
7032+
def _fake_open(url, timeout=None, extra_headers=None):
7033+
calls.append(url)
7034+
raise AssertionError(f"network fetch attempted: {url}")
7035+
7036+
runner = CliRunner()
7037+
with patch("specify_cli.authentication.http.open_url", side_effect=_fake_open):
7038+
for bad_source in ("https://x/y.yml", "./local.yml", "BadCase"):
7039+
result = runner.invoke(
7040+
app,
7041+
["workflow", "add", bad_source, "--from", "https://example.com/workflow.yml"],
7042+
)
7043+
assert result.exit_code != 0
7044+
assert "Invalid workflow ID" in result.output
7045+
assert calls == []
7046+
70237047
# -- search --author -----------------------------------------------
70247048

70257049
def test_search_author_filters(self, project_dir, monkeypatch):

0 commit comments

Comments
 (0)