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/<id>/ now map back to
the workflow ID and refuse to run while disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
marcelsafin
2026-07-10 23:33:50 +02:00
parent e44d21436f
commit 8efa3c02dc
2 changed files with 24 additions and 5 deletions

View File

@@ -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)

View File

@@ -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