-
Notifications
You must be signed in to change notification settings - Fork 9.6k
feat(workflows): add JSON output for workflow run resume and status #2814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
94f33e6
60f36d4
abfa237
ea95653
9575f2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4174,19 +4174,46 @@ def extension_set_priority( | |
| workflow_app.add_typer(workflow_catalog_app, name="catalog") | ||
|
|
||
|
|
||
| def _workflow_run_payload(state: Any) -> dict[str, Any]: | ||
| """Machine-readable summary of a run/resume outcome.""" | ||
| return { | ||
| "run_id": state.run_id, | ||
| "workflow_id": state.workflow_id, | ||
| "status": state.status.value, | ||
| "current_step_id": state.current_step_id, | ||
| "current_step_index": state.current_step_index, | ||
| } | ||
|
|
||
|
|
||
| def _emit_workflow_json(payload: dict[str, Any]) -> None: | ||
| """Write a workflow payload as machine-readable JSON to stdout. | ||
|
|
||
| Uses the builtin ``print`` rather than ``console.print`` so Rich | ||
| markup interpretation, syntax highlighting, and line-wrapping can | ||
| never alter the emitted JSON. | ||
| """ | ||
| print(json.dumps(payload, indent=2)) | ||
|
|
||
|
|
||
| @workflow_app.command("run") | ||
| def workflow_run( | ||
| source: str = typer.Argument(..., help="Workflow ID or YAML file path"), | ||
| input_values: list[str] | None = typer.Option( | ||
| None, "--input", "-i", help="Input values as key=value pairs" | ||
| ), | ||
| json_output: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Emit the run outcome as a single JSON object instead of formatted text.", | ||
| ), | ||
| ): | ||
| """Run a workflow from an installed ID or local YAML path.""" | ||
| from .workflows.engine import WorkflowEngine | ||
|
|
||
| project_root = _require_specify_project() | ||
| engine = WorkflowEngine(project_root) | ||
| engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026") | ||
| if not json_output: | ||
| engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — fixed in abfa237. While |
||
|
|
||
| try: | ||
| definition = engine.load_workflow(source) | ||
|
|
@@ -4215,8 +4242,9 @@ def workflow_run( | |
| key, _, value = kv.partition("=") | ||
| inputs[key.strip()] = value.strip() | ||
|
|
||
| console.print(f"\n[bold cyan]Running workflow:[/bold cyan] {definition.name} ({definition.id})") | ||
| console.print(f"[dim]Version: {definition.version}[/dim]\n") | ||
| if not json_output: | ||
| console.print(f"\n[bold cyan]Running workflow:[/bold cyan] {definition.name} ({definition.id})") | ||
| console.print(f"[dim]Version: {definition.version}[/dim]\n") | ||
|
|
||
| try: | ||
| state = engine.execute(definition, inputs) | ||
|
|
@@ -4227,6 +4255,10 @@ def workflow_run( | |
| console.print(f"[red]Workflow failed:[/red] {exc}") | ||
| raise typer.Exit(1) | ||
|
|
||
| if json_output: | ||
| _emit_workflow_json(_workflow_run_payload(state)) | ||
| return | ||
|
|
||
| status_colors = { | ||
| "completed": "green", | ||
| "paused": "yellow", | ||
|
|
@@ -4244,13 +4276,19 @@ def workflow_run( | |
| @workflow_app.command("resume") | ||
| def workflow_resume( | ||
| run_id: str = typer.Argument(..., help="Run ID to resume"), | ||
| json_output: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Emit the resume outcome as a single JSON object instead of formatted text.", | ||
| ), | ||
| ): | ||
| """Resume a paused or failed workflow run.""" | ||
| from .workflows.engine import WorkflowEngine | ||
|
|
||
| project_root = _require_specify_project() | ||
| engine = WorkflowEngine(project_root) | ||
| engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026") | ||
| if not json_output: | ||
| engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026") | ||
|
|
||
| try: | ||
| state = engine.resume(run_id) | ||
|
|
@@ -4264,6 +4302,10 @@ def workflow_resume( | |
| console.print(f"[red]Resume failed:[/red] {exc}") | ||
| raise typer.Exit(1) | ||
|
|
||
| if json_output: | ||
| _emit_workflow_json(_workflow_run_payload(state)) | ||
| return | ||
|
|
||
| status_colors = { | ||
| "completed": "green", | ||
| "paused": "yellow", | ||
|
|
@@ -4277,6 +4319,11 @@ def workflow_resume( | |
| @workflow_app.command("status") | ||
| def workflow_status( | ||
| run_id: str | None = typer.Argument(None, help="Run ID to inspect (shows all if omitted)"), | ||
| json_output: bool = typer.Option( | ||
| False, | ||
| "--json", | ||
| help="Emit run status as a single JSON object instead of formatted text.", | ||
| ), | ||
| ): | ||
| """Show workflow run status.""" | ||
| from .workflows.engine import WorkflowEngine | ||
|
|
@@ -4292,6 +4339,22 @@ def workflow_status( | |
| console.print(f"[red]Error:[/red] Run not found: {run_id}") | ||
| raise typer.Exit(1) | ||
|
|
||
| if json_output: | ||
| payload = { | ||
| "run_id": state.run_id, | ||
| "workflow_id": state.workflow_id, | ||
| "status": state.status.value, | ||
| "created_at": state.created_at, | ||
| "updated_at": state.updated_at, | ||
| "current_step_id": state.current_step_id, | ||
| "steps": { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed: the single-run |
||
| sid: sd.get("status", "unknown") | ||
| for sid, sd in state.step_results.items() | ||
| }, | ||
| } | ||
| _emit_workflow_json(payload) | ||
| return | ||
|
|
||
| status_colors = { | ||
| "completed": "green", | ||
| "paused": "yellow", | ||
|
|
@@ -4319,6 +4382,22 @@ def workflow_status( | |
| console.print(f" [{sc}]●[/{sc}] {step_id}: {s}") | ||
| else: | ||
| runs = engine.list_runs() | ||
|
|
||
| if json_output: | ||
| payload = { | ||
| "runs": [ | ||
| { | ||
| "run_id": r["run_id"], | ||
| "workflow_id": r.get("workflow_id"), | ||
| "status": r.get("status", "unknown"), | ||
| "updated_at": r.get("updated_at"), | ||
| } | ||
| for r in runs | ||
| ] | ||
| } | ||
| _emit_workflow_json(payload) | ||
| return | ||
|
|
||
| if not runs: | ||
| console.print("[yellow]No workflow runs found.[/yellow]") | ||
| return | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed: the example now uses
my-pipeline.ymlwith"workflow_id": "build-and-review"and a one-line note thatworkflow_idis the YAMLworkflow.id, not the file name.