action-platform 0.1.0__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 +52 -0
- action_platform/cli/__init__.py +1 -0
- action_platform/cli/commands/__init__.py +1 -0
- action_platform/cli/commands/branch.py +36 -0
- action_platform/cli/commands/cloud.py +52 -0
- action_platform/cli/commands/deploy.py +90 -0
- action_platform/cli/commands/gitflow.py +49 -0
- action_platform/cli/commands/init.py +136 -0
- action_platform/cli/commands/install.py +50 -0
- action_platform/cli/commands/mcp.py +26 -0
- action_platform/cli/commands/pr.py +41 -0
- action_platform/cli/commands/release.py +22 -0
- action_platform/cli/commands/service.py +50 -0
- action_platform/cli/setup.py +35 -0
- action_platform/core/__init__.py +1 -0
- action_platform/core/action_platform.py +63 -0
- action_platform/core/branching.py +132 -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 +218 -0
- action_platform/core/git.py +98 -0
- action_platform/core/gitflow.py +189 -0
- action_platform/core/install.py +199 -0
- action_platform/core/module.py +21 -0
- action_platform/core/pipeline.py +151 -0
- action_platform/core/pullrequest.py +113 -0
- action_platform/core/templates.py +261 -0
- action_platform/core/versioning.py +43 -0
- action_platform/hooks/__init__.py +1 -0
- action_platform/hooks/commit-msg +7 -0
- action_platform/hooks/gitflow.sh +78 -0
- action_platform/hooks/pre-commit +5 -0
- action_platform/hooks/pre-push +16 -0
- action_platform/logging.py +14 -0
- action_platform/main.py +35 -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 +70 -0
- action_platform/mcp/tools/__init__.py +3 -0
- action_platform/mcp/tools/flow.py +148 -0
- action_platform/mcp/tools/lifecycle.py +111 -0
- action_platform/mcp/tools/matrix.py +49 -0
- action_platform/mcp/tools/project.py +146 -0
- action_platform/providers/__init__.py +5 -0
- action_platform/providers/source_github.py +122 -0
- action_platform/settings.py +27 -0
- action_platform/testing/__init__.py +1 -0
- action_platform-0.1.0.dist-info/METADATA +401 -0
- action_platform-0.1.0.dist-info/RECORD +59 -0
- action_platform-0.1.0.dist-info/WHEEL +4 -0
- action_platform-0.1.0.dist-info/entry_points.txt +4 -0
- action_platform-0.1.0.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,52 @@
|
|
|
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
|
+
draft: bool = False,
|
|
51
|
+
) -> "PRRef":
|
|
52
|
+
"""Open pull/merge request."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action Platform CLI package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action Platform CLI commands."""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""`action-platform branch` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from action_platform.core import branching
|
|
8
|
+
from action_platform.logging import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
kind: str = typer.Argument(
|
|
13
|
+
...,
|
|
14
|
+
help="feature | bugfix | hotfix | release | "
|
|
15
|
+
+ " | ".join(
|
|
16
|
+
k
|
|
17
|
+
for k in branching.KINDS
|
|
18
|
+
if k not in {"feature", "bugfix", "hotfix", "release"}
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
code: str = typer.Argument(
|
|
22
|
+
..., help="Issue or ticket code: 42, PROJ-123, 1.4.0 for release"
|
|
23
|
+
),
|
|
24
|
+
slug: str | None = typer.Argument(None, help="Optional words appended as a slug"),
|
|
25
|
+
push: bool = typer.Option(
|
|
26
|
+
True, "--push/--no-push", help="Push the new branch upstream"
|
|
27
|
+
),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Start a branch: checkout the right base (develop or main), pull, create <kind>/<code>."""
|
|
30
|
+
branch = branching.start(kind, code, slug, push=push)
|
|
31
|
+
logger.info(
|
|
32
|
+
"on %s (from %s)%s",
|
|
33
|
+
branch.name,
|
|
34
|
+
branch.base,
|
|
35
|
+
" — pushed" if branch.pushed else "",
|
|
36
|
+
)
|
|
@@ -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,49 @@
|
|
|
1
|
+
"""`action-platform gitflow` command."""
|
|
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 import gitflow
|
|
11
|
+
from action_platform.core.exception import ActionPlatformError
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(
|
|
17
|
+
since: str | None = typer.Option(
|
|
18
|
+
None,
|
|
19
|
+
"--since",
|
|
20
|
+
help="Check commits after this ref (default: merge base with develop/main)",
|
|
21
|
+
),
|
|
22
|
+
install: bool = typer.Option(
|
|
23
|
+
False,
|
|
24
|
+
"--install-hooks",
|
|
25
|
+
help="Install the bundled git hooks into .git/hooks and exit",
|
|
26
|
+
),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Check the current branch and commits against git-flow and Conventional Commits."""
|
|
29
|
+
cwd = Path.cwd()
|
|
30
|
+
|
|
31
|
+
if install:
|
|
32
|
+
if gitflow.install_hooks(cwd):
|
|
33
|
+
console.print("[green]hooks installed[/green] (.git/hooks)")
|
|
34
|
+
return
|
|
35
|
+
raise ActionPlatformError("not a git repository")
|
|
36
|
+
|
|
37
|
+
report = gitflow.audit(cwd, since=since)
|
|
38
|
+
console.print(
|
|
39
|
+
f"branch [bold]{report.branch}[/bold], {report.checked_commits} commit(s) checked"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if report.ok:
|
|
43
|
+
console.print("[green]git-flow ok[/green]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
for problem in report.problems:
|
|
47
|
+
console.print(f"[red]✗[/red] {problem}")
|
|
48
|
+
|
|
49
|
+
raise typer.Exit(code=1)
|
|
@@ -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,50 @@
|
|
|
1
|
+
"""`action-platform install` command."""
|
|
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 import install as installing
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(
|
|
16
|
+
type_: str = typer.Option("web", "--type", help="web, library, docs, plugin"),
|
|
17
|
+
language: str | None = typer.Option(
|
|
18
|
+
None, "--language", help="python, go, node, php, java, rust (default: detected)"
|
|
19
|
+
),
|
|
20
|
+
ci: str = typer.Option("github", "--ci", help="github, gitlab, jenkins"),
|
|
21
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be created"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Install the platform in this repository: platform.toml, code quality, CI files (never overwritten) and git hooks into .git/hooks (always refreshed)."""
|
|
24
|
+
plan = installing.install(
|
|
25
|
+
Path.cwd(), type_=type_, language=language, ci=ci, dry_run=dry_run
|
|
26
|
+
)
|
|
27
|
+
verb = "would create" if dry_run else "created"
|
|
28
|
+
console.print(
|
|
29
|
+
f"[bold]{plan.root.name}[/bold] — {plan.language} {plan.type}, ci {plan.ci}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for rel in plan.created:
|
|
33
|
+
console.print(f" [green]+[/green] {rel} ({verb})")
|
|
34
|
+
|
|
35
|
+
for rel in plan.skipped:
|
|
36
|
+
console.print(f" [dim]= {rel} (exists, kept)[/dim]")
|
|
37
|
+
|
|
38
|
+
if plan.hooks_installed:
|
|
39
|
+
console.print(
|
|
40
|
+
" [green]✓[/green] git hooks installed (.git/hooks, unversioned)"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if dry_run or not plan.created:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
console.print(
|
|
47
|
+
"\nnext: review the files, then "
|
|
48
|
+
'[bold]git add -A && git commit -m "chore(platform): install action-platform"[/bold] '
|
|
49
|
+
"— chore(platform) commits are allowed on main"
|
|
50
|
+
)
|
|
@@ -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,41 @@
|
|
|
1
|
+
"""`action-platform pr` command."""
|
|
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 import pullrequest
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(
|
|
16
|
+
base: str | None = typer.Option(
|
|
17
|
+
None, "--base", help="Target branch (default from git-flow)"
|
|
18
|
+
),
|
|
19
|
+
title: str | None = typer.Option(
|
|
20
|
+
None, "--title", help="Default: first commit on the branch"
|
|
21
|
+
),
|
|
22
|
+
body: str | None = typer.Option(
|
|
23
|
+
None, "--body", help="Default: commits grouped by type"
|
|
24
|
+
),
|
|
25
|
+
draft: bool = typer.Option(False, "--draft"),
|
|
26
|
+
dry_run: bool = typer.Option(
|
|
27
|
+
False, "--dry-run", help="Show target, title and body; open nothing"
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Open a pull request for the current branch: audits git-flow, picks the target, describes the commits."""
|
|
31
|
+
cwd = Path.cwd()
|
|
32
|
+
|
|
33
|
+
if dry_run:
|
|
34
|
+
proposal = pullrequest.propose(cwd, base=base, title=title)
|
|
35
|
+
console.print(f"[bold]{proposal.head}[/bold] → [bold]{proposal.base}[/bold]")
|
|
36
|
+
console.print(f"title: {proposal.title}\n")
|
|
37
|
+
console.print(body or proposal.body)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
ref = pullrequest.open_pr(cwd, base=base, title=title, body=body, draft=draft)
|
|
41
|
+
console.print(f"[green]opened[/green] #{ref.number} {ref.url}")
|
|
@@ -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)
|