action-platform 0.1.2__tar.gz → 0.2.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 (60) hide show
  1. {action_platform-0.1.2 → action_platform-0.2.0}/PKG-INFO +57 -5
  2. {action_platform-0.1.2 → action_platform-0.2.0}/README.md +55 -3
  3. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/__init__.py +1 -1
  4. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/install.py +5 -1
  5. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/release.py +7 -2
  6. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/action_platform.py +9 -2
  7. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/git.py +17 -0
  8. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/install.py +21 -6
  9. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/pipeline.py +48 -7
  10. action_platform-0.2.0/action_platform/core/versioning.py +98 -0
  11. action_platform-0.2.0/action_platform/mcp/prompts.py +136 -0
  12. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/server.py +2 -0
  13. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/lifecycle.py +2 -1
  14. {action_platform-0.1.2 → action_platform-0.2.0}/pyproject.toml +4 -4
  15. action_platform-0.1.2/action_platform/core/versioning.py +0 -43
  16. {action_platform-0.1.2 → action_platform-0.2.0}/LICENSE +0 -0
  17. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/__init__.py +0 -0
  18. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/ci_runner.py +0 -0
  19. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/deploy_target.py +0 -0
  20. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/source_host.py +0 -0
  21. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/__init__.py +0 -0
  22. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/__init__.py +0 -0
  23. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/branch.py +0 -0
  24. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/cloud.py +0 -0
  25. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/deploy.py +0 -0
  26. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/gitflow.py +0 -0
  27. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/init.py +0 -0
  28. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/mcp.py +0 -0
  29. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/pr.py +0 -0
  30. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/service.py +0 -0
  31. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/setup.py +0 -0
  32. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/__init__.py +0 -0
  33. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/branching.py +0 -0
  34. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/changelog.py +0 -0
  35. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/config.py +0 -0
  36. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/context.py +0 -0
  37. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/exception.py +0 -0
  38. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/generate.py +0 -0
  39. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/gitflow.py +0 -0
  40. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/module.py +0 -0
  41. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/pullrequest.py +0 -0
  42. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/templates.py +0 -0
  43. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/__init__.py +0 -0
  44. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/commit-msg +0 -0
  45. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/gitflow.sh +0 -0
  46. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/pre-commit +0 -0
  47. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/pre-push +0 -0
  48. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/logging.py +0 -0
  49. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/main.py +0 -0
  50. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/__init__.py +0 -0
  51. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/__main__.py +0 -0
  52. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/annotations.py +0 -0
  53. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/__init__.py +0 -0
  54. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/flow.py +0 -0
  55. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/matrix.py +0 -0
  56. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/project.py +0 -0
  57. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/providers/__init__.py +0 -0
  58. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/providers/source_github.py +0 -0
  59. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/settings.py +0 -0
  60. {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/testing/__init__.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: action-platform
3
- Version: 0.1.2
4
- Summary: From idea to production in one command: scaffold, CI, release and deploy on any stack and cloud, with git-flow and Conventional Commits enforced and an MCP server for AI agents.
3
+ Version: 0.2.0
4
+ Summary: Your platform team, as a CLI.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
7
7
  http://www.apache.org/licenses/
@@ -248,7 +248,7 @@ Description-Content-Type: text/markdown
248
248
  </p>
249
249
 
250
250
  <p align="center">
251
- <strong>From idea to production in one command — on any stack, on any cloud.</strong>
251
+ <strong>Your platform team, as a CLI.</strong>
252
252
  </p>
253
253
 
254
254
  <p align="center">
@@ -321,13 +321,64 @@ action-platform branch feature 42 login # develop (or main) → pull
321
321
  action-platform branch hotfix PROJ-7 # from main/master
322
322
  action-platform gitflow # audit current branch + commits; --install-hooks
323
323
  action-platform pr [--draft] [--dry-run] # PR for the current branch: target from git-flow, body from commits
324
- action-platform release patch # bump, changelog, tag, GitHub release
324
+ action-platform release patch # bump, changelog, tag, GitHub release; off main/master → X.Y.Z-rc.N pre-release
325
325
  action-platform deploy --stage prod
326
326
  action-platform rollback
327
327
  action-platform diagnose
328
328
  action-platform destroy
329
329
  ```
330
330
 
331
+ ## Git-flow, enforced
332
+
333
+ Every project follows the same flow. Git hooks refuse the wrong move before it exists; CI refuses it on the pull request; the CLI and the MCP tools guide the right one.
334
+
335
+ ```mermaid
336
+ gitGraph
337
+ commit id: "chore: bootstrap"
338
+ branch develop
339
+ checkout develop
340
+ commit id: "chore(release): 0.3.2-rc.1" tag: "v0.3.2-rc.1"
341
+ branch feature/42-login
342
+ checkout feature/42-login
343
+ commit id: "feat(login): form"
344
+ commit id: "test(login): cover form"
345
+ checkout develop
346
+ merge feature/42-login id: "PR #1 → develop"
347
+ branch release/0.3.2
348
+ checkout release/0.3.2
349
+ commit id: "chore(release): 0.3.2-rc.2" tag: "v0.3.2-rc.2"
350
+ checkout main
351
+ merge release/0.3.2 id: "PR #2 → main"
352
+ commit id: "chore(release): 0.3.2" tag: "v0.3.2"
353
+ checkout develop
354
+ merge main id: "back-merge"
355
+ checkout main
356
+ branch hotfix/PROJ-7
357
+ checkout hotfix/PROJ-7
358
+ commit id: "fix(auth): expiry"
359
+ checkout main
360
+ merge hotfix/PROJ-7 id: "PR #3 → main"
361
+ commit id: "chore(release): 0.3.3" tag: "v0.3.3"
362
+ checkout develop
363
+ merge main id: "back-merge hotfix"
364
+ ```
365
+
366
+ | Branch | Starts from | Merges into | Release |
367
+ |--------|-------------|-------------|---------|
368
+ | `feature/<code>`, `bugfix/…`, `chore/…`, `docs/…`, `refactor/…`, `test/…`, `ci/…`, `perf/…` | `develop` (or the default branch when there is no `develop`) | `develop` | `X.Y.Z-rc.N` pre-release |
369
+ | `release/<version>` | `develop` | `main` and `develop` | `X.Y.Z-rc.N` until merged |
370
+ | `hotfix/<code>` | `main` | `main` and `develop` | `X.Y.Z-rc.N` until merged |
371
+ | `main` / `master` | — | — | stable `X.Y.Z` → PyPI / npm / … |
372
+
373
+ Rules the hooks and CI apply: branch names are `<kind>/<code>[-slug]`; commits are [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/); no direct commits on `main`, `master` or `develop` except `chore(release):`, `chore(platform):` and the bootstrap commit; a pull request may only target what the table allows.
374
+
375
+ ```bash
376
+ action-platform branch feature 42 login # develop → pull → feature/42-login → push
377
+ action-platform gitflow # audit branch + commits
378
+ action-platform pr # target and body from the rules and the commits
379
+ action-platform release patch # rc off main, stable on main
380
+ ```
381
+
331
382
  Everything a project needs is declared in one file:
332
383
 
333
384
  ```toml
@@ -336,6 +387,7 @@ name = "orders"
336
387
  type = "web"
337
388
  stack = "python"
338
389
  template = "fastapi"
390
+ ci = "github"
339
391
  language = "python"
340
392
 
341
393
  [source_host]
@@ -351,7 +403,7 @@ postgres = "aws-rds"
351
403
 
352
404
  ## Use it from an AI client
353
405
 
354
- The platform ships as an MCP server. Claude Code, Codex, Cursor — anything that speaks MCP — gets `list_matrix`, `init_project`, `cloud_set`, `service_add`, `release`, `deploy`, `diagnose` as tools, plus skills that make the agent preview and ask before pushing or deploying.
406
+ The platform ships as an MCP server. Claude Code, Codex, Cursor — anything that speaks MCP — gets 17 tools (`list_matrix`, `init_project`, `install_platform`, `start_branch`, `gitflow_audit`, `propose_pull_request`, `release`, `deploy`, `diagnose`, …), 6 prompts that put them in the right order (`new_service`, `ship_feature`, `cut_release`, `deploy_project`, `adopt_repository`, `fix_gitflow`) and 12 skills that make the agent preview and ask before anything leaves the machine.
355
407
 
356
408
  ```bash
357
409
  pip install "action-platform[mcp]"
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>From idea to production in one command — on any stack, on any cloud.</strong>
6
+ <strong>Your platform team, as a CLI.</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -76,13 +76,64 @@ action-platform branch feature 42 login # develop (or main) → pull
76
76
  action-platform branch hotfix PROJ-7 # from main/master
77
77
  action-platform gitflow # audit current branch + commits; --install-hooks
78
78
  action-platform pr [--draft] [--dry-run] # PR for the current branch: target from git-flow, body from commits
79
- action-platform release patch # bump, changelog, tag, GitHub release
79
+ action-platform release patch # bump, changelog, tag, GitHub release; off main/master → X.Y.Z-rc.N pre-release
80
80
  action-platform deploy --stage prod
81
81
  action-platform rollback
82
82
  action-platform diagnose
83
83
  action-platform destroy
84
84
  ```
85
85
 
86
+ ## Git-flow, enforced
87
+
88
+ Every project follows the same flow. Git hooks refuse the wrong move before it exists; CI refuses it on the pull request; the CLI and the MCP tools guide the right one.
89
+
90
+ ```mermaid
91
+ gitGraph
92
+ commit id: "chore: bootstrap"
93
+ branch develop
94
+ checkout develop
95
+ commit id: "chore(release): 0.3.2-rc.1" tag: "v0.3.2-rc.1"
96
+ branch feature/42-login
97
+ checkout feature/42-login
98
+ commit id: "feat(login): form"
99
+ commit id: "test(login): cover form"
100
+ checkout develop
101
+ merge feature/42-login id: "PR #1 → develop"
102
+ branch release/0.3.2
103
+ checkout release/0.3.2
104
+ commit id: "chore(release): 0.3.2-rc.2" tag: "v0.3.2-rc.2"
105
+ checkout main
106
+ merge release/0.3.2 id: "PR #2 → main"
107
+ commit id: "chore(release): 0.3.2" tag: "v0.3.2"
108
+ checkout develop
109
+ merge main id: "back-merge"
110
+ checkout main
111
+ branch hotfix/PROJ-7
112
+ checkout hotfix/PROJ-7
113
+ commit id: "fix(auth): expiry"
114
+ checkout main
115
+ merge hotfix/PROJ-7 id: "PR #3 → main"
116
+ commit id: "chore(release): 0.3.3" tag: "v0.3.3"
117
+ checkout develop
118
+ merge main id: "back-merge hotfix"
119
+ ```
120
+
121
+ | Branch | Starts from | Merges into | Release |
122
+ |--------|-------------|-------------|---------|
123
+ | `feature/<code>`, `bugfix/…`, `chore/…`, `docs/…`, `refactor/…`, `test/…`, `ci/…`, `perf/…` | `develop` (or the default branch when there is no `develop`) | `develop` | `X.Y.Z-rc.N` pre-release |
124
+ | `release/<version>` | `develop` | `main` and `develop` | `X.Y.Z-rc.N` until merged |
125
+ | `hotfix/<code>` | `main` | `main` and `develop` | `X.Y.Z-rc.N` until merged |
126
+ | `main` / `master` | — | — | stable `X.Y.Z` → PyPI / npm / … |
127
+
128
+ Rules the hooks and CI apply: branch names are `<kind>/<code>[-slug]`; commits are [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/); no direct commits on `main`, `master` or `develop` except `chore(release):`, `chore(platform):` and the bootstrap commit; a pull request may only target what the table allows.
129
+
130
+ ```bash
131
+ action-platform branch feature 42 login # develop → pull → feature/42-login → push
132
+ action-platform gitflow # audit branch + commits
133
+ action-platform pr # target and body from the rules and the commits
134
+ action-platform release patch # rc off main, stable on main
135
+ ```
136
+
86
137
  Everything a project needs is declared in one file:
87
138
 
88
139
  ```toml
@@ -91,6 +142,7 @@ name = "orders"
91
142
  type = "web"
92
143
  stack = "python"
93
144
  template = "fastapi"
145
+ ci = "github"
94
146
  language = "python"
95
147
 
96
148
  [source_host]
@@ -106,7 +158,7 @@ postgres = "aws-rds"
106
158
 
107
159
  ## Use it from an AI client
108
160
 
109
- The platform ships as an MCP server. Claude Code, Codex, Cursor — anything that speaks MCP — gets `list_matrix`, `init_project`, `cloud_set`, `service_add`, `release`, `deploy`, `diagnose` as tools, plus skills that make the agent preview and ask before pushing or deploying.
161
+ The platform ships as an MCP server. Claude Code, Codex, Cursor — anything that speaks MCP — gets 17 tools (`list_matrix`, `init_project`, `install_platform`, `start_branch`, `gitflow_audit`, `propose_pull_request`, `release`, `deploy`, `diagnose`, …), 6 prompts that put them in the right order (`new_service`, `ship_feature`, `cut_release`, `deploy_project`, `adopt_repository`, `fix_gitflow`) and 12 skills that make the agent preview and ask before anything leaves the machine.
110
162
 
111
163
  ```bash
112
164
  pip install "action-platform[mcp]"
@@ -1,6 +1,6 @@
1
1
  """Action Platform __init__ module."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
4
4
  __description__ = "🛠️ Action Platform padroniza init, release e deploy."
5
5
 
6
6
  from .core.config import Config
@@ -17,7 +17,11 @@ def run(
17
17
  language: str | None = typer.Option(
18
18
  None, "--language", help="python, go, node, php, java, rust (default: detected)"
19
19
  ),
20
- ci: str = typer.Option("github", "--ci", help="github, gitlab, jenkins"),
20
+ ci: str | None = typer.Option(
21
+ None,
22
+ "--ci",
23
+ help="github, gitlab, jenkins (default: platform.toml, else github)",
24
+ ),
21
25
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be created"),
22
26
  ) -> None:
23
27
  """Install the platform in this repository: platform.toml, code quality, CI files (never overwritten) and git hooks into .git/hooks (always refreshed)."""
@@ -14,9 +14,14 @@ from action_platform.logging import logger
14
14
  def run(
15
15
  level: str = typer.Argument("patch", help="patch | minor | major | X.Y.Z"),
16
16
  dry_run: bool = typer.Option(False, "--dry-run"),
17
+ rc: bool | None = typer.Option(
18
+ None,
19
+ "--rc/--stable",
20
+ help="Pre-release X.Y.Z-rc.N (default: rc off main/master, stable on them)",
21
+ ),
17
22
  ) -> None:
18
- """Bump version, generate changelog, tag, and publish release."""
23
+ """Bump version, generate changelog, tag, and publish release. Off main/master it cuts an rc."""
19
24
  config = Config.from_toml(Path.cwd() / "platform.toml")
20
25
  tool = ActionPlatform(config=config)
21
- ctx = tool.release(level=level, dry_run=dry_run)
26
+ ctx = tool.release(level=level, dry_run=dry_run, prerelease=rc)
22
27
  logger.info("release done: %s", ctx.next_version)
@@ -34,8 +34,15 @@ class ActionPlatform:
34
34
  self.config = config or Config()
35
35
  self.repo_root = repo_root or Path.cwd()
36
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)
37
+ def release(
38
+ self,
39
+ level: str = "patch",
40
+ dry_run: bool = False,
41
+ prerelease: bool | None = None,
42
+ ) -> Context:
43
+ return pipeline.release(
44
+ self.config, level, self.repo_root, dry_run=dry_run, prerelease=prerelease
45
+ )
39
46
 
40
47
  def deploy(
41
48
  self, target: str | None = None, dry_run: bool = False, stage: str | None = None
@@ -96,3 +96,20 @@ def add_remote(url: str, cwd: Path, remote: str = "origin") -> None:
96
96
 
97
97
  def push_upstream(branch: str, cwd: Path, remote: str = "origin") -> None:
98
98
  run(["push", "-u", remote, branch], cwd=cwd)
99
+
100
+
101
+ def tags(cwd: Path | None = None) -> list[str]:
102
+ out = run(["tag", "--list"], cwd=cwd)
103
+
104
+ return [t for t in out.split("\n") if t]
105
+
106
+
107
+ def remote_tag_exists(
108
+ tag: str, cwd: Path | None = None, remote: str = "origin"
109
+ ) -> bool:
110
+ try:
111
+ out = run(["ls-remote", "--tags", remote, tag], cwd=cwd)
112
+ except subprocess.CalledProcessError:
113
+ return False
114
+
115
+ return bool(out.strip())
@@ -73,7 +73,7 @@ def install(
73
73
  root: Path,
74
74
  type_: str = "web",
75
75
  language: str | None = None,
76
- ci: str = "github",
76
+ ci: str | None = None,
77
77
  dry_run: bool = False,
78
78
  ) -> Plan:
79
79
  root = root.resolve()
@@ -81,6 +81,7 @@ def install(
81
81
  if not (root / ".git").exists():
82
82
  raise InstallError(f"{root} is not a git repository")
83
83
 
84
+ ci = ci or _existing_ci(root) or "github"
84
85
  language = language or detect_language(root)
85
86
 
86
87
  if language is None:
@@ -96,7 +97,9 @@ def install(
96
97
  source = _source_leaf(repo, matrix, language)
97
98
  plan = Plan(root=root, language=language, type=type_, ci=ci)
98
99
 
99
- _write(plan, settings.CONFIG_FILE, _platform_toml(root, type_, language), dry_run)
100
+ _write(
101
+ plan, settings.CONFIG_FILE, _platform_toml(root, type_, language, ci), dry_run
102
+ )
100
103
  _write(plan, settings.LAST_VERSION_FILE, "0.1.0\n", dry_run)
101
104
  _write(plan, "AGENTS.md", AGENTS, dry_run)
102
105
 
@@ -133,16 +136,28 @@ def _language_of(leaf_dir: Path) -> str:
133
136
  return ""
134
137
 
135
138
 
136
- def _platform_toml(root: Path, type_: str, language: str) -> str:
139
+ def _existing_ci(root: Path) -> str | None:
140
+ path = root / settings.CONFIG_FILE
141
+
142
+ if not path.exists():
143
+ return None
144
+
145
+ import tomllib
146
+
147
+ try:
148
+ return tomllib.loads(path.read_text()).get("project", {}).get("ci") or None
149
+ except tomllib.TOMLDecodeError:
150
+ return None
151
+
152
+
153
+ def _platform_toml(root: Path, type_: str, language: str, ci: str) -> str:
137
154
  remote = git.remote_url(cwd=root)
138
155
  repo = ""
139
156
 
140
157
  if "github.com" in remote:
141
158
  repo = remote.split("github.com", 1)[1].strip(":/").removesuffix(".git")
142
159
 
143
- text = (
144
- f'[project]\nname = "{root.name}"\ntype = "{type_}"\nlanguage = "{language}"\n'
145
- )
160
+ text = f'[project]\nname = "{root.name}"\ntype = "{type_}"\nci = "{ci}"\nlanguage = "{language}"\n'
146
161
 
147
162
  if repo:
148
163
  text += f'\n[source_host]\nkind = "github"\nrepo = "{repo}"\n'
@@ -27,6 +27,24 @@ def build_context(
27
27
  )
28
28
 
29
29
 
30
+ def _next_version(current: str, level: str, prerelease: bool, repo_root: Path) -> str:
31
+ """Stable: plain bump. Pre-release: bump the stable base (or keep it when already on an rc) and add -rc.N."""
32
+ base = versioning.strip_pre(current)
33
+ _, _, _, pre = versioning.parse(current)
34
+
35
+ if not prerelease:
36
+ return versioning.bump(base, level)
37
+
38
+ if versioning.SEMVER_RE.match(level):
39
+ target = versioning.strip_pre(level)
40
+ elif pre:
41
+ target = base
42
+ else:
43
+ target = versioning.bump(base, level)
44
+
45
+ return versioning.next_rc(target, git.tags(cwd=repo_root))
46
+
47
+
30
48
  def _targets(config: Config, target_name: str | None):
31
49
  targets = (
32
50
  config.deploy
@@ -44,15 +62,35 @@ def _targets(config: Config, target_name: str | None):
44
62
 
45
63
 
46
64
  def release(
47
- config: Config, level: str, repo_root: Path, dry_run: bool = False
65
+ config: Config,
66
+ level: str,
67
+ repo_root: Path,
68
+ dry_run: bool = False,
69
+ prerelease: bool | None = None,
48
70
  ) -> Context:
49
71
  ctx = build_context(config, repo_root, dry_run=dry_run)
50
72
 
51
73
  if not git.is_clean(cwd=repo_root):
52
74
  raise ReleaseError("working tree is dirty")
53
75
 
54
- ctx.next_version = versioning.bump(ctx.current_version, level)
55
- logger.info("bump %s -> %s", ctx.current_version, ctx.next_version)
76
+ if prerelease is None:
77
+ prerelease = ctx.branch not in {"main", "master"}
78
+
79
+ ctx.next_version = _next_version(ctx.current_version, level, prerelease, repo_root)
80
+ tag = f"v{ctx.next_version}"
81
+
82
+ if ctx.next_version == ctx.current_version:
83
+ raise ReleaseError(f"{ctx.current_version} is already the current version")
84
+
85
+ if tag in git.tags(cwd=repo_root) or git.remote_tag_exists(tag, cwd=repo_root):
86
+ raise ReleaseError(f"tag {tag} already exists")
87
+
88
+ logger.info(
89
+ "bump %s -> %s%s",
90
+ ctx.current_version,
91
+ ctx.next_version,
92
+ " (pre-release)" if prerelease else "",
93
+ )
56
94
 
57
95
  commits = git.commits_since(git.latest_tag(cwd=repo_root), cwd=repo_root)
58
96
  ctx.changelog = changelog.render(ctx.next_version, commits)
@@ -63,17 +101,20 @@ def release(
63
101
 
64
102
  versioning.write(repo_root / settings.LAST_VERSION_FILE, ctx.next_version)
65
103
  changelog.prepend(repo_root / settings.CHANGELOG_FILE, ctx.changelog)
104
+ synced = versioning.sync_files(repo_root, ctx.next_version)
66
105
 
67
- tag = f"v{ctx.next_version}"
68
-
69
- git.add([settings.LAST_VERSION_FILE, settings.CHANGELOG_FILE], cwd=repo_root)
106
+ git.add(
107
+ [settings.LAST_VERSION_FILE, settings.CHANGELOG_FILE, *synced], cwd=repo_root
108
+ )
70
109
  git.commit(f"chore(release): {ctx.next_version}", cwd=repo_root)
71
110
  git.create_tag(tag, tag, cwd=repo_root)
72
111
  git.push(cwd=repo_root)
73
112
  git.push_tag(tag, cwd=repo_root)
74
113
 
75
114
  if config.source_host:
76
- config.source_host.create_release(ctx, tag=tag, notes=ctx.changelog)
115
+ config.source_host.create_release(
116
+ ctx, tag=tag, notes=ctx.changelog, prerelease=prerelease
117
+ )
77
118
 
78
119
  for runner in config.ci:
79
120
  run = runner.trigger(
@@ -0,0 +1,98 @@
1
+ """Semver bump + LAST_VERSION management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from action_platform.core.exception import ActionPlatformError
9
+
10
+ SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$")
11
+
12
+
13
+ def parse(version: str) -> tuple[int, int, int, str | None]:
14
+ match = SEMVER_RE.match(version.strip().lstrip("v"))
15
+ if not match:
16
+ raise ActionPlatformError(f"Invalid semver: {version!r}")
17
+ major, minor, patch, pre = match.groups()
18
+ return int(major), int(minor), int(patch), pre
19
+
20
+
21
+ def format(major: int, minor: int, patch: int, pre: str | None = None) -> str:
22
+ base = f"{major}.{minor}.{patch}"
23
+ return f"{base}-{pre}" if pre else base
24
+
25
+
26
+ def bump(version: str, level: str) -> str:
27
+ major, minor, patch, _ = parse(version)
28
+ if level == "major":
29
+ return format(major + 1, 0, 0)
30
+ if level == "minor":
31
+ return format(major, minor + 1, 0)
32
+ if level == "patch":
33
+ return format(major, minor, patch + 1)
34
+ parse(level)
35
+ return level
36
+
37
+
38
+ def strip_pre(version: str) -> str:
39
+ major, minor, patch, _ = parse(version)
40
+
41
+ return format(major, minor, patch)
42
+
43
+
44
+ def next_rc(base: str, existing_tags: list[str]) -> str:
45
+ """`base` plus the next -rc.N not yet tagged."""
46
+ prefix = f"v{base}-rc."
47
+ taken = [
48
+ int(t[len(prefix) :])
49
+ for t in existing_tags
50
+ if t.startswith(prefix) and t[len(prefix) :].isdigit()
51
+ ]
52
+
53
+ return f"{base}-rc.{max(taken, default=0) + 1}"
54
+
55
+
56
+ def read(path: Path) -> str:
57
+ return path.read_text().strip() if path.exists() else "0.0.0"
58
+
59
+
60
+ def write(path: Path, version: str) -> None:
61
+ path.write_text(f"{version}\n")
62
+
63
+
64
+ VERSION_PATTERNS = {
65
+ "pyproject.toml": re.compile(r'^(version\s*=\s*")[^"]*(")', re.M),
66
+ "package.json": re.compile(r'^(\s*"version":\s*")[^"]*(")', re.M),
67
+ "Cargo.toml": re.compile(r'^(version\s*=\s*")[^"]*(")', re.M),
68
+ "composer.json": re.compile(r'^(\s*"version":\s*")[^"]*(")', re.M),
69
+ }
70
+ INIT_RE = re.compile(r'^(__version__\s*=\s*")[^"]*(")', re.M)
71
+
72
+
73
+ def sync_files(root: Path, version: str) -> list[str]:
74
+ """Write `version` into every manifest and top-level `__version__` found. Returns the files touched."""
75
+ touched = []
76
+
77
+ for name, pattern in VERSION_PATTERNS.items():
78
+ path = root / name
79
+
80
+ if not path.exists():
81
+ continue
82
+
83
+ text = path.read_text()
84
+ updated = pattern.sub(rf"\g<1>{version}\g<2>", text)
85
+
86
+ if updated != text:
87
+ path.write_text(updated)
88
+ touched.append(name)
89
+
90
+ for init in sorted(root.glob("*/__init__.py")):
91
+ text = init.read_text()
92
+ updated = INIT_RE.sub(rf"\g<1>{version}\g<2>", text)
93
+
94
+ if updated != text:
95
+ init.write_text(updated)
96
+ touched.append(str(init.relative_to(root)))
97
+
98
+ return touched
@@ -0,0 +1,136 @@
1
+ """Prompts that put the tools in the order a platform task actually needs.
2
+
3
+ The costly mistakes live in the order: pushing before the user saw the
4
+ repository name, deploying before preflight, opening a pull request from a
5
+ branch that fails git-flow, releasing from the wrong branch.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ def register(mcp: Any) -> None:
14
+ @mcp.prompt(
15
+ title="New service",
16
+ description="Scaffold a service from the templates, add its cloud and dependencies, push on approval.",
17
+ )
18
+ def new_service(
19
+ name: str = "", stack: str = "python", cloud: str = "", services: str = ""
20
+ ) -> str:
21
+ return (
22
+ "Create a new service on the platform.\n\n"
23
+ f"Name: {name or 'ask the user'}. Stack: {stack}. Cloud: {cloud or 'none yet'}. "
24
+ f"Dependencies: {services or 'none'}.\n\n"
25
+ "1. Call list_matrix and pick the `web` template for the stack; when "
26
+ "several exist, ask which unless the user named one.\n"
27
+ "2. Call init_project with type=web, the stack, the name and ci=github "
28
+ "unless the user uses GitLab or Jenkins. Pass `cloud` only when one was named.\n"
29
+ "3. For each dependency call service_add; pick the provider from context "
30
+ "(docker locally, aws-* when the cloud is AWS).\n"
31
+ "4. Show project_info and the files that matter: platform.toml, DEPLOY.md, "
32
+ "requirements/ when a cloud was added.\n"
33
+ "5. Say the repository that push_project would create — owner/name, public "
34
+ "or private — and wait for a yes before calling it.\n\n"
35
+ "Never call push_project in the same turn as init_project."
36
+ )
37
+
38
+ @mcp.prompt(
39
+ title="Ship a feature",
40
+ description="Start a git-flow branch, work, audit, and open the pull request on approval.",
41
+ )
42
+ def ship_feature(issue: str = "", summary: str = "") -> str:
43
+ return (
44
+ "Take a change from idea to pull request under git-flow.\n\n"
45
+ f"Issue or ticket: {issue or 'ask the user — a branch needs a code'}. "
46
+ f"Summary: {summary or 'ask what the change is'}.\n\n"
47
+ "1. Decide the kind from the intent: new capability → feature, bug in "
48
+ "development → bugfix, production incident → hotfix, everything else by "
49
+ "its Conventional Commit type (chore, docs, refactor, test, ci, perf). "
50
+ "gitflow_rules lists them.\n"
51
+ "2. Call start_branch with the kind, the code and a two-word slug. It picks "
52
+ "develop or main as base; never commit on those directly.\n"
53
+ "3. Make the change in small Conventional Commits: `type(scope): description`, "
54
+ "one concern per commit, files staged explicitly.\n"
55
+ "4. Call gitflow_audit. Fix every problem it lists before going on.\n"
56
+ "5. Call propose_pull_request and show head → base, title and body.\n"
57
+ "6. On approval call open_pull_request. For release or hotfix branches, "
58
+ "remind the user a second pull request into develop follows the merge."
59
+ )
60
+
61
+ @mcp.prompt(
62
+ title="Cut a release",
63
+ description="Preview the next version and changelog, publish only on approval; rc off main.",
64
+ )
65
+ def cut_release(level: str = "patch") -> str:
66
+ return (
67
+ f"Cut a {level} release.\n\n"
68
+ "1. Call project_info and gitflow_audit; a dirty tree or a branch that "
69
+ "fails git-flow stops here — say what to fix.\n"
70
+ "2. Call release with dry_run=true. Report the current and next version "
71
+ "and the changelog. On main/master the version is stable; on any other "
72
+ "branch it is X.Y.Z-rc.N and the release is marked pre-release — say which "
73
+ "case applies and why.\n"
74
+ "3. An empty changelog means the commits are not Conventional Commits; "
75
+ "stop and explain instead of releasing an empty entry.\n"
76
+ "4. On approval call release again with dry_run=false and report the tag "
77
+ "and the release URL. Mention that the publish workflow runs from the "
78
+ "release (PyPI, npm, Packagist… for stable; the test index for rc)."
79
+ )
80
+
81
+ @mcp.prompt(
82
+ title="Deploy",
83
+ description="Preflight, deploy on approval, verify with diagnose.",
84
+ )
85
+ def deploy_project(stage: str = "") -> str:
86
+ return (
87
+ "Deploy the current version.\n\n"
88
+ f"Stage: {stage or 'from the branch — main/master is prod, anything else dev'}.\n\n"
89
+ "1. Call project_info; without a [deploy] target offer cloud_set and stop.\n"
90
+ "2. Call deploy with dry_run=true. That is preflight: tooling, template "
91
+ "validation, credentials. Relay any error verbatim; 'no provider installed' "
92
+ "means the target's provider package is missing.\n"
93
+ "3. Say the target, stage and version that would go live and wait for a yes.\n"
94
+ "4. Call deploy with dry_run=false, then diagnose. Report status and URL.\n"
95
+ "5. If diagnose is not ok, propose rollback — and confirm before calling it."
96
+ )
97
+
98
+ @mcp.prompt(
99
+ title="Adopt an existing repository",
100
+ description="Bring a repo that was not generated by the platform onto it, without touching its code.",
101
+ )
102
+ def adopt_repository(ci: str = "github") -> str:
103
+ return (
104
+ "Install the platform in an existing repository.\n\n"
105
+ f"CI: {ci}.\n\n"
106
+ "1. Call install_platform with dry_run=true. Show created and kept: "
107
+ "nothing that exists is overwritten, and app code and deploy files are "
108
+ "not touched.\n"
109
+ "2. Explain what changes for the team from that moment: Conventional "
110
+ "Commits, work on <kind>/<code> branches, main/develop refuse direct "
111
+ "commits. If the repo used another commit style, say so before proceeding.\n"
112
+ "3. On approval call install_platform with dry_run=false. Hooks are "
113
+ "installed into .git/hooks; every other clone runs install_hooks once.\n"
114
+ "4. Suggest the commit `chore(platform): install action-platform` on the "
115
+ "default branch — one of the exceptions the hooks allow — then gitflow_audit."
116
+ )
117
+
118
+ @mcp.prompt(
119
+ title="Fix git-flow",
120
+ description="Diagnose and repair a branch that violates git-flow before it reaches a pull request.",
121
+ )
122
+ def fix_gitflow() -> str:
123
+ return (
124
+ "Repair git-flow violations on the current branch.\n\n"
125
+ "1. Call gitflow_audit and gitflow_rules. Map each problem to a fix:\n"
126
+ " - wrong branch name → `git branch -m <kind>/<code>-slug`\n"
127
+ " - commits on main/develop → start_branch from the same base, "
128
+ "cherry-pick them, reset the protected branch to origin\n"
129
+ " - non-conventional messages → `git commit --amend` for the last one, "
130
+ "`git rebase -i` to reword older ones\n"
131
+ "2. Every fix rewrites history: show the exact commands and the new "
132
+ "messages, and wait for a yes. Never rewrite commits already pushed to a "
133
+ "shared branch without saying so.\n"
134
+ "3. If violations got through, the hooks are missing: call install_hooks.\n"
135
+ "4. Re-run gitflow_audit until it reports ok."
136
+ )
@@ -8,6 +8,7 @@ from typing import Optional
8
8
  from mcp.server.mcpserver import MCPServer
9
9
 
10
10
  from action_platform import __version__
11
+ from action_platform.mcp import prompts
11
12
  from action_platform.mcp.tools import flow, lifecycle, matrix, project
12
13
 
13
14
  INSTRUCTIONS = """Scaffold, deploy and operate projects on the Action Platform.
@@ -35,6 +36,7 @@ def build() -> MCPServer:
35
36
  project.register(mcp)
36
37
  flow.register(mcp)
37
38
  lifecycle.register(mcp)
39
+ prompts.register(mcp)
38
40
 
39
41
  return mcp
40
42
 
@@ -45,7 +45,8 @@ def register(mcp: Any) -> None:
45
45
  """Bump version, write CHANGELOG, tag, push and publish a release.
46
46
 
47
47
  Defaults to a dry run. Show the user the next version and changelog,
48
- then call again with dry_run=false to publish.
48
+ then call again with dry_run=false to publish. Off main/master the
49
+ version becomes X.Y.Z-rc.N and the release is marked pre-release.
49
50
  """
50
51
  ctx = _tool(project).release(level=level, dry_run=dry_run)
51
52
 
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "action-platform"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  authors = [
5
5
  { name="Fernando Celmer", email="email@fernandocelmer.com" },
6
6
  ]
7
7
  license = {file = "LICENSE"}
8
- description = "From idea to production in one command: scaffold, CI, release and deploy on any stack and cloud, with git-flow and Conventional Commits enforced and an MCP server for AI agents."
8
+ description = "Your platform team, as a CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
@@ -71,8 +71,8 @@ mcp = ["mcp>=2.0"]
71
71
 
72
72
  [tool.poetry]
73
73
  name = "action-platform"
74
- version = "0.1.2"
75
- description = "From idea to production in one command: scaffold, CI, release and deploy on any stack and cloud, with git-flow and Conventional Commits enforced and an MCP server for AI agents."
74
+ version = "0.2.0"
75
+ description = "Your platform team, as a CLI."
76
76
  authors = ["Fernando Celmer <email@fernandocelmer.com>"]
77
77
  readme = "README.md"
78
78
  license = "Apache-2.0"
@@ -1,43 +0,0 @@
1
- """Semver bump + LAST_VERSION management."""
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
- from pathlib import Path
7
-
8
- from action_platform.core.exception import ActionPlatformError
9
-
10
- SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$")
11
-
12
-
13
- def parse(version: str) -> tuple[int, int, int, str | None]:
14
- match = SEMVER_RE.match(version.strip().lstrip("v"))
15
- if not match:
16
- raise ActionPlatformError(f"Invalid semver: {version!r}")
17
- major, minor, patch, pre = match.groups()
18
- return int(major), int(minor), int(patch), pre
19
-
20
-
21
- def format(major: int, minor: int, patch: int, pre: str | None = None) -> str:
22
- base = f"{major}.{minor}.{patch}"
23
- return f"{base}-{pre}" if pre else base
24
-
25
-
26
- def bump(version: str, level: str) -> str:
27
- major, minor, patch, _ = parse(version)
28
- if level == "major":
29
- return format(major + 1, 0, 0)
30
- if level == "minor":
31
- return format(major, minor + 1, 0)
32
- if level == "patch":
33
- return format(major, minor, patch + 1)
34
- parse(level)
35
- return level
36
-
37
-
38
- def read(path: Path) -> str:
39
- return path.read_text().strip() if path.exists() else "0.0.0"
40
-
41
-
42
- def write(path: Path, version: str) -> None:
43
- path.write_text(f"{version}\n")
File without changes