Skip to content

Commit 0add713

Browse files
jawwad-aliclaude
andauthored
fix(workflows): workflow add detects local YAML files case-insensitively (#3633)
`workflow add` gated the local-file branches on a case-SENSITIVE `.suffix in (".yml", ".yaml")` (the `--dev` branch and the plain local-path branch), while every other YAML-file detector in the CLI normalizes case: `workflow run` uses `source_path.suffix.lower()` and `WorkflowEngine.load_workflow` uses `path.suffix.lower()`. The result was an add/run inconsistency: `specify workflow run Sample.YAML` loads the file, but `specify workflow add Sample.YAML` does not recognize it as a local workflow — the `--dev` branch rejects it with "--dev source must be a workflow YAML file ..." and the plain path falls through to a catalog lookup that fails with "not found in catalog". Add `.lower()` to both suffix reads so `workflow add` matches its siblings. The lowercase happy path is unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cef00a1 commit 0add713

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/specify_cli/workflows/_commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,7 +1555,7 @@ def _validate_and_install_local(
15551555
# precedence over --from so a URL that would be ignored is never fetched.
15561556
if dev:
15571557
dev_path = Path(source).expanduser()
1558-
if dev_path.is_file() and dev_path.suffix in (".yml", ".yaml"):
1558+
if dev_path.is_file() and dev_path.suffix.lower() in (".yml", ".yaml"):
15591559
_validate_and_install_local(dev_path, str(dev_path))
15601560
return
15611561
if dev_path.is_dir():
@@ -1714,7 +1714,7 @@ def _validate_and_install_local(
17141714
# Try as a local file/directory
17151715
source_path = Path(source)
17161716
if source_path.exists():
1717-
if source_path.is_file() and source_path.suffix in (".yml", ".yaml"):
1717+
if source_path.is_file() and source_path.suffix.lower() in (".yml", ".yaml"):
17181718
_validate_and_install_local(source_path, str(source_path))
17191719
return
17201720
elif source_path.is_dir():

tests/test_workflows.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7786,6 +7786,63 @@ def rename_boom(src, dst):
77867786
assert "[stage]permissiondenied" in output_compact
77877787
assert "[reg]diskfull" in output_compact
77887788

7789+
7790+
class TestWorkflowAddCaseInsensitiveSuffix:
7791+
"""`workflow add` must detect a local YAML file case-insensitively, matching
7792+
`workflow run` (_commands.py:workflow_run) and the engine loader
7793+
(engine.py:WorkflowEngine.load_workflow), which both use `.suffix.lower()`.
7794+
Without it, `workflow run Sample.YAML` works but `workflow add Sample.YAML`
7795+
fails — an add/run inconsistency for an uppercase extension."""
7796+
7797+
def test_plain_path_accepts_uppercase_extension(self, temp_dir, monkeypatch, sample_workflow_yaml):
7798+
from typer.testing import CliRunner
7799+
from specify_cli import app
7800+
7801+
(temp_dir / ".specify" / "workflows").mkdir(parents=True)
7802+
src = temp_dir / "Sample.YAML"
7803+
src.write_text(sample_workflow_yaml, encoding="utf-8")
7804+
7805+
monkeypatch.chdir(temp_dir)
7806+
result = CliRunner().invoke(app, ["workflow", "add", str(src)])
7807+
7808+
# Before the fix: `.suffix in (...)` is case-sensitive, so ".YAML" is not
7809+
# recognized as a local file; the path falls through to catalog lookup
7810+
# and fails. After the fix it installs like the lowercase happy path.
7811+
assert result.exit_code == 0, result.output
7812+
assert "installed" in result.output
7813+
7814+
def test_dev_path_accepts_uppercase_extension(self, temp_dir, monkeypatch, sample_workflow_yaml):
7815+
from typer.testing import CliRunner
7816+
from specify_cli import app
7817+
7818+
(temp_dir / ".specify" / "workflows").mkdir(parents=True)
7819+
src = temp_dir / "Sample.YAML"
7820+
src.write_text(sample_workflow_yaml, encoding="utf-8")
7821+
7822+
monkeypatch.chdir(temp_dir)
7823+
result = CliRunner().invoke(app, ["workflow", "add", "--dev", str(src)])
7824+
7825+
# Before the fix the --dev branch rejects ".YAML" with
7826+
# "--dev source must be a workflow YAML file ...".
7827+
assert result.exit_code == 0, result.output
7828+
assert "installed" in result.output
7829+
7830+
def test_lowercase_extension_still_installs(self, temp_dir, monkeypatch, sample_workflow_yaml):
7831+
"""Happy path (lowercase .yml) is unchanged by the case-normalization."""
7832+
from typer.testing import CliRunner
7833+
from specify_cli import app
7834+
7835+
(temp_dir / ".specify" / "workflows").mkdir(parents=True)
7836+
src = temp_dir / "sample.yml"
7837+
src.write_text(sample_workflow_yaml, encoding="utf-8")
7838+
7839+
monkeypatch.chdir(temp_dir)
7840+
result = CliRunner().invoke(app, ["workflow", "add", str(src)])
7841+
7842+
assert result.exit_code == 0, result.output
7843+
assert "installed" in result.output
7844+
7845+
77897846
class TestWorkflowAddSymlinkGuard:
77907847
def test_add_malformed_ipv6_url_exits_cleanly(self, temp_dir, monkeypatch):
77917848
"""A malformed IPv6 URL must produce a clean error, not a ValueError traceback."""

0 commit comments

Comments
 (0)