Skip to content

Commit 54b0564

Browse files
jawwad-aliclaude
andcommitted
fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition
A present-but-non-mapping top-level `workflow:` block (bare `workflow:` -> YAML null, or `workflow: <str>` / `workflow: [..]`) crashed WorkflowDefinition.__init__ with AttributeError: the `{}` default of `data.get("workflow", {})` only applies when the key is ABSENT, so a non-dict value reached `workflow.get("id", ...)`. This fires inside from_yaml/ from_string — before validate_workflow can report the malformed shape — and in the CLI escapes as a raw traceback (load_workflow is wrapped to catch only FileNotFoundError/ValueError). Normalize the local `workflow` to {} when it is not a mapping (self.data keeps the raw value so validate_workflow still reports it), mirroring the adjacent default_options guard. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9b3546f commit 54b0564

2 files changed

Lines changed: 25 additions & 0 deletions

File tree

src/specify_cli/workflows/engine.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ def __init__(self, data: dict[str, Any], source_path: Path | None = None) -> Non
4242
self.source_path = source_path
4343

4444
workflow = data.get("workflow", {})
45+
# A present-but-non-mapping ``workflow:`` block (bare ``workflow:`` ->
46+
# None, or ``workflow: <str/list>``) would crash the following
47+
# ``workflow.get(...)`` calls with AttributeError before validate can
48+
# report the malformed shape. Normalize the local to {} (self.data
49+
# keeps the raw value for validate_workflow), mirroring the
50+
# default_options guard below.
51+
if not isinstance(workflow, dict):
52+
workflow = {}
4553
self.id: str = workflow.get("id", "")
4654
self.name: str = workflow.get("name", "")
4755
self.version: str = workflow.get("version", "0.0.0")

tests/test_workflows.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3590,6 +3590,23 @@ def test_from_string(self, sample_workflow_yaml):
35903590
assert definition.id == "test-workflow"
35913591
assert len(definition.inputs) == 2
35923592

3593+
@pytest.mark.parametrize(
3594+
"block", ["workflow:\nsteps: []\n", "workflow: hi\nsteps: []\n", "workflow: [a]\nsteps: []\n"]
3595+
)
3596+
def test_non_mapping_workflow_block_parses_then_validates(self, block):
3597+
# A present-but-non-mapping `workflow:` block must not crash construction
3598+
# with AttributeError; it should parse (empty header) and let
3599+
# validate_workflow report the missing id/name, mirroring how the other
3600+
# raw fields are validated later.
3601+
from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow
3602+
3603+
definition = WorkflowDefinition.from_string(block) # must not raise
3604+
assert definition.id == ""
3605+
errors = validate_workflow(definition)
3606+
assert any("workflow.id" in e for e in errors)
3607+
# The raw value is preserved on .data for validation/inspection.
3608+
assert "workflow" in definition.data
3609+
35933610
def test_from_string_invalid(self):
35943611
from specify_cli.workflows.engine import WorkflowDefinition
35953612

0 commit comments

Comments
 (0)