Skip to content

workflow add crashes with a traceback when workflow.yml scalars are not strings #3420

Closed

Description

@marcelsafin

Description

validate_workflow() and the workflow add local-install path assume every scalar in workflow.yml is a string. YAML parses unquoted values like 1.0 and 123 as float/int, so a small authoring mistake crashes the CLI with a raw traceback instead of a validation error.

Reproduction

Tested on current main (0.12.9.dev0).

1. Unquoted version — version: 1.0 instead of version: "1.0.0":

workflow:
  id: probe
  name: Probe
  version: 1.0
File "src/specify_cli/workflows/engine.py", line 152, in validate_workflow
  elif not re.match(r"^\d+\.\d+\.\d+$", definition.version):
TypeError: expected string or bytes-like object, got 'float'

2. Numeric workflow id — id: 123:

File "src/specify_cli/workflows/_commands.py", line 604, in _validate_and_install_local
  if not definition.id or not definition.id.strip():
AttributeError: 'int' object has no attribute 'strip'

3. Numeric step id — steps: [{id: 123, type: shell, run: echo hi}]:

File "src/specify_cli/workflows/engine.py", line 263, in _validate_steps
  if ":" in step_id:
TypeError: argument of type 'int' is not iterable

Related: schema_version: 1.0 (unquoted) doesn't crash but produces a confusing message: Unsupported schema_version 1.0. Expected '1.0'. — the float renders identically to the accepted string.

Expected

Clean validation errors from workflow validate/workflow add, e.g. Workflow version 1.0 must be a string — quote it in YAML (version: "1.0.0").

Root cause

validate_workflow() in src/specify_cli/workflows/engine.py regex-matches and iterates definition.id, definition.version and step id values without checking they are strings first. Same for the definition.id.strip() call in _validate_and_install_local. Since this is the shared validation path, the crash reaches workflow add, workflow validate and catalog installs alike.

I have a fix ready and will open a PR.

Activity

  1. github-actions commented on Jul 10, 2026

    @github-actions
    Contributor

    Bug assessment — yaml-scalar-type-crash: Valid · severity medium


    Bug Assessment: workflow add crashes with traceback when YAML scalars are not strings

    Report (summarized)

    Reported by @marcelsafin on 2026-07-08. When a workflow.yml contains unquoted numeric YAML values (e.g. version: 1.0, id: 123, or steps: [{id: 123, ...}]), the CLI crashes with a raw Python traceback instead of emitting a clean validation error. Three distinct crash sites are identified, plus a fourth edge case that produces a misleading (but non-crashing) message.

    Symptom

    When YAML parses unquoted scalars as float or int, validate_workflow() and the workflow add local-install path call string-only operations (re.match, str.strip, in containment) on those values without first checking their type, producing uncaught TypeError / AttributeError exceptions. The user sees a raw traceback instead of a friendly validation message.

    Reproduction

    1. Create a workflow.yml with version: 1.0 (unquoted float):
      workflow:
        id: probe
        name: Probe
        version: 1.0
    2. Run specify workflow add <path> or specify workflow validate <path>.
    3. Actual: TypeError: expected string or bytes-like object, got 'float' at engine.py:152.
    4. Expected: Workflow version must be a string — quote it in YAML (version: "1.0.0").

    Variant A — id: 123: AttributeError: 'int' object has no attribute 'strip' at _commands.py:604.

    Variant B — step id: 123: TypeError: argument of type 'int' is not iterable at engine.py:263.

    Variant C — schema_version: 1.0: no crash but prints Unsupported schema_version 1.0. Expected '1.0'. — the float renders identically to the accepted string, so the message is confusing but technically correct.

    Suspected Code Paths

    • src/specify_cli/workflows/engine.py:141 — _ID_PATTERN.match(definition.id) is called after if not definition.id: but with no isinstance(str, ...) guard; a non-string truthy value (e.g. 123) passes the falsy check and then crashes re.match.
    • src/specify_cli/workflows/engine.py:152 — re.match(r"^\d+\.\d+\.\d+$", definition.version) crashes when definition.version is a float.
    • src/specify_cli/workflows/engine.py:263 — if ":" in step_id: crashes when step_id is an int (containment test on non-string).
    • src/specify_cli/workflows/_commands.py:604 — definition.id.strip() crashes when definition.id is an int.
    • src/specify_cli/workflows/engine.py:132 — definition.schema_version not in ("1.0", "1") — does not crash but yields the misleading float-vs-string mismatch message.

    Root Cause Hypothesis

    validate_workflow() and the pre-validation guard in _validate_and_install_local assume all top-level and step scalar fields are already str after YAML parsing. PyYAML's default safe_load follows the YAML 1.1 core schema and silently coerces unquoted 1.0 → float and 123 → int. No type guard exists at any of the four call sites before string operations are performed. Confidence: high — all crash sites are confirmed by the reporter's tracebacks and by reading the current source.

    Proposed Remediation

    Preferred: Add isinstance checks at the top of validate_workflow() for definition.id, definition.version, and definition.schema_version, and at the top of _validate_steps() for each step_id, before any string operation is attempted. When the check fails, append a human-friendly error message (e.g. "Workflow 'id' must be a string — got int; quote it in YAML.") and continue or return errors early so the rest of validation still runs where safe. The pre-validation guard in _validate_and_install_local (definition.id.strip()) should also be guarded or removed (it is redundant since validate_workflow already checks definition.id).

    Alternatives:

    • Coerce all top-level scalars to str immediately after WorkflowDefinition.from_yaml() (e.g. in the dataclass constructor or from_yaml). Trade-off: silently accepts bad input without telling the author what was wrong; also changes the repr in error messages.
    • Switch to yaml.load(..., Loader=yaml.BaseLoader) which leaves all scalars as strings. Trade-off: would change how booleans and numbers in inputs.default are handled; wider blast radius.

    Files likely to change:

    • src/specify_cli/workflows/engine.py — add isinstance(str) guards in validate_workflow() (lines ~139–155) and _validate_steps() (line ~263).
    • src/specify_cli/workflows/_commands.py — remove or guard the redundant definition.id.strip() pre-check at line 604.
    • tests/workflows/test_engine.py (or equivalent) — add test cases for each non-string scalar variant.

    Tests to add or update:

    • test_validate_workflow_float_version — version: 1.0 (float) → validation error, no exception.
    • test_validate_workflow_int_id — id: 123 (int) → validation error, no exception.
    • test_validate_steps_int_step_id — step id: 123 (int) → validation error, no exception.
    • test_validate_schema_version_float — schema_version: 1.0 → clear "must be a string" error rather than float-renders-as-string confusion.

    Risks & Considerations

    • Low regression risk: the change tightens validation, not behavior. Workflows with valid string scalars are unaffected.
    • The fix should ensure all four validation paths emit errors rather than raising, so a single YAML file with multiple type mistakes surfaces all of them at once.
    • No API breakage: validate_workflow already returns list[str]; callers handle a non-empty list as failure.

    Open Questions

    • None — the report is self-contained and the code confirms all four crash sites.

    Posted on behalf of @mnriem by GitHub Copilot (model: claude-sonnet-4.6, autonomous)

    Generated by 🐛 Assess Bug from Labeled Issue for issue #3420 · 73 AIC · ⌖ 11.2 AIC · ⊞ 31.8K · ◷

  2. added a commit that references this issue on Jul 10, 2026
    34514fb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions