omniship 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. omniship-0.1.0/PKG-INFO +122 -0
  2. omniship-0.1.0/README.md +107 -0
  3. omniship-0.1.0/pyproject.toml +37 -0
  4. omniship-0.1.0/pyproject.toml.orig +36 -0
  5. omniship-0.1.0/src/omniship/__init__.py +2 -0
  6. omniship-0.1.0/src/omniship/cli/app.py +29 -0
  7. omniship-0.1.0/src/omniship/cli/build.py +16 -0
  8. omniship-0.1.0/src/omniship/cli/check.py +14 -0
  9. omniship-0.1.0/src/omniship/cli/plan.py +65 -0
  10. omniship-0.1.0/src/omniship/cli/plugins.py +73 -0
  11. omniship-0.1.0/src/omniship/cli/runner.py +68 -0
  12. omniship-0.1.0/src/omniship/cli/ship.py +23 -0
  13. omniship-0.1.0/src/omniship/cli/ui.py +57 -0
  14. omniship-0.1.0/src/omniship/config/loader.py +56 -0
  15. omniship-0.1.0/src/omniship/config/models.py +23 -0
  16. omniship-0.1.0/src/omniship/config/validation.py +62 -0
  17. omniship-0.1.0/src/omniship/core/artifact.py +52 -0
  18. omniship-0.1.0/src/omniship/core/context.py +24 -0
  19. omniship-0.1.0/src/omniship/core/executor.py +263 -0
  20. omniship-0.1.0/src/omniship/core/graph.py +108 -0
  21. omniship-0.1.0/src/omniship/core/node.py +26 -0
  22. omniship-0.1.0/src/omniship/core/result.py +34 -0
  23. omniship-0.1.0/src/omniship/core/stage.py +7 -0
  24. omniship-0.1.0/src/omniship/operations/__init__.py +14 -0
  25. omniship-0.1.0/src/omniship/operations/command.py +172 -0
  26. omniship-0.1.0/src/omniship/operations/github.py +188 -0
  27. omniship-0.1.0/src/omniship/operations/noop.py +41 -0
  28. omniship-0.1.0/src/omniship/plugins/api.py +25 -0
  29. omniship-0.1.0/src/omniship/plugins/discovery.py +39 -0
  30. omniship-0.1.0/src/omniship/plugins/metadata.py +13 -0
  31. omniship-0.1.0/src/omniship/plugins/registry.py +42 -0
  32. omniship-0.1.0/src/omniship/plugins/template.py +38 -0
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.3
2
+ Name: omniship
3
+ Version: 0.1.0
4
+ Summary: Universal release orchestration
5
+ Author: Ata Sesli
6
+ Requires-Dist: click>=8.5.0
7
+ Requires-Dist: pydantic>=2.13.5
8
+ Requires-Dist: pyyaml>=6.0.3
9
+ Requires-Dist: rich>=15.0.0
10
+ Requires-Python: >=3.14
11
+ Project-URL: Homepage, https://github.com/0ctacity/omniship
12
+ Project-URL: Repository, https://github.com/0ctacity/omniship
13
+ Project-URL: Issues, https://github.com/0ctacity/omniship/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # OmniShip (v0.1)
17
+
18
+ OmniShip is a plugin-driven DAG executor designed around three strict stage boundaries:
19
+ `Check` → `Build` → `Ship`.
20
+
21
+ ---
22
+
23
+ ## Architecture
24
+
25
+ 1. **Strict Stage Boundaries**:
26
+ - `CHECK`: Unit tests, integration tests, linting, code preparation.
27
+ - `BUILD`: Compiles binaries, bundles assets, produces named `Artifact`s.
28
+ - `SHIP`: Consumes produced artifacts, publishes releases, notifies destinations.
29
+ - All nodes in a stage must succeed before the next stage starts.
30
+
31
+ 2. **Async DAG Execution**:
32
+ - Sequential dependencies (`needs: [...]`)
33
+ - Concurrent parallel fan-out
34
+ - Fan-in barriers
35
+ - Automatic cycle detection
36
+ - Failure propagation & downstream skipping
37
+
38
+ 3. **First-Class Artifacts**:
39
+ - Build operations produce declared artifacts.
40
+ - Validated on disk, indexed in `ArtifactSet`, and made available to the `Ship` stage.
41
+
42
+ 4. **Plugin Architecture**:
43
+ - Standard Python package entry-point discovery: `omniship.plugins`.
44
+ - First-party operations use the exact same registration protocol as third-party plugins.
45
+ - Built-ins: `core/command`, `core/noop`, `github/release`.
46
+
47
+ ---
48
+
49
+ ## Configuration (`omniship.yaml`)
50
+
51
+ ```yaml
52
+ version: 1
53
+
54
+ check:
55
+ prepare:
56
+ uses: core/command
57
+ with:
58
+ run: ./scripts/prepare.sh
59
+
60
+ test-a:
61
+ uses: core/command
62
+ needs: [prepare]
63
+ with:
64
+ run: pytest tests/test_a.py
65
+
66
+ test-b:
67
+ uses: core/command
68
+ needs: [prepare]
69
+ with:
70
+ run: pytest tests/test_b.py
71
+
72
+ build:
73
+ linux:
74
+ uses: core/command
75
+ with:
76
+ run: ./scripts/build-linux.sh
77
+ artifacts:
78
+ - dist/linux/app
79
+
80
+ macos:
81
+ uses: core/command
82
+ with:
83
+ run: ./scripts/build-macos.sh
84
+ artifacts:
85
+ - dist/macos/app
86
+
87
+ ship:
88
+ github:
89
+ uses: github/release
90
+ with:
91
+ repository: octacity/example
92
+ tag: v0.1.0
93
+ dry_run: true
94
+ ```
95
+
96
+ ---
97
+
98
+ ## CLI Usage
99
+
100
+ ```bash
101
+ # Run Check only
102
+ omniship check
103
+
104
+ # Run Check → Build
105
+ omniship build
106
+
107
+ # Run Build only (skip Check)
108
+ omniship build --skip-check
109
+
110
+ # Run full flow: Check → Build → Ship
111
+ omniship ship
112
+
113
+ # Inspect the execution DAG
114
+ omniship plan
115
+
116
+ # List registered plugins & operations
117
+ omniship plugins
118
+
119
+ # View operation details or generate YAML config template
120
+ omniship plugin show core/command
121
+ omniship plugin template core/command
122
+ ```
@@ -0,0 +1,107 @@
1
+ # OmniShip (v0.1)
2
+
3
+ OmniShip is a plugin-driven DAG executor designed around three strict stage boundaries:
4
+ `Check` → `Build` → `Ship`.
5
+
6
+ ---
7
+
8
+ ## Architecture
9
+
10
+ 1. **Strict Stage Boundaries**:
11
+ - `CHECK`: Unit tests, integration tests, linting, code preparation.
12
+ - `BUILD`: Compiles binaries, bundles assets, produces named `Artifact`s.
13
+ - `SHIP`: Consumes produced artifacts, publishes releases, notifies destinations.
14
+ - All nodes in a stage must succeed before the next stage starts.
15
+
16
+ 2. **Async DAG Execution**:
17
+ - Sequential dependencies (`needs: [...]`)
18
+ - Concurrent parallel fan-out
19
+ - Fan-in barriers
20
+ - Automatic cycle detection
21
+ - Failure propagation & downstream skipping
22
+
23
+ 3. **First-Class Artifacts**:
24
+ - Build operations produce declared artifacts.
25
+ - Validated on disk, indexed in `ArtifactSet`, and made available to the `Ship` stage.
26
+
27
+ 4. **Plugin Architecture**:
28
+ - Standard Python package entry-point discovery: `omniship.plugins`.
29
+ - First-party operations use the exact same registration protocol as third-party plugins.
30
+ - Built-ins: `core/command`, `core/noop`, `github/release`.
31
+
32
+ ---
33
+
34
+ ## Configuration (`omniship.yaml`)
35
+
36
+ ```yaml
37
+ version: 1
38
+
39
+ check:
40
+ prepare:
41
+ uses: core/command
42
+ with:
43
+ run: ./scripts/prepare.sh
44
+
45
+ test-a:
46
+ uses: core/command
47
+ needs: [prepare]
48
+ with:
49
+ run: pytest tests/test_a.py
50
+
51
+ test-b:
52
+ uses: core/command
53
+ needs: [prepare]
54
+ with:
55
+ run: pytest tests/test_b.py
56
+
57
+ build:
58
+ linux:
59
+ uses: core/command
60
+ with:
61
+ run: ./scripts/build-linux.sh
62
+ artifacts:
63
+ - dist/linux/app
64
+
65
+ macos:
66
+ uses: core/command
67
+ with:
68
+ run: ./scripts/build-macos.sh
69
+ artifacts:
70
+ - dist/macos/app
71
+
72
+ ship:
73
+ github:
74
+ uses: github/release
75
+ with:
76
+ repository: octacity/example
77
+ tag: v0.1.0
78
+ dry_run: true
79
+ ```
80
+
81
+ ---
82
+
83
+ ## CLI Usage
84
+
85
+ ```bash
86
+ # Run Check only
87
+ omniship check
88
+
89
+ # Run Check → Build
90
+ omniship build
91
+
92
+ # Run Build only (skip Check)
93
+ omniship build --skip-check
94
+
95
+ # Run full flow: Check → Build → Ship
96
+ omniship ship
97
+
98
+ # Inspect the execution DAG
99
+ omniship plan
100
+
101
+ # List registered plugins & operations
102
+ omniship plugins
103
+
104
+ # View operation details or generate YAML config template
105
+ omniship plugin show core/command
106
+ omniship plugin template core/command
107
+ ```
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "omniship"
3
+ version = "0.1.0"
4
+ description = "Universal release orchestration"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "click>=8.5.0",
9
+ "pydantic>=2.13.5",
10
+ "pyyaml>=6.0.3",
11
+ "rich>=15.0.0",
12
+ ]
13
+
14
+ [[project.authors]]
15
+ name = "Ata Sesli"
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/0ctacity/omniship"
19
+ Repository = "https://github.com/0ctacity/omniship"
20
+ Issues = "https://github.com/0ctacity/omniship/issues"
21
+
22
+ [project.scripts]
23
+ omniship = "omniship.cli.app:cli"
24
+
25
+ [project.entry-points."omniship.plugins"]
26
+ core = "omniship.operations:register_core_plugin"
27
+ github = "omniship.operations.github:register_github_plugin"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.12.9,<0.13.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest>=9.1.1",
36
+ "pytest-asyncio>=1.4.0",
37
+ ]
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "omniship"
3
+ version = "0.1.0"
4
+ description = "Universal release orchestration"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ata Sesli" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "click>=8.5.0",
12
+ "pydantic>=2.13.5",
13
+ "pyyaml>=6.0.3",
14
+ "rich>=15.0.0",
15
+ ]
16
+ [project.urls]
17
+ Homepage = "https://github.com/0ctacity/omniship"
18
+ Repository = "https://github.com/0ctacity/omniship"
19
+ Issues = "https://github.com/0ctacity/omniship/issues"
20
+
21
+ [project.scripts]
22
+ omniship = "omniship.cli.app:cli"
23
+
24
+ [project.entry-points."omniship.plugins"]
25
+ core = "omniship.operations:register_core_plugin"
26
+ github = "omniship.operations.github:register_github_plugin"
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.12.9,<0.13.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=9.1.1",
35
+ "pytest-asyncio>=1.4.0",
36
+ ]
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from omniship!")
@@ -0,0 +1,29 @@
1
+ import click
2
+
3
+ from omniship.cli.build import build_cmd
4
+ from omniship.cli.check import check_cmd
5
+ from omniship.cli.plan import plan_cmd
6
+ from omniship.cli.plugins import plugin_cmd, plugins_cmd
7
+ from omniship.cli.ship import ship_cmd
8
+
9
+
10
+ @click.group()
11
+ def cli() -> None:
12
+ """OmniShip: Next-generation release & build DAG executor."""
13
+ pass
14
+
15
+
16
+ cli.add_command(check_cmd, name="check")
17
+ cli.add_command(build_cmd, name="build")
18
+ cli.add_command(ship_cmd, name="ship")
19
+ cli.add_command(plan_cmd, name="plan")
20
+ cli.add_command(plugins_cmd, name="plugins")
21
+ cli.add_command(plugin_cmd, name="plugin")
22
+
23
+
24
+ def main() -> None:
25
+ cli()
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,16 @@
1
+ import sys
2
+ import click
3
+
4
+ from omniship.cli.runner import run_pipeline
5
+ from omniship.core.stage import Stage
6
+
7
+
8
+ @click.command()
9
+ @click.option("--skip-check", is_flag=True, default=False, help="Skip the Check stage")
10
+ @click.option("-c", "--config", "config_path", help="Path to configuration file")
11
+ def build_cmd(skip_check: bool, config_path: str | None) -> None:
12
+ """Run Check -> Build (or only Build if --skip-check)."""
13
+ stages = [Stage.BUILD] if skip_check else [Stage.CHECK, Stage.BUILD]
14
+ code = run_pipeline(stages, config_path=config_path)
15
+ if code != 0:
16
+ sys.exit(code)
@@ -0,0 +1,14 @@
1
+ import sys
2
+ import click
3
+
4
+ from omniship.cli.runner import run_pipeline
5
+ from omniship.core.stage import Stage
6
+
7
+
8
+ @click.command()
9
+ @click.option("-c", "--config", "config_path", help="Path to configuration file")
10
+ def check_cmd(config_path: str | None) -> None:
11
+ """Run only the Check stage."""
12
+ code = run_pipeline([Stage.CHECK], config_path=config_path)
13
+ if code != 0:
14
+ sys.exit(code)
@@ -0,0 +1,65 @@
1
+ import sys
2
+ from pathlib import Path
3
+ import click
4
+ from rich.console import Console
5
+ from rich.tree import Tree
6
+
7
+ from omniship.config.loader import find_config_file, load_and_validate
8
+ from omniship.core.graph import StageGraph
9
+ from omniship.core.stage import Stage
10
+ from omniship.plugins.discovery import load_plugins
11
+
12
+
13
+ def print_plan(graphs: dict[Stage, StageGraph], console: Console | None = None) -> None:
14
+ console = console or Console()
15
+
16
+ console.print("[bold]OmniShip Execution Plan[/bold]\n")
17
+
18
+ for stage in (Stage.CHECK, Stage.BUILD, Stage.SHIP):
19
+ graph = graphs.get(stage)
20
+ console.print(f"[bold cyan]{stage.value.upper()}[/bold cyan]")
21
+ if not graph or not graph.nodes:
22
+ console.print(" [dim](empty)[/dim]\n")
23
+ continue
24
+
25
+ roots = graph.get_roots()
26
+ if roots:
27
+ for root in sorted(roots, key=lambda n: n.id):
28
+ root_tree = Tree(f"[bold]{root.id}[/bold] [dim]({root.operation_name})[/dim]")
29
+ _add_children(root.id, root_tree, graph, set())
30
+ console.print(root_tree)
31
+ else:
32
+ for node_id, node in sorted(graph.nodes.items()):
33
+ console.print(f" • {node_id} [dim]({node.operation_name})[/dim]")
34
+ console.print()
35
+
36
+
37
+ def _add_children(node_id: str, tree: Tree, graph: StageGraph, visited: set[str]) -> None:
38
+ if node_id in visited:
39
+ return
40
+ visited.add(node_id)
41
+ dependents = sorted(graph.get_direct_dependents(node_id))
42
+ for dep_id in dependents:
43
+ node = graph.nodes[dep_id]
44
+ branch = tree.add(f"[bold]{node.id}[/bold] [dim]({node.operation_name})[/dim]")
45
+ _add_children(dep_id, branch, graph, visited.copy())
46
+
47
+
48
+ @click.command(name="plan")
49
+ @click.option("-c", "--config", "config_path", help="Path to configuration file")
50
+ def plan_cmd(config_path: str | None) -> None:
51
+ """Inspect the execution plan and dependency DAG."""
52
+ console = Console()
53
+ registry = load_plugins()
54
+ path = Path(config_path) if config_path else find_config_file()
55
+ if not path or not path.is_file():
56
+ console.print("[bold red]Error:[/bold red] No omniship.yaml configuration file found.")
57
+ sys.exit(1)
58
+
59
+ try:
60
+ _, graphs = load_and_validate(path, registry)
61
+ except Exception as exc:
62
+ console.print(f"[bold red]Configuration Error:[/bold red] {exc}")
63
+ sys.exit(1)
64
+
65
+ print_plan(graphs, console)
@@ -0,0 +1,73 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from omniship.plugins.discovery import load_plugins
6
+ from omniship.plugins.template import generate_operation_template
7
+
8
+
9
+ @click.group(name="plugins", invoke_without_command=True)
10
+ @click.pass_context
11
+ def plugins_cmd(ctx: click.Context) -> None:
12
+ """List available plugins and operations."""
13
+ if ctx.invoked_subcommand is None:
14
+ console = Console()
15
+ registry = load_plugins()
16
+ ops = registry.list_operations()
17
+
18
+ table = Table(title="Registered Operations")
19
+ table.add_column("Operation", style="bold cyan")
20
+ table.add_column("Stages", style="green")
21
+ table.add_column("Cacheable", style="magenta")
22
+ table.add_column("Description")
23
+
24
+ for op in sorted(ops, key=lambda x: x.name):
25
+ stages_str = ", ".join(s.value for s in sorted(op.stages))
26
+ table.add_row(
27
+ op.name,
28
+ stages_str,
29
+ "yes" if op.cacheable else "no",
30
+ op.description,
31
+ )
32
+
33
+ console.print(table)
34
+
35
+
36
+ @click.group(name="plugin")
37
+ def plugin_cmd() -> None:
38
+ """Inspect or generate templates for operations."""
39
+ pass
40
+
41
+
42
+ @plugin_cmd.command(name="show")
43
+ @click.argument("name")
44
+ def plugin_show(name: str) -> None:
45
+ """Show details for an operation."""
46
+ console = Console()
47
+ registry = load_plugins()
48
+ definition = registry.get_definition(name)
49
+ if not definition:
50
+ console.print(f"[bold red]Error:[/bold red] Operation '{name}' not found.")
51
+ return
52
+
53
+ console.print(f"[bold cyan]Operation:[/bold cyan] {definition.name}")
54
+ console.print(f"[bold]Description:[/bold] {definition.description}")
55
+ console.print(f"[bold]Allowed Stages:[/bold] {', '.join(s.value for s in definition.stages)}")
56
+ console.print(f"[bold]Cacheable:[/bold] {'yes' if definition.cacheable else 'no'}")
57
+ if definition.config_model:
58
+ console.print("\n[bold]Configuration Template:[/bold]")
59
+ console.print(generate_operation_template(definition))
60
+
61
+
62
+ @plugin_cmd.command(name="template")
63
+ @click.argument("name")
64
+ def plugin_template(name: str) -> None:
65
+ """Generate example YAML configuration for an operation."""
66
+ console = Console()
67
+ registry = load_plugins()
68
+ definition = registry.get_definition(name)
69
+ if not definition:
70
+ console.print(f"[bold red]Error:[/bold red] Operation '{name}' not found.")
71
+ return
72
+
73
+ console.print(generate_operation_template(definition), end="")
@@ -0,0 +1,68 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ from rich.console import Console
5
+
6
+ from omniship.cli.ui import TerminalUI
7
+ from omniship.config.loader import find_config_file, load_and_validate
8
+ from omniship.core.context import ExecutionContext
9
+ from omniship.core.executor import StageExecutor
10
+ from omniship.core.stage import Stage
11
+ from omniship.plugins.discovery import load_plugins
12
+
13
+
14
+ def run_pipeline(
15
+ stages_to_run: list[Stage],
16
+ config_path: str | None = None,
17
+ console: Console | None = None,
18
+ ) -> int:
19
+ console = console or Console()
20
+ console.print("[bold]OmniShip v0.1.0[/bold]")
21
+
22
+ # 1. Load plugins
23
+ registry = load_plugins()
24
+
25
+ # 2. Locate config
26
+ path = Path(config_path) if config_path else find_config_file()
27
+ if not path or not path.is_file():
28
+ console.print("[bold red]Error:[/bold red] No omniship.yaml configuration file found.")
29
+ return 1
30
+
31
+ # 3. Load & validate configuration
32
+ try:
33
+ config, graphs = load_and_validate(path, registry)
34
+ except Exception as exc:
35
+ console.print(f"[bold red]Configuration Error:[/bold red] {exc}")
36
+ return 1
37
+
38
+ # 4. Set up context and executor
39
+ workspace_root = path.parent.resolve()
40
+ context = ExecutionContext(
41
+ workspace_root=workspace_root,
42
+ stage=stages_to_run[0],
43
+ )
44
+
45
+ ui = TerminalUI(console=console)
46
+ executor = StageExecutor(registry=registry, listeners=[ui])
47
+
48
+ async def _execute() -> bool:
49
+ graphs_to_execute = [graphs[st] for st in stages_to_run if st in graphs]
50
+ for g in graphs_to_execute:
51
+ context.stage = g.stage
52
+ success = await executor.execute_stage(g, context)
53
+ if g.stage == Stage.BUILD and context.artifacts:
54
+ ui.print_artifacts(context.artifacts.to_list())
55
+ if not success:
56
+ return False
57
+ return True
58
+
59
+ success = asyncio.run(_execute())
60
+
61
+ if success:
62
+ if Stage.SHIP in stages_to_run:
63
+ console.print("\n[bold green]Shipped successfully![/bold green]")
64
+ else:
65
+ console.print(f"\n[bold green]{stages_to_run[-1].value.capitalize()} completed successfully![/bold green]")
66
+ return 0
67
+ else:
68
+ return 1
@@ -0,0 +1,23 @@
1
+ import sys
2
+ import click
3
+
4
+ from omniship.cli.runner import run_pipeline
5
+ from omniship.core.stage import Stage
6
+
7
+
8
+ @click.command()
9
+ @click.option("--skip-check", is_flag=True, default=False, help="Skip the Check stage")
10
+ @click.option("--skip-build", is_flag=True, default=False, help="Skip the Build stage")
11
+ @click.option("-c", "--config", "config_path", help="Path to configuration file")
12
+ def ship_cmd(skip_check: bool, skip_build: bool, config_path: str | None) -> None:
13
+ """Run Check -> Build -> Ship (complete release flow)."""
14
+ stages: list[Stage] = []
15
+ if not skip_check:
16
+ stages.append(Stage.CHECK)
17
+ if not skip_build:
18
+ stages.append(Stage.BUILD)
19
+ stages.append(Stage.SHIP)
20
+
21
+ code = run_pipeline(stages, config_path=config_path)
22
+ if code != 0:
23
+ sys.exit(code)
@@ -0,0 +1,57 @@
1
+ from rich.console import Console
2
+
3
+ from omniship.core.graph import StageGraph
4
+ from omniship.core.node import Node
5
+ from omniship.core.result import NodeResult, NodeStatus
6
+ from omniship.core.stage import Stage
7
+
8
+
9
+ class TerminalUI:
10
+ def __init__(self, console: Console | None = None) -> None:
11
+ self.console = console or Console()
12
+ self._current_stage: Stage | None = None
13
+ self._stage_failed = False
14
+ self._printed_stages: set[Stage] = set()
15
+
16
+ def on_stage_start(self, stage: Stage, graph: StageGraph) -> None:
17
+ self._current_stage = stage
18
+ self.console.print()
19
+ self.console.print(f"[bold cyan]{stage.value.upper()}[/bold cyan]")
20
+ if not graph.nodes:
21
+ self.console.print(" [dim](no nodes defined)[/dim]")
22
+
23
+ def on_node_start(self, stage: Stage, node: Node) -> None:
24
+ pass
25
+
26
+ def on_node_finish(self, stage: Stage, node: Node, result: NodeResult) -> None:
27
+ if result.status == NodeStatus.SUCCESS:
28
+ dur = f"{result.duration:.2f}s"
29
+ self.console.print(f" [green]✓[/green] [bold]{node.id:<24}[/bold] [dim]{dur:>8}[/dim]")
30
+ elif result.status == NodeStatus.SKIPPED:
31
+ reason = result.error_message or "skipped"
32
+ self.console.print(f" [yellow]⊘[/yellow] [bold]{node.id:<24}[/bold] [yellow]{reason}[/yellow]")
33
+ elif result.status == NodeStatus.FAILED:
34
+ dur = f"{result.duration:.2f}s"
35
+ self.console.print(f" [red]✗[/red] [bold]{node.id:<24}[/bold] [red]{dur:>8}[/red]")
36
+ if result.error_message:
37
+ self.console.print(f" [red]Error:[/red] {result.error_message}")
38
+ if result.stdout.strip():
39
+ self.console.print(" [dim]--- stdout ---[/dim]")
40
+ for line in result.stdout.strip().splitlines():
41
+ self.console.print(f" {line}")
42
+ if result.stderr.strip():
43
+ self.console.print(" [dim]--- stderr ---[/dim]")
44
+ for line in result.stderr.strip().splitlines():
45
+ self.console.print(f" [red]{line}[/red]")
46
+
47
+ def on_stage_finish(self, stage: Stage, success: bool) -> None:
48
+ if not success:
49
+ self._stage_failed = True
50
+ self.console.print(f"\n[bold red]{stage.value.upper()} FAILED[/bold red]")
51
+
52
+ def print_artifacts(self, artifacts: list) -> None:
53
+ if artifacts:
54
+ self.console.print()
55
+ self.console.print("[bold]Artifacts[/bold]")
56
+ for art in artifacts:
57
+ self.console.print(f" [green]•[/green] {art.name} [dim]({art.path})[/dim]")