Skip to content

fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition#3694

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/engine-workflow-block-non-mapping-guard
Open

fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition#3694
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/engine-workflow-block-non-mapping-guard

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

What

A present-but-non-mapping top-level workflow: block crashed WorkflowDefinition.__init__ with AttributeError:

  • bare workflow: → YAML null
  • workflow: some-stringstr
  • workflow: [a, b]list

workflow = data.get("workflow", {}) only substitutes {} when the key is absent, so a present non-dict value reached workflow.get("id", "")AttributeError: 'NoneType'/'str'/'list' object has no attribute 'get'.

This fires inside from_yaml/from_stringbefore validate_workflow can report the malformed shape — and in the CLI (workflow run/resume, whose load_workflow is wrapped to catch only FileNotFoundError/ValueError) it escapes as a raw traceback instead of a clean "Invalid workflow" message.

Fix

Normalize the local workflow to {} when it isn't a mapping (self.data keeps the raw value so validate_workflow still reports it), mirroring the adjacent default_options guard (if not isinstance(...): = {}).

Tests

tests/test_workflows.py::TestWorkflowDefinition::test_non_mapping_workflow_block_parses_then_validates (parametrized null/str/list) — construction no longer raises, and validate_workflow reports the missing workflow.id. Fails before the fix (all three crash). ruff clean.


AI-assisted: authored with Claude Code. Verified against the default_options sibling guard and confirmed fail-before/pass-after, incl. self.data round-trip.

…tion

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents malformed workflow: blocks from crashing workflow parsing.

Changes:

  • Normalizes non-mapping workflow headers locally.
  • Adds regression coverage for null, string, and list values.
Show a summary per file
File Description
src/specify_cli/workflows/engine.py Guards workflow header access.
tests/test_workflows.py Tests malformed header handling.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread tests/test_workflows.py
errors = validate_workflow(definition)
assert any("workflow.id" in e for e in errors)
# The raw value is preserved on .data for validation/inspection.
assert "workflow" in definition.data
@mnriem

mnriem commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants