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.
- {action_platform-0.1.2 → action_platform-0.2.0}/PKG-INFO +57 -5
- {action_platform-0.1.2 → action_platform-0.2.0}/README.md +55 -3
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/__init__.py +1 -1
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/install.py +5 -1
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/release.py +7 -2
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/action_platform.py +9 -2
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/git.py +17 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/install.py +21 -6
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/pipeline.py +48 -7
- action_platform-0.2.0/action_platform/core/versioning.py +98 -0
- action_platform-0.2.0/action_platform/mcp/prompts.py +136 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/server.py +2 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/lifecycle.py +2 -1
- {action_platform-0.1.2 → action_platform-0.2.0}/pyproject.toml +4 -4
- action_platform-0.1.2/action_platform/core/versioning.py +0 -43
- {action_platform-0.1.2 → action_platform-0.2.0}/LICENSE +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/ci_runner.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/deploy_target.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/abc/source_host.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/branch.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/cloud.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/deploy.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/gitflow.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/init.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/mcp.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/pr.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/commands/service.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/cli/setup.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/branching.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/changelog.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/config.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/context.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/exception.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/generate.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/gitflow.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/module.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/pullrequest.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/core/templates.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/commit-msg +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/gitflow.sh +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/pre-commit +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/hooks/pre-push +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/logging.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/main.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/__main__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/annotations.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/flow.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/matrix.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/mcp/tools/project.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/providers/__init__.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/providers/source_github.py +0 -0
- {action_platform-0.1.2 → action_platform-0.2.0}/action_platform/settings.py +0 -0
- {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.
|
|
4
|
-
Summary:
|
|
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>
|
|
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`, `
|
|
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>
|
|
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`, `
|
|
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]"
|
|
@@ -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(
|
|
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(
|
|
38
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
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.
|
|
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 = "
|
|
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.
|
|
75
|
-
description = "
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|