action-platform 0.1.1__py3-none-any.whl
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.
- action_platform/__init__.py +10 -0
- action_platform/abc/__init__.py +7 -0
- action_platform/abc/ci_runner.py +25 -0
- action_platform/abc/deploy_target.py +46 -0
- action_platform/abc/source_host.py +51 -0
- action_platform/cli/__init__.py +1 -0
- action_platform/cli/commands/__init__.py +1 -0
- action_platform/cli/commands/cloud.py +52 -0
- action_platform/cli/commands/deploy.py +90 -0
- action_platform/cli/commands/init.py +136 -0
- action_platform/cli/commands/mcp.py +26 -0
- action_platform/cli/commands/release.py +22 -0
- action_platform/cli/commands/service.py +50 -0
- action_platform/cli/setup.py +27 -0
- action_platform/core/__init__.py +1 -0
- action_platform/core/action_platform.py +63 -0
- action_platform/core/changelog.py +73 -0
- action_platform/core/config.py +109 -0
- action_platform/core/context.py +65 -0
- action_platform/core/exception.py +25 -0
- action_platform/core/generate.py +217 -0
- action_platform/core/git.py +91 -0
- action_platform/core/module.py +21 -0
- action_platform/core/pipeline.py +150 -0
- action_platform/core/templates.py +261 -0
- action_platform/core/versioning.py +43 -0
- action_platform/logging.py +14 -0
- action_platform/main.py +27 -0
- action_platform/mcp/__init__.py +11 -0
- action_platform/mcp/__main__.py +4 -0
- action_platform/mcp/annotations.py +33 -0
- action_platform/mcp/server.py +63 -0
- action_platform/mcp/tools/__init__.py +3 -0
- action_platform/mcp/tools/lifecycle.py +111 -0
- action_platform/mcp/tools/matrix.py +49 -0
- action_platform/mcp/tools/project.py +109 -0
- action_platform/providers/__init__.py +5 -0
- action_platform/providers/source_github.py +110 -0
- action_platform/settings.py +27 -0
- action_platform/testing/__init__.py +1 -0
- action_platform-0.1.1.dist-info/METADATA +390 -0
- action_platform-0.1.1.dist-info/RECORD +45 -0
- action_platform-0.1.1.dist-info/WHEEL +4 -0
- action_platform-0.1.1.dist-info/entry_points.txt +4 -0
- action_platform-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Action Platform __init__ module."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__description__ = "🛠️ Action Platform padroniza init, release e deploy."
|
|
5
|
+
|
|
6
|
+
from .core.config import Config
|
|
7
|
+
from .core.context import Context
|
|
8
|
+
from .core.action_platform import ActionPlatform
|
|
9
|
+
|
|
10
|
+
__all__ = ["Config", "Context", "ActionPlatform"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CIRunner ABC."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Iterator
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from action_platform.core.context import Context, RunRef, RunResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CIRunner(ABC):
|
|
11
|
+
"""CIRunner"""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def trigger(self, ctx: "Context", job: str, params: dict) -> "RunRef":
|
|
17
|
+
"""Enqueue pipeline run."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def wait(self, ctx: "Context", run: "RunRef", timeout: int = 1800) -> "RunResult":
|
|
21
|
+
"""Block until pipeline finishes or timeout."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def logs(self, ctx: "Context", run: "RunRef") -> Iterator[str]:
|
|
25
|
+
"""Stream pipeline logs."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""DeployTarget ABC — the lifecycle every deploy target answers to.
|
|
2
|
+
|
|
3
|
+
Vocabulary mirrors what a deployment scope needs over its life: provision the
|
|
4
|
+
target once, ship versions, move traffic, undo, inspect, tear down. A target
|
|
5
|
+
that cannot do one of them raises NotImplementedError and the CLI says so.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from action_platform.core.context import Context, DeployResult, Diagnosis
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeployTarget(ABC):
|
|
16
|
+
"""DeployTarget"""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def preflight(self, ctx: "Context") -> None:
|
|
22
|
+
"""Validate credentials, tooling, artifacts. Raise DeployError otherwise."""
|
|
23
|
+
|
|
24
|
+
def create(self, ctx: "Context") -> None:
|
|
25
|
+
"""Provision the target itself (stack, app, registry). Idempotent."""
|
|
26
|
+
raise NotImplementedError(f"{self.name} cannot create")
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def deploy(self, ctx: "Context") -> "DeployResult":
|
|
30
|
+
"""Ship the current version."""
|
|
31
|
+
|
|
32
|
+
def switch_traffic(self, ctx: "Context", weight: int) -> None:
|
|
33
|
+
"""Move `weight` percent of traffic to the newest version (blue-green)."""
|
|
34
|
+
raise NotImplementedError(f"{self.name} cannot switch traffic")
|
|
35
|
+
|
|
36
|
+
def rollback(self, ctx: "Context", to_version: str | None = None) -> None:
|
|
37
|
+
"""Revert to `to_version` or the previous one."""
|
|
38
|
+
raise NotImplementedError(f"{self.name} cannot rollback")
|
|
39
|
+
|
|
40
|
+
def diagnose(self, ctx: "Context") -> "Diagnosis":
|
|
41
|
+
"""Health, last deploy, logs pointer."""
|
|
42
|
+
raise NotImplementedError(f"{self.name} cannot diagnose")
|
|
43
|
+
|
|
44
|
+
def delete(self, ctx: "Context") -> None:
|
|
45
|
+
"""Tear the target down."""
|
|
46
|
+
raise NotImplementedError(f"{self.name} cannot delete")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""SourceHost ABC."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from action_platform.core.context import Context, PRRef, ReleaseRef
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SourceHost(ABC):
|
|
12
|
+
"""SourceHost"""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def detect(self, remote_url: str) -> bool:
|
|
18
|
+
"""Return True if this provider handles remote_url."""
|
|
19
|
+
|
|
20
|
+
def create_repository(
|
|
21
|
+
self, repo: str, description: str = "", private: bool = False
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Create the remote repository; return its clone URL."""
|
|
24
|
+
raise NotImplementedError(f"{self.name} cannot create repositories")
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def create_tag(self, ctx: "Context", tag: str) -> None:
|
|
28
|
+
"""Create annotated tag on remote."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def create_release(
|
|
32
|
+
self,
|
|
33
|
+
ctx: "Context",
|
|
34
|
+
tag: str,
|
|
35
|
+
notes: str,
|
|
36
|
+
assets: list[Path] | None = None,
|
|
37
|
+
draft: bool = False,
|
|
38
|
+
prerelease: bool = False,
|
|
39
|
+
) -> "ReleaseRef":
|
|
40
|
+
"""Publish release on remote host."""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def open_pr(
|
|
44
|
+
self,
|
|
45
|
+
ctx: "Context",
|
|
46
|
+
base: str,
|
|
47
|
+
head: str,
|
|
48
|
+
title: str,
|
|
49
|
+
body: str,
|
|
50
|
+
) -> "PRRef":
|
|
51
|
+
"""Open pull/merge request."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action Platform CLI package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action Platform CLI commands."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""`action-platform cloud` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from action_platform.core.generate import apply_cloud, read_platform
|
|
11
|
+
from action_platform.core.templates import load_matrix
|
|
12
|
+
from action_platform.logging import logger
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
help="Apply deploy overlays to an existing project.", no_args_is_help=True
|
|
16
|
+
)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("set")
|
|
21
|
+
def set_(
|
|
22
|
+
name: str = typer.Argument(..., help="aws/lambda, docker, ..."),
|
|
23
|
+
project: Path = typer.Option(
|
|
24
|
+
None, "--project", "-p", help="Project directory (default: cwd)"
|
|
25
|
+
),
|
|
26
|
+
update: bool = typer.Option(False, "--update", help="Refresh the templates cache"),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Apply a cloud overlay and set [deploy] target in platform.toml (replaces the previous one)."""
|
|
29
|
+
repo, matrix = load_matrix(update=update)
|
|
30
|
+
target = (project or Path.cwd()).resolve()
|
|
31
|
+
|
|
32
|
+
apply_cloud(repo, matrix.cloud(name), target)
|
|
33
|
+
|
|
34
|
+
logger.info("applied cloud %s to %s", name, target)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("list")
|
|
38
|
+
def list_(
|
|
39
|
+
project: Path = typer.Option(
|
|
40
|
+
None, "--project", "-p", help="Filter by what this project supports"
|
|
41
|
+
),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""List cloud overlays, optionally only those compatible with a project."""
|
|
44
|
+
_, matrix = load_matrix()
|
|
45
|
+
clouds = matrix.clouds
|
|
46
|
+
|
|
47
|
+
if project is not None:
|
|
48
|
+
meta = read_platform(project.resolve())
|
|
49
|
+
clouds = matrix.clouds_for(meta.get("type", ""), meta.get("language", ""))
|
|
50
|
+
|
|
51
|
+
for cloud in clouds:
|
|
52
|
+
console.print(f"[bold]{cloud.name}[/bold] {cloud.description}")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""`action-platform deploy | rollback | diagnose | destroy` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from action_platform.core.action_platform import ActionPlatform
|
|
11
|
+
from action_platform.core.config import Config
|
|
12
|
+
from action_platform.core.exception import DeployError
|
|
13
|
+
from action_platform.logging import logger
|
|
14
|
+
from action_platform.settings import settings
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
TARGET = typer.Option(
|
|
19
|
+
None, "--target", help="Filter by target name (aws/lambda, docker, ...)"
|
|
20
|
+
)
|
|
21
|
+
STAGE = typer.Option(
|
|
22
|
+
None, "--stage", help="dev | prod (default: prod on main/master, else dev)"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _tool() -> ActionPlatform:
|
|
27
|
+
return ActionPlatform(config=Config.from_toml(Path.cwd() / settings.CONFIG_FILE))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(
|
|
31
|
+
target: str | None = TARGET,
|
|
32
|
+
stage: str | None = STAGE,
|
|
33
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Ship the current version to the [deploy] target in platform.toml."""
|
|
36
|
+
for r in _tool().deploy(target=target, dry_run=dry_run, stage=stage):
|
|
37
|
+
logger.info(
|
|
38
|
+
"deploy %s ok=%s version=%s url=%s", r.target, r.ok, r.version, r.url
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not r.ok:
|
|
42
|
+
raise DeployError(r.error or f"{r.target} failed")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rollback(
|
|
46
|
+
to_version: str | None = typer.Argument(
|
|
47
|
+
None, help="Version to return to (default: previous)"
|
|
48
|
+
),
|
|
49
|
+
target: str | None = TARGET,
|
|
50
|
+
stage: str | None = STAGE,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Return the target to a previous version."""
|
|
53
|
+
try:
|
|
54
|
+
_tool().rollback(target=target, to_version=to_version, stage=stage)
|
|
55
|
+
except NotImplementedError as e:
|
|
56
|
+
raise DeployError(str(e)) from e
|
|
57
|
+
logger.info("rollback done")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def diagnose(target: str | None = TARGET, stage: str | None = STAGE) -> None:
|
|
61
|
+
"""Health, status and URL of the deployed target."""
|
|
62
|
+
try:
|
|
63
|
+
results = _tool().diagnose(target=target, stage=stage)
|
|
64
|
+
except NotImplementedError as e:
|
|
65
|
+
raise DeployError(str(e)) from e
|
|
66
|
+
for d in results:
|
|
67
|
+
mark = "[green]ok[/green]" if d.ok else "[red]not ok[/red]"
|
|
68
|
+
console.print(f"[bold]{d.target}[/bold] {mark} {d.status}")
|
|
69
|
+
|
|
70
|
+
if d.url:
|
|
71
|
+
console.print(f" url: {d.url}")
|
|
72
|
+
|
|
73
|
+
for k, v in d.details.items():
|
|
74
|
+
console.print(f" {k}: {v}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def destroy(
|
|
78
|
+
target: str | None = TARGET,
|
|
79
|
+
stage: str | None = STAGE,
|
|
80
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Tear the target down. Irreversible."""
|
|
83
|
+
if not yes and not typer.confirm("Delete the deployed target?"):
|
|
84
|
+
raise typer.Abort()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
_tool().destroy(target=target, stage=stage)
|
|
88
|
+
except NotImplementedError as e:
|
|
89
|
+
raise DeployError(str(e)) from e
|
|
90
|
+
logger.info("destroy done")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""`action-platform init` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from action_platform.core.exception import TemplateError
|
|
12
|
+
from action_platform.core.generate import apply_cloud, generate_project, push_project
|
|
13
|
+
from action_platform.core.templates import Matrix, load_matrix
|
|
14
|
+
from action_platform.logging import logger
|
|
15
|
+
|
|
16
|
+
CI_PROVIDERS = ["github", "gitlab", "jenkins"]
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(
|
|
22
|
+
type_: str | None = typer.Argument(
|
|
23
|
+
None, metavar="TYPE", help="web, library, mcp, ..."
|
|
24
|
+
),
|
|
25
|
+
stack: str | None = typer.Argument(None, help="python, go, node, ..."),
|
|
26
|
+
template: str | None = typer.Argument(
|
|
27
|
+
None, help="fastapi, gin, ... (default per stack)"
|
|
28
|
+
),
|
|
29
|
+
name: str | None = typer.Option(None, "--name", "-n", help="Project name"),
|
|
30
|
+
ci: str | None = typer.Option(
|
|
31
|
+
None, "--ci", help="CI provider: " + ", ".join(CI_PROVIDERS)
|
|
32
|
+
),
|
|
33
|
+
cloud: str | None = typer.Option(
|
|
34
|
+
None, "--cloud", help="Deploy overlay: aws/lambda, docker, ..."
|
|
35
|
+
),
|
|
36
|
+
output: Path | None = typer.Option(
|
|
37
|
+
None, "--output", "-o", help="Where to create the project"
|
|
38
|
+
),
|
|
39
|
+
push: bool = typer.Option(
|
|
40
|
+
True,
|
|
41
|
+
"--push/--no-push",
|
|
42
|
+
help="Create the remote repo via [source_host] and push (default: on)",
|
|
43
|
+
),
|
|
44
|
+
private: bool = typer.Option(False, "--private", help="With --push: private repo"),
|
|
45
|
+
list_: bool = typer.Option(False, "--list", "-l", help="Show the template matrix"),
|
|
46
|
+
update: bool = typer.Option(False, "--update", help="Refresh the templates cache"),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Bootstrap a project from the templates matrix."""
|
|
49
|
+
repo, matrix = load_matrix(update=update)
|
|
50
|
+
|
|
51
|
+
if list_:
|
|
52
|
+
print_matrix(matrix)
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if type_ is None:
|
|
56
|
+
type_ = choose("type", matrix.types())
|
|
57
|
+
if stack is None and matrix.stacks(type_):
|
|
58
|
+
stack = choose("stack", matrix.stacks(type_))
|
|
59
|
+
if template is None and stack is not None:
|
|
60
|
+
leaves = matrix.templates(type_, stack)
|
|
61
|
+
if len(leaves) > 1:
|
|
62
|
+
template = choose("template", [leaf.template for leaf in leaves])
|
|
63
|
+
|
|
64
|
+
leaf = matrix.resolve(type_, stack, template)
|
|
65
|
+
|
|
66
|
+
if name is None:
|
|
67
|
+
name = typer.prompt("project name")
|
|
68
|
+
if ci is None and leaf.type != "empty":
|
|
69
|
+
ci = choose("ci", CI_PROVIDERS)
|
|
70
|
+
if ci is not None and ci not in CI_PROVIDERS:
|
|
71
|
+
raise TemplateError(f"unknown ci: {ci} (available: {', '.join(CI_PROVIDERS)})")
|
|
72
|
+
|
|
73
|
+
project = generate_project(
|
|
74
|
+
repo, leaf, name=name, ci=ci, output=output or Path.cwd()
|
|
75
|
+
)
|
|
76
|
+
logger.info("created %s", project)
|
|
77
|
+
|
|
78
|
+
if cloud is not None:
|
|
79
|
+
apply_cloud(repo, matrix.cloud(cloud), project)
|
|
80
|
+
logger.info("applied cloud %s", cloud)
|
|
81
|
+
|
|
82
|
+
if push:
|
|
83
|
+
url = push_project(project, private=private)
|
|
84
|
+
logger.info("pushed to %s", url)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def choose(label: str, options: list[str]) -> str:
|
|
88
|
+
console.print(f"[bold]{label}[/bold]")
|
|
89
|
+
for i, opt in enumerate(options, 1):
|
|
90
|
+
console.print(f" {i}. {opt}")
|
|
91
|
+
while True:
|
|
92
|
+
raw = typer.prompt(f"{label} [1-{len(options)}]")
|
|
93
|
+
if raw in options:
|
|
94
|
+
return raw
|
|
95
|
+
if raw.isdigit() and 1 <= int(raw) <= len(options):
|
|
96
|
+
return options[int(raw) - 1]
|
|
97
|
+
console.print("[red]invalid choice[/red]")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_matrix(matrix: Matrix) -> None:
|
|
101
|
+
table = Table(title="Projects")
|
|
102
|
+
table.add_column("type")
|
|
103
|
+
table.add_column("stack")
|
|
104
|
+
table.add_column("template")
|
|
105
|
+
table.add_column("description")
|
|
106
|
+
for leaf in matrix.leaves:
|
|
107
|
+
tpl = f"{leaf.template} *" if leaf.default else leaf.template
|
|
108
|
+
table.add_row(leaf.type, leaf.stack, tpl, leaf.description)
|
|
109
|
+
console.print(table)
|
|
110
|
+
console.print("[dim]* default template for the stack[/dim]\n")
|
|
111
|
+
|
|
112
|
+
clouds = Table(title="Clouds")
|
|
113
|
+
clouds.add_column("cloud")
|
|
114
|
+
clouds.add_column("types")
|
|
115
|
+
clouds.add_column("languages")
|
|
116
|
+
clouds.add_column("description")
|
|
117
|
+
for cloud in matrix.clouds:
|
|
118
|
+
clouds.add_row(
|
|
119
|
+
cloud.name,
|
|
120
|
+
", ".join(cloud.types) or "any",
|
|
121
|
+
", ".join(cloud.languages) or "any",
|
|
122
|
+
cloud.description,
|
|
123
|
+
)
|
|
124
|
+
console.print(clouds)
|
|
125
|
+
|
|
126
|
+
services = Table(title="Services")
|
|
127
|
+
services.add_column("service")
|
|
128
|
+
services.add_column("providers")
|
|
129
|
+
services.add_column("description")
|
|
130
|
+
|
|
131
|
+
for service in matrix.services:
|
|
132
|
+
services.add_row(
|
|
133
|
+
service.name, ", ".join(service.providers), service.description
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
console.print(services)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""`action-platform mcp` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from action_platform.core.exception import ActionPlatformError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(
|
|
11
|
+
http: bool = typer.Option(
|
|
12
|
+
False, "--http", help="Serve over streamable HTTP instead of stdio"
|
|
13
|
+
),
|
|
14
|
+
host: str = typer.Option("127.0.0.1", "--host"),
|
|
15
|
+
port: int = typer.Option(8765, "--port", help="Port for --http"),
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Run the embedded MCP server so AI agents can scaffold, deploy and operate projects."""
|
|
18
|
+
try:
|
|
19
|
+
from action_platform.mcp.server import main
|
|
20
|
+
except ModuleNotFoundError as e:
|
|
21
|
+
raise ActionPlatformError(
|
|
22
|
+
"MCP support is not installed: pip install 'action-platform[mcp]'"
|
|
23
|
+
) from e
|
|
24
|
+
|
|
25
|
+
argv = ["--http", "--host", host, "--port", str(port)] if http else []
|
|
26
|
+
main(argv)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""`action-platform release` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from action_platform.core.config import Config
|
|
10
|
+
from action_platform.core.action_platform import ActionPlatform
|
|
11
|
+
from action_platform.logging import logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(
|
|
15
|
+
level: str = typer.Argument("patch", help="patch | minor | major | X.Y.Z"),
|
|
16
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Bump version, generate changelog, tag, and publish release."""
|
|
19
|
+
config = Config.from_toml(Path.cwd() / "platform.toml")
|
|
20
|
+
tool = ActionPlatform(config=config)
|
|
21
|
+
ctx = tool.release(level=level, dry_run=dry_run)
|
|
22
|
+
logger.info("release done: %s", ctx.next_version)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""`action-platform service` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from action_platform.core.generate import apply_service
|
|
11
|
+
from action_platform.core.templates import load_matrix
|
|
12
|
+
from action_platform.logging import logger
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
help="Add application dependencies (database, cache, storage) to a project.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("add")
|
|
22
|
+
def add(
|
|
23
|
+
name: str = typer.Argument(..., help="postgres, redis, s3, ..."),
|
|
24
|
+
provider: str | None = typer.Option(
|
|
25
|
+
None, "--provider", help="docker, aws-rds, aws, ... (default: first listed)"
|
|
26
|
+
),
|
|
27
|
+
project: Path = typer.Option(
|
|
28
|
+
None, "--project", "-p", help="Project directory (default: cwd)"
|
|
29
|
+
),
|
|
30
|
+
update: bool = typer.Option(False, "--update", help="Refresh the templates cache"),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Add services/<name>/ with up + link scripts and record it in platform.toml."""
|
|
33
|
+
repo, matrix = load_matrix(update=update)
|
|
34
|
+
target = (project or Path.cwd()).resolve()
|
|
35
|
+
|
|
36
|
+
apply_service(repo, matrix.service(name), target, provider=provider)
|
|
37
|
+
|
|
38
|
+
logger.info("added service %s to %s", name, target)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
def list_() -> None:
|
|
43
|
+
"""List available services and their providers."""
|
|
44
|
+
_, matrix = load_matrix()
|
|
45
|
+
|
|
46
|
+
for service in matrix.services:
|
|
47
|
+
console.print(
|
|
48
|
+
f"[bold]{service.name}[/bold] {service.description} "
|
|
49
|
+
f"[dim]({', '.join(service.providers)})[/dim]"
|
|
50
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typer app assembly."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from action_platform.cli.commands import cloud as cloud_cmd
|
|
6
|
+
from action_platform.cli.commands import deploy as deploy_cmd
|
|
7
|
+
from action_platform.cli.commands import init as init_cmd
|
|
8
|
+
from action_platform.cli.commands import mcp as mcp_cmd
|
|
9
|
+
from action_platform.cli.commands import release as release_cmd
|
|
10
|
+
from action_platform.cli.commands import service as service_cmd
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="action-platform",
|
|
14
|
+
help="Standardize init, release, and deploy across any stack.",
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
pretty_exceptions_enable=False,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
app.command("init")(init_cmd.run)
|
|
20
|
+
app.command("release")(release_cmd.run)
|
|
21
|
+
app.command("deploy")(deploy_cmd.run)
|
|
22
|
+
app.command("rollback")(deploy_cmd.rollback)
|
|
23
|
+
app.command("diagnose")(deploy_cmd.diagnose)
|
|
24
|
+
app.command("destroy")(deploy_cmd.destroy)
|
|
25
|
+
app.command("mcp")(mcp_cmd.run)
|
|
26
|
+
app.add_typer(cloud_cmd.app, name="cloud")
|
|
27
|
+
app.add_typer(service_cmd.app, name="service")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action Platform core module."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""ActionPlatform orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from action_platform.core import pipeline
|
|
8
|
+
from action_platform.core.config import Config
|
|
9
|
+
from action_platform.core.context import Context, DeployResult, Diagnosis
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ActionPlatform:
|
|
13
|
+
"""
|
|
14
|
+
Import:
|
|
15
|
+
from action_platform import ActionPlatform, Config
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
tool = ActionPlatform(config=Config(...))
|
|
19
|
+
tool.release("patch")
|
|
20
|
+
tool.deploy()
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config (Config): configuration with injected providers.
|
|
24
|
+
repo_root (Path): repository root. Defaults to cwd.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
config (Config):
|
|
28
|
+
repo_root (Path):
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, config: Config | None = None, repo_root: Path | None = None
|
|
33
|
+
) -> None:
|
|
34
|
+
self.config = config or Config()
|
|
35
|
+
self.repo_root = repo_root or Path.cwd()
|
|
36
|
+
|
|
37
|
+
def release(self, level: str = "patch", dry_run: bool = False) -> Context:
|
|
38
|
+
return pipeline.release(self.config, level, self.repo_root, dry_run=dry_run)
|
|
39
|
+
|
|
40
|
+
def deploy(
|
|
41
|
+
self, target: str | None = None, dry_run: bool = False, stage: str | None = None
|
|
42
|
+
) -> list[DeployResult]:
|
|
43
|
+
return pipeline.deploy(
|
|
44
|
+
self.config, target, self.repo_root, dry_run=dry_run, stage=stage
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def rollback(
|
|
48
|
+
self,
|
|
49
|
+
target: str | None = None,
|
|
50
|
+
to_version: str | None = None,
|
|
51
|
+
stage: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
pipeline.rollback(
|
|
54
|
+
self.config, target, self.repo_root, to_version=to_version, stage=stage
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def diagnose(
|
|
58
|
+
self, target: str | None = None, stage: str | None = None
|
|
59
|
+
) -> list[Diagnosis]:
|
|
60
|
+
return pipeline.diagnose(self.config, target, self.repo_root, stage=stage)
|
|
61
|
+
|
|
62
|
+
def destroy(self, target: str | None = None, stage: str | None = None) -> None:
|
|
63
|
+
pipeline.destroy(self.config, target, self.repo_root, stage=stage)
|