From e44d21436f8a41a668b703031d0956440afa14e6 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:24:12 +0200 Subject: [PATCH] fix(workflows): validate workflow ID in run command and document new CLI flags Path-equivalent spellings like "align-wf/" previously bypassed the registry disabled check because the engine normalizes the path while the registry matches the raw string. workflow run now validates non-file sources against the workflow ID pattern before lookup. Also updates docs/reference/workflows.md with --dev/--from install options, update/enable/disable commands, and the search --author flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reference/workflows.md | 29 +++++++++++++++++++++++--- src/specify_cli/workflows/_commands.py | 9 ++++++++ tests/test_workflows.py | 17 +++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/reference/workflows.md b/docs/reference/workflows.md index 16bbe0893..c80ea7cd3 100644 --- a/docs/reference/workflows.md +++ b/docs/reference/workflows.md @@ -86,8 +86,30 @@ Lists workflows installed in the current project. specify workflow add ``` +| Option | Description | +| -------- | ------------------------------------------------------ | +| `--dev` | Install from a local workflow YAML file or directory | +| `--from` | Install from a custom URL (`` names the expected workflow ID) | + Installs a workflow from the catalog, a URL (HTTPS required), or a local file path. +## Update Workflows + +```bash +specify workflow update [workflow_id] +``` + +Updates one installed catalog workflow — or all of them when no ID is given — to the latest catalog version. Prompts for confirmation and keeps the installed copy if a download or validation fails. + +## Enable or Disable a Workflow + +```bash +specify workflow enable +specify workflow disable +``` + +Disabled workflows stay installed and listed (marked `[disabled]`) but refuse to run until re-enabled. + ## Remove a Workflow ```bash @@ -102,9 +124,10 @@ Removes an installed workflow from the project. specify workflow search [query] ``` -| Option | Description | -| ------- | --------------- | -| `--tag` | Filter by tag | +| Option | Description | +| ---------- | ----------------- | +| `--tag` | Filter by tag | +| `--author` | Filter by author | Searches all active catalogs for workflows matching the query. diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index 5b48aed0a..be6e766c5 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -352,6 +352,15 @@ def workflow_run( if not is_file_source: from .catalog import WorkflowRegistry + # 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. + if source in _RESERVED_WORKFLOW_IDS or not _WORKFLOW_ID_PATTERN.match(source): + err.print( + f"[red]Error:[/red] Invalid workflow ID: {_escape_markup(repr(source))}" + ) + raise typer.Exit(1) + installed_meta = WorkflowRegistry(project_root).get(source) if isinstance(installed_meta, dict) and installed_meta.get("enabled", True) is False: err.print( diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 0725db8d9..c6ef0191c 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -8023,6 +8023,23 @@ steps: result = runner.invoke(app, ["workflow", "run", "align-wf"]) assert result.exit_code == 0, result.output + def test_disable_blocks_run_via_path_equivalent_id(self, project_dir, monkeypatch): + """"align-wf/" must not run a disabled workflow by dodging the registry lookup.""" + from typer.testing import CliRunner + from specify_cli import app + + monkeypatch.chdir(project_dir) + runner = CliRunner() + self._install_dev(runner, app, project_dir) + + result = runner.invoke(app, ["workflow", "disable", "align-wf"]) + assert result.exit_code == 0, result.output + + for spelling in ("align-wf/", "align-wf/."): + result = runner.invoke(app, ["workflow", "run", spelling]) + assert result.exit_code != 0, spelling + assert "Invalid workflow ID" in result.output, spelling + def test_disable_shows_marker_in_list(self, project_dir, monkeypatch): from typer.testing import CliRunner from specify_cli import app