From 8efa3c02dca5eb1bb4498d62fe81c4dcd1e26518 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:33:50 +0200 Subject: [PATCH] fix(workflows): enforce disabled state for direct paths to installed workflows Running the installed copy's YAML directly (specify workflow run .specify/workflows/align-wf/workflow.yml) skipped the registry check. File sources resolving inside .specify/workflows// now map back to the workflow ID and refuse to run while disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/workflows/_commands.py | 22 +++++++++++++++++----- tests/test_workflows.py | 7 +++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index be6e766c5..2ef514643 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -349,9 +349,10 @@ def workflow_run( err = _error_console(json_output) - if not is_file_source: - from .catalog import WorkflowRegistry + from .catalog import WorkflowRegistry + registered_id: str | None = None + if not is_file_source: # Reject path-equivalent spellings ("align-wf/", "align-wf/.") that # would miss the registry lookup yet still load the installed file, # bypassing the disabled check below. @@ -360,12 +361,23 @@ def workflow_run( f"[red]Error:[/red] Invalid workflow ID: {_escape_markup(repr(source))}" ) raise typer.Exit(1) + registered_id = source + else: + # A direct YAML path may still point at an installed workflow's own + # file; map it back to its ID so disabled state is enforced there too. + workflows_root = (project_root / ".specify" / "workflows").resolve() + resolved = source_path.resolve() + if resolved.is_relative_to(workflows_root): + rel_parts = resolved.relative_to(workflows_root).parts + if rel_parts: + registered_id = rel_parts[0] - installed_meta = WorkflowRegistry(project_root).get(source) + if registered_id is not None: + installed_meta = WorkflowRegistry(project_root).get(registered_id) if isinstance(installed_meta, dict) and installed_meta.get("enabled", True) is False: err.print( - f"[red]Error:[/red] Workflow '{_escape_markup(source)}' is disabled. " - f"Enable with: specify workflow enable {_escape_markup(source)}" + f"[red]Error:[/red] Workflow '{_escape_markup(registered_id)}' is disabled. " + f"Enable with: specify workflow enable {_escape_markup(registered_id)}" ) raise typer.Exit(1) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index c6ef0191c..2277db359 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -8040,6 +8040,13 @@ steps: assert result.exit_code != 0, spelling assert "Invalid workflow ID" in result.output, spelling + # Direct path to the installed workflow's own YAML must also refuse. + installed_yaml = ".specify/workflows/align-wf/workflow.yml" + assert (project_dir / installed_yaml).is_file() + result = runner.invoke(app, ["workflow", "run", installed_yaml]) + assert result.exit_code != 0 + assert "disabled" in result.output + def test_disable_shows_marker_in_list(self, project_dir, monkeypatch): from typer.testing import CliRunner from specify_cli import app