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>
This commit is contained in:
marcelsafin
2026-07-10 23:24:12 +02:00
parent 560c7cdd5f
commit e44d21436f
3 changed files with 52 additions and 3 deletions

View File

@@ -86,8 +86,30 @@ Lists workflows installed in the current project.
specify workflow add <source>
```
| Option | Description |
| -------- | ------------------------------------------------------ |
| `--dev` | Install from a local workflow YAML file or directory |
| `--from` | Install from a custom URL (`<source>` 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 <workflow_id>
specify workflow disable <workflow_id>
```
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.

View File

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

View File

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