Skip to content

Commit fb796c2

Browse files
jawwad-aliclaude
andauthored
fix(workflows): shell step validate() rejects non-string run (#3348)
ShellStep.validate() only checked that 'run' was present, so run: (null) or a GitHub-Actions-style list validated clean; execute() then str()-coerces the value and invokes it under shell=True, literally running 'None' or "['echo', 'hi']" as a command. Add a type check after the presence check, mirroring the command-step (#3262) and gate options validation. Expression strings ('{{ ... }}') are strings, so they stay valid. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 0151d23 commit fb796c2

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

src/specify_cli/workflows/steps/shell/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ def validate(self, config: dict[str, Any]) -> list[str]:
9090
errors.append(
9191
f"Shell step {config.get('id', '?')!r} is missing 'run' field."
9292
)
93+
elif not isinstance(config["run"], str):
94+
# execute() str()-coerces run and invokes it under shell=True, so a
95+
# null or list 'run' would run the Python repr ('None', "['echo']")
96+
# as a command. Reject non-strings at validation, mirroring the
97+
# command-step input/options and gate options type checks. An
98+
# expression like "{{ ... }}" is still a str, so it stays valid.
99+
errors.append(
100+
f"Shell step {config.get('id', '?')!r}: 'run' must be a string, "
101+
f"got {type(config['run']).__name__}."
102+
)
93103
output_format = config.get("output_format")
94104
if output_format is not None and output_format != "json":
95105
errors.append(

tests/test_workflows.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,26 @@ def test_validate_missing_run(self):
12661266
errors = step.validate({"id": "test"})
12671267
assert any("missing 'run'" in e for e in errors)
12681268

1269+
@pytest.mark.parametrize("bad_run", [None, ["echo", "hi"], 42])
1270+
def test_validate_rejects_non_string_run(self, bad_run):
1271+
"""A non-string 'run' must be rejected at validation.
1272+
1273+
execute() str()-coerces run and invokes it under shell=True, so a
1274+
null or list run would otherwise run the Python repr as a command.
1275+
"""
1276+
from specify_cli.workflows.steps.shell import ShellStep
1277+
1278+
step = ShellStep()
1279+
errors = step.validate({"id": "s", "run": bad_run})
1280+
assert any("'run' must be a string" in e for e in errors)
1281+
1282+
def test_validate_accepts_string_and_expression_run(self):
1283+
from specify_cli.workflows.steps.shell import ShellStep
1284+
1285+
step = ShellStep()
1286+
assert step.validate({"id": "s", "run": "echo hi"}) == []
1287+
assert step.validate({"id": "s", "run": "{{ steps.x.output }}"}) == []
1288+
12691289

12701290
def test_output_format_json_exposes_data(self, tmp_path):
12711291
from specify_cli.workflows.steps.shell import ShellStep

0 commit comments

Comments
 (0)