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.
- omniship-0.1.0/PKG-INFO +122 -0
- omniship-0.1.0/README.md +107 -0
- omniship-0.1.0/pyproject.toml +37 -0
- omniship-0.1.0/pyproject.toml.orig +36 -0
- omniship-0.1.0/src/omniship/__init__.py +2 -0
- omniship-0.1.0/src/omniship/cli/app.py +29 -0
- omniship-0.1.0/src/omniship/cli/build.py +16 -0
- omniship-0.1.0/src/omniship/cli/check.py +14 -0
- omniship-0.1.0/src/omniship/cli/plan.py +65 -0
- omniship-0.1.0/src/omniship/cli/plugins.py +73 -0
- omniship-0.1.0/src/omniship/cli/runner.py +68 -0
- omniship-0.1.0/src/omniship/cli/ship.py +23 -0
- omniship-0.1.0/src/omniship/cli/ui.py +57 -0
- omniship-0.1.0/src/omniship/config/loader.py +56 -0
- omniship-0.1.0/src/omniship/config/models.py +23 -0
- omniship-0.1.0/src/omniship/config/validation.py +62 -0
- omniship-0.1.0/src/omniship/core/artifact.py +52 -0
- omniship-0.1.0/src/omniship/core/context.py +24 -0
- omniship-0.1.0/src/omniship/core/executor.py +263 -0
- omniship-0.1.0/src/omniship/core/graph.py +108 -0
- omniship-0.1.0/src/omniship/core/node.py +26 -0
- omniship-0.1.0/src/omniship/core/result.py +34 -0
- omniship-0.1.0/src/omniship/core/stage.py +7 -0
- omniship-0.1.0/src/omniship/operations/__init__.py +14 -0
- omniship-0.1.0/src/omniship/operations/command.py +172 -0
- omniship-0.1.0/src/omniship/operations/github.py +188 -0
- omniship-0.1.0/src/omniship/operations/noop.py +41 -0
- omniship-0.1.0/src/omniship/plugins/api.py +25 -0
- omniship-0.1.0/src/omniship/plugins/discovery.py +39 -0
- omniship-0.1.0/src/omniship/plugins/metadata.py +13 -0
- omniship-0.1.0/src/omniship/plugins/registry.py +42 -0
- omniship-0.1.0/src/omniship/plugins/template.py +38 -0
omniship-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
omniship-0.1.0/README.md
ADDED
|
@@ -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,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]")
|