Files
spec-kit/src/specify_cli/workflows/base.py
T
675143591d feat: bind gate verdict to workflow input via verdict_input (#3725)
* feat: bind gate verdict to workflow input via verdict_input

Add an optional `verdict_input` field to gate steps that lets an
external system supply a verdict through a declared workflow input
instead of an interactive TTY prompt.

When the referenced input carries a non-empty string value that
matches one of the gate's `options` (case-insensitive), the gate
auto-decides, records the matched spelling in `output.choice`, and
applies the existing `on_reject` / abort / skip / retry semantics.
If the value is present but does not match an option, or is a
non-string, the gate fails immediately with a clear error message.
When the input is absent, null, or empty, the gate falls back to
today's TTY-prompt-or-pause behaviour unchanged.

The engine now persists `result.error` alongside each step's status
and output so that failed-step error messages survive across runs.
The CLI (`workflow run` and `workflow resume`) surfaces these
persisted errors after a failed or aborted run. `validate_workflow`
cross-references `verdict_input` against the workflow's declared
inputs block and reports an error for undeclared names, consistent
with the existing `wait_for` id cross-check.

Closes discussion: https://github.com/github/spec-kit/discussions/3717

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix workflow JSON error payloads

Include persisted step errors in _workflow_run_payload so workflow run/resume/status --json all surface failure reasons consistently. Add JSON-path tests for failed and successful runs.

Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(workflows): reject verdict inputs in fan-out

Fan-out items share workflow inputs and cannot safely consume a bound gate verdict. Reject verdict_input bindings during validation and at runtime while preserving unbound gates.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: update workflow command handling

Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Markus <markus@example.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-30 07:39:49 -05:00

147 lines
4.3 KiB
Python

"""Base classes for workflow step types.
Provides:
- ``StepBase`` — abstract base every step type must implement.
- ``StepContext`` — execution context passed to each step.
- ``StepResult`` — return value from step execution.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class StepStatus(str, Enum):
"""Status of a step execution."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
SKIPPED = "skipped"
PAUSED = "paused"
class RunStatus(str, Enum):
"""Status of a workflow run."""
CREATED = "created"
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
FAILED = "failed"
ABORTED = "aborted"
@dataclass
class StepContext:
"""Execution context passed to each step.
Contains everything the step needs to resolve expressions, dispatch
commands, and record results.
"""
#: Resolved workflow inputs (from user prompts / defaults).
inputs: dict[str, Any] = field(default_factory=dict)
#: Accumulated step results keyed by step ID. Each entry is the dict the
#: engine persists per step:
#: ``{"type": ..., "integration": ..., "model": ..., "options": ...,
#: "input": ..., "output": ..., "status": ...}``.
steps: dict[str, dict[str, Any]] = field(default_factory=dict)
#: Current fan-out item (set only inside fan-out iterations).
item: Any = None
#: Whether the current step is executing inside a fan-out template.
inside_fan_out: bool = False
#: Fan-in aggregated results (set only for fan-in steps).
fan_in: dict[str, Any] = field(default_factory=dict)
#: Workflow-level default integration key.
default_integration: str | None = None
#: Workflow-level default model.
default_model: str | None = None
#: Workflow-level default options.
default_options: dict[str, Any] = field(default_factory=dict)
#: Project root path.
project_root: str | None = None
#: Current run ID.
run_id: str | None = None
#: Source directory of the workflow definition file.
workflow_dir: str | None = None
@dataclass
class StepResult:
"""Return value from a step execution."""
#: Step status.
status: StepStatus = StepStatus.COMPLETED
#: Output data (stored as ``steps.<id>.output``).
output: dict[str, Any] = field(default_factory=dict)
#: Nested steps to execute (for control-flow steps like if/then).
next_steps: list[dict[str, Any]] = field(default_factory=list)
#: Error message if step failed.
error: str | None = None
class StepBase(ABC):
"""Abstract base class for workflow step types.
Every step type — built-in or extension-provided — implements this
interface and registers in ``STEP_REGISTRY``.
Thread-safety: ``STEP_REGISTRY`` holds a single shared instance per type, so
a concurrent ``fan-out`` (``max_concurrency > 1``) can invoke ``execute`` on
the same instance from several threads at once. Implementations must be
stateless / thread-safe — derive all per-run state from the ``config`` and
``context`` arguments and never mutate ``self`` in ``execute``. The built-in
steps follow this rule.
"""
#: Matches the ``type:`` value in workflow YAML.
type_key: str = ""
@abstractmethod
def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
"""Execute the step with the given config and context.
Parameters
----------
config:
The step configuration from workflow YAML.
context:
The execution context with inputs, accumulated step results, etc.
Returns
-------
StepResult with status, output data, and optional nested steps.
"""
def validate(self, config: dict[str, Any]) -> list[str]:
"""Validate step configuration and return a list of error messages.
An empty list means the configuration is valid.
"""
errors: list[str] = []
if "id" not in config:
errors.append("Step is missing required 'id' field.")
return errors
def can_resume(self, state: dict[str, Any]) -> bool:
"""Return whether this step can be resumed from the given state."""
return True