verkit 0.1.1__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.
- verkit-0.1.1/.github/workflows/ci.yml +36 -0
- verkit-0.1.1/.github/workflows/publish.yml +41 -0
- verkit-0.1.1/.gitignore +7 -0
- verkit-0.1.1/PKG-INFO +88 -0
- verkit-0.1.1/README.md +78 -0
- verkit-0.1.1/pyproject.toml +24 -0
- verkit-0.1.1/src/verkit/__init__.py +14 -0
- verkit-0.1.1/src/verkit/cli.py +73 -0
- verkit-0.1.1/src/verkit/inspector.py +142 -0
- verkit-0.1.1/src/verkit/promoter.py +305 -0
- verkit-0.1.1/src/verkit/tagger.py +162 -0
- verkit-0.1.1/tests/test_inspector.py +20 -0
- verkit-0.1.1/tests/test_promoter.py +7 -0
- verkit-0.1.1/uv.lock +80 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
# https://github.com/actions/checkout
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
# https://github.com/astral-sh/setup-uv
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v7
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Set up Python
|
|
30
|
+
run: uv python install
|
|
31
|
+
|
|
32
|
+
- name: Run ruff
|
|
33
|
+
run: uv run ruff check .
|
|
34
|
+
|
|
35
|
+
- name: Run mypy
|
|
36
|
+
run: uv run mypy src
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Build and publish
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
contents: write
|
|
18
|
+
attestations: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
# https://github.com/actions/checkout
|
|
22
|
+
- uses: actions/checkout@v6
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
# https://github.com/astral-sh/setup-uv
|
|
27
|
+
- name: Install uv
|
|
28
|
+
uses: astral-sh/setup-uv@v7
|
|
29
|
+
with:
|
|
30
|
+
enable-cache: true
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
run: uv python install
|
|
34
|
+
|
|
35
|
+
- name: Build package
|
|
36
|
+
run: uv build
|
|
37
|
+
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
with:
|
|
41
|
+
verbose: true
|
verkit-0.1.1/.gitignore
ADDED
verkit-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verkit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Polyglot project version inspector, promoter, tagger, and release workflow engine
|
|
5
|
+
Author: Mark Stouffer
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: rich>=10.0.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# verkit
|
|
12
|
+
|
|
13
|
+
Polyglot project version inspector, promoter, tagger, and release workflow engine.
|
|
14
|
+
|
|
15
|
+
`verkit` provides a unified Python API and CLI tool to manage version releases across multi-language projects (`pyproject.toml`, `package.json`, `Cargo.toml`, `*.csproj`, `pom.xml`, `build.gradle`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Supported Ecosystems
|
|
20
|
+
| Project Type | Version File | Parser / Bump Engine |
|
|
21
|
+
| :--- | :--- | :--- |
|
|
22
|
+
| **Python** | `pyproject.toml` | Regex & `bump-my-version` |
|
|
23
|
+
| **Node.js** | `package.json` | JSON parser & `npm version` |
|
|
24
|
+
| **Rust** | `Cargo.toml` | Regex (`^version = "..."`) |
|
|
25
|
+
| **.NET** | `*.csproj` | XML Regex (`<Version>...</Version>`) |
|
|
26
|
+
| **Java (Maven)** | `pom.xml` | XML Regex (`<version>...</version>`) |
|
|
27
|
+
| **Java (Gradle)** | `build.gradle` / `.kts` | Regex (`version = "..."`) |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
```bash
|
|
33
|
+
pip install verkit
|
|
34
|
+
# or using uv
|
|
35
|
+
uv add verkit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## CLI Usage
|
|
41
|
+
|
|
42
|
+
### Inspect version info
|
|
43
|
+
Inspect current working tree and HEAD committed versions:
|
|
44
|
+
```bash
|
|
45
|
+
verkit inspect
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Promote version
|
|
49
|
+
Bump version (`major`, `minor`, or `patch`) and automatically commit or amend local release commit:
|
|
50
|
+
```bash
|
|
51
|
+
verkit promote minor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Tag and push release
|
|
55
|
+
Verify HEAD is in sync, create `vX.Y.Z` git tag, and push branch + tag:
|
|
56
|
+
```bash
|
|
57
|
+
verkit tag
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### One-shot atomic release
|
|
61
|
+
Combine promote, tag, and push in a single step:
|
|
62
|
+
```bash
|
|
63
|
+
verkit release patch
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Python API Usage
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import verkit
|
|
72
|
+
|
|
73
|
+
# 1. Inspect version info
|
|
74
|
+
info = verkit.inspect_project()
|
|
75
|
+
print(f"Working version: {info.version} (from {info.source})")
|
|
76
|
+
|
|
77
|
+
committed = verkit.inspect_committed()
|
|
78
|
+
print(f"HEAD committed version: {committed.version}")
|
|
79
|
+
|
|
80
|
+
# 2. Promote version
|
|
81
|
+
new_version = verkit.promote_version("minor")
|
|
82
|
+
|
|
83
|
+
# 3. Tag and push release
|
|
84
|
+
tag_name = verkit.tag_version(push=True)
|
|
85
|
+
|
|
86
|
+
# 4. Atomic release
|
|
87
|
+
verkit.release_version("patch", push=True)
|
|
88
|
+
```
|
verkit-0.1.1/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# verkit
|
|
2
|
+
|
|
3
|
+
Polyglot project version inspector, promoter, tagger, and release workflow engine.
|
|
4
|
+
|
|
5
|
+
`verkit` provides a unified Python API and CLI tool to manage version releases across multi-language projects (`pyproject.toml`, `package.json`, `Cargo.toml`, `*.csproj`, `pom.xml`, `build.gradle`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Supported Ecosystems
|
|
10
|
+
| Project Type | Version File | Parser / Bump Engine |
|
|
11
|
+
| :--- | :--- | :--- |
|
|
12
|
+
| **Python** | `pyproject.toml` | Regex & `bump-my-version` |
|
|
13
|
+
| **Node.js** | `package.json` | JSON parser & `npm version` |
|
|
14
|
+
| **Rust** | `Cargo.toml` | Regex (`^version = "..."`) |
|
|
15
|
+
| **.NET** | `*.csproj` | XML Regex (`<Version>...</Version>`) |
|
|
16
|
+
| **Java (Maven)** | `pom.xml` | XML Regex (`<version>...</version>`) |
|
|
17
|
+
| **Java (Gradle)** | `build.gradle` / `.kts` | Regex (`version = "..."`) |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
```bash
|
|
23
|
+
pip install verkit
|
|
24
|
+
# or using uv
|
|
25
|
+
uv add verkit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## CLI Usage
|
|
31
|
+
|
|
32
|
+
### Inspect version info
|
|
33
|
+
Inspect current working tree and HEAD committed versions:
|
|
34
|
+
```bash
|
|
35
|
+
verkit inspect
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Promote version
|
|
39
|
+
Bump version (`major`, `minor`, or `patch`) and automatically commit or amend local release commit:
|
|
40
|
+
```bash
|
|
41
|
+
verkit promote minor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Tag and push release
|
|
45
|
+
Verify HEAD is in sync, create `vX.Y.Z` git tag, and push branch + tag:
|
|
46
|
+
```bash
|
|
47
|
+
verkit tag
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### One-shot atomic release
|
|
51
|
+
Combine promote, tag, and push in a single step:
|
|
52
|
+
```bash
|
|
53
|
+
verkit release patch
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Python API Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import verkit
|
|
62
|
+
|
|
63
|
+
# 1. Inspect version info
|
|
64
|
+
info = verkit.inspect_project()
|
|
65
|
+
print(f"Working version: {info.version} (from {info.source})")
|
|
66
|
+
|
|
67
|
+
committed = verkit.inspect_committed()
|
|
68
|
+
print(f"HEAD committed version: {committed.version}")
|
|
69
|
+
|
|
70
|
+
# 2. Promote version
|
|
71
|
+
new_version = verkit.promote_version("minor")
|
|
72
|
+
|
|
73
|
+
# 3. Tag and push release
|
|
74
|
+
tag_name = verkit.tag_version(push=True)
|
|
75
|
+
|
|
76
|
+
# 4. Atomic release
|
|
77
|
+
verkit.release_version("patch", push=True)
|
|
78
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verkit"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Polyglot project version inspector, promoter, tagger, and release workflow engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Mark Stouffer" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"rich>=10.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
verkit = "verkit.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
testpaths = ["tests"]
|
|
24
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Polyglot project version inspector, promoter, tagger, and release engine."""
|
|
2
|
+
|
|
3
|
+
from verkit.inspector import VersionInfo, inspect_project, inspect_committed
|
|
4
|
+
from verkit.promoter import promote_version
|
|
5
|
+
from verkit.tagger import tag_version
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"VersionInfo",
|
|
10
|
+
"inspect_project",
|
|
11
|
+
"inspect_committed",
|
|
12
|
+
"promote_version",
|
|
13
|
+
"tag_version",
|
|
14
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from verkit.inspector import inspect_committed, inspect_project
|
|
7
|
+
from verkit.promoter import promote_version
|
|
8
|
+
from verkit.tagger import release_version, tag_version
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="verkit",
|
|
14
|
+
description="Polyglot project version inspector, promoter, tagger, and release workflow tool",
|
|
15
|
+
)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command", help="Subcommand to run")
|
|
17
|
+
|
|
18
|
+
# verkit inspect
|
|
19
|
+
subparsers.add_parser("inspect", help="Inspect working tree and HEAD version")
|
|
20
|
+
|
|
21
|
+
# verkit promote <major|minor|patch>
|
|
22
|
+
promote_p = subparsers.add_parser("promote", help="Bump version and commit/amend")
|
|
23
|
+
promote_p.add_argument("part", choices=["major", "minor", "patch"])
|
|
24
|
+
promote_p.add_argument(
|
|
25
|
+
"--no-amend",
|
|
26
|
+
dest="allow_amend",
|
|
27
|
+
action="store_false",
|
|
28
|
+
help="Always create a new commit",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# verkit tag
|
|
32
|
+
tag_p = subparsers.add_parser("tag", help="Create git tag on HEAD and push")
|
|
33
|
+
tag_p.add_argument(
|
|
34
|
+
"--no-push",
|
|
35
|
+
dest="push",
|
|
36
|
+
action="store_false",
|
|
37
|
+
help="Skip pushing to origin",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# verkit release <major|minor|patch>
|
|
41
|
+
rel_p = subparsers.add_parser("release", help="Atomic promote + tag + push")
|
|
42
|
+
rel_p.add_argument("part", choices=["major", "minor", "patch"])
|
|
43
|
+
rel_p.add_argument(
|
|
44
|
+
"--no-push",
|
|
45
|
+
dest="push",
|
|
46
|
+
action="store_false",
|
|
47
|
+
help="Promote and tag locally only",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
console = Console()
|
|
52
|
+
|
|
53
|
+
if not args.command or args.command == "inspect":
|
|
54
|
+
info = inspect_project()
|
|
55
|
+
committed = inspect_committed()
|
|
56
|
+
console.print(f"[bold blue]Working version:[/bold blue] {info.version} (from {info.source or 'unknown'})")
|
|
57
|
+
console.print(f"[dim]Committed version (HEAD):[/dim] {committed.version}")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
if args.command == "promote":
|
|
62
|
+
promote_version(args.part, console=console, allow_amend=getattr(args, "allow_amend", True))
|
|
63
|
+
elif args.command == "tag":
|
|
64
|
+
tag_version(console=console, push=getattr(args, "push", True))
|
|
65
|
+
elif args.command == "release":
|
|
66
|
+
release_version(args.part, console=console, push=getattr(args, "push", True))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class VersionInfo:
|
|
12
|
+
version: str
|
|
13
|
+
source: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_project_version(root: Optional[Path] = None) -> VersionInfo:
|
|
17
|
+
"""Read the current project version from various project files (working tree)."""
|
|
18
|
+
root = root or Path.cwd()
|
|
19
|
+
|
|
20
|
+
# Check pyproject.toml
|
|
21
|
+
pyproject = root / "pyproject.toml"
|
|
22
|
+
if pyproject.exists():
|
|
23
|
+
try:
|
|
24
|
+
with pyproject.open("r", encoding="utf-8") as f:
|
|
25
|
+
content = f.read()
|
|
26
|
+
match = re.search(r'version\s*=\s*"(.*?)"', content)
|
|
27
|
+
if match:
|
|
28
|
+
return VersionInfo(version=match.group(1), source="pyproject.toml")
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# Check package.json
|
|
33
|
+
package_json = root / "package.json"
|
|
34
|
+
if package_json.exists():
|
|
35
|
+
try:
|
|
36
|
+
with package_json.open("r", encoding="utf-8") as f:
|
|
37
|
+
data = json.load(f)
|
|
38
|
+
if "version" in data:
|
|
39
|
+
return VersionInfo(version=data["version"], source="package.json")
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Check Cargo.toml (Rust)
|
|
44
|
+
cargo = root / "Cargo.toml"
|
|
45
|
+
if cargo.exists():
|
|
46
|
+
try:
|
|
47
|
+
with cargo.open("r", encoding="utf-8") as f:
|
|
48
|
+
content = f.read()
|
|
49
|
+
match = re.search(r'^version\s*=\s*"(.*?)"', content, re.MULTILINE)
|
|
50
|
+
if match:
|
|
51
|
+
return VersionInfo(version=match.group(1), source="Cargo.toml")
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# Check *.csproj (.NET)
|
|
56
|
+
for csproj in root.glob("*.csproj"):
|
|
57
|
+
try:
|
|
58
|
+
with csproj.open("r", encoding="utf-8") as f:
|
|
59
|
+
content = f.read()
|
|
60
|
+
match = re.search(r"<Version>(.*?)</Version>", content)
|
|
61
|
+
if match:
|
|
62
|
+
return VersionInfo(version=match.group(1), source=csproj.name)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Check pom.xml (Java/Maven)
|
|
67
|
+
pom = root / "pom.xml"
|
|
68
|
+
if pom.exists():
|
|
69
|
+
try:
|
|
70
|
+
with pom.open("r", encoding="utf-8") as f:
|
|
71
|
+
content = f.read()
|
|
72
|
+
match = re.search(r"<version>(.*?)</version>", content)
|
|
73
|
+
if match:
|
|
74
|
+
return VersionInfo(version=match.group(1), source="pom.xml")
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Check build.gradle (Java/Gradle)
|
|
79
|
+
gradle_kts = root / "build.gradle.kts"
|
|
80
|
+
gradle = root / "build.gradle"
|
|
81
|
+
gradle_file = None
|
|
82
|
+
if gradle_kts.exists():
|
|
83
|
+
gradle_file = gradle_kts
|
|
84
|
+
elif gradle.exists():
|
|
85
|
+
gradle_file = gradle
|
|
86
|
+
|
|
87
|
+
if gradle_file:
|
|
88
|
+
try:
|
|
89
|
+
with gradle_file.open("r", encoding="utf-8") as f:
|
|
90
|
+
content = f.read()
|
|
91
|
+
match = re.search(r'version\s*=\s*["\'](.*?)["\']', content)
|
|
92
|
+
if match:
|
|
93
|
+
return VersionInfo(version=match.group(1), source=gradle_file.name)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
return VersionInfo(version="unknown", source=None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def inspect_project(root: Optional[Path] = None) -> VersionInfo:
|
|
101
|
+
return get_project_version(root)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def inspect_committed(root: Optional[Path] = None) -> VersionInfo:
|
|
105
|
+
"""Read the version from HEAD (committed code), not working tree."""
|
|
106
|
+
files_to_check = [
|
|
107
|
+
("pyproject.toml", r'version\s*=\s*"(.*?)"'),
|
|
108
|
+
("package.json", None),
|
|
109
|
+
("Cargo.toml", r'^version\s*=\s*"(.*?)"'),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
git_c = ["git"]
|
|
113
|
+
if root is not None:
|
|
114
|
+
git_c = ["git", "-C", str(root)]
|
|
115
|
+
|
|
116
|
+
for filename, pattern in files_to_check:
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
git_c + ["show", f"HEAD:{filename}"],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
check=False,
|
|
123
|
+
shell=(os.name == "nt"),
|
|
124
|
+
)
|
|
125
|
+
if result.returncode == 0:
|
|
126
|
+
if filename == "package.json":
|
|
127
|
+
try:
|
|
128
|
+
data = json.loads(result.stdout)
|
|
129
|
+
if "version" in data:
|
|
130
|
+
return VersionInfo(
|
|
131
|
+
version=data["version"], source="package.json"
|
|
132
|
+
)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
elif pattern:
|
|
136
|
+
match = re.search(pattern, result.stdout, re.MULTILINE)
|
|
137
|
+
if match:
|
|
138
|
+
return VersionInfo(version=match.group(1), source=filename)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
return VersionInfo(version="unknown", source=None)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from verkit.inspector import get_project_version, inspect_committed
|
|
10
|
+
|
|
11
|
+
_VERSION_FILES = ("pyproject.toml", "package.json", "uv.lock", "Cargo.toml")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _git(*args: str, cwd: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
15
|
+
cmd = ["git"]
|
|
16
|
+
if cwd is not None:
|
|
17
|
+
cmd += ["-C", str(cwd)]
|
|
18
|
+
cmd.extend(args)
|
|
19
|
+
return subprocess.run(
|
|
20
|
+
cmd,
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
check=False,
|
|
24
|
+
shell=(os.name == "nt"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _find_git_root(path: Path) -> Optional[Path]:
|
|
29
|
+
res = _git("rev-parse", "--show-toplevel", cwd=path)
|
|
30
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
31
|
+
return Path(res.stdout.strip()).resolve()
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _can_amend_version_safely(git_root: Path) -> Tuple[bool, str]:
|
|
36
|
+
"""Whether folding the version bump into HEAD via amend is safe."""
|
|
37
|
+
res = _git("rev-parse", "HEAD", cwd=git_root)
|
|
38
|
+
if res.returncode != 0 or not res.stdout.strip():
|
|
39
|
+
return False, "no HEAD commit"
|
|
40
|
+
|
|
41
|
+
tags_res = _git(
|
|
42
|
+
"tag", "--points-at", "HEAD", "--format=%(refname:short)", cwd=git_root
|
|
43
|
+
)
|
|
44
|
+
if tags_res.returncode == 0 and tags_res.stdout.strip():
|
|
45
|
+
tags = [t for t in tags_res.stdout.strip().splitlines() if t]
|
|
46
|
+
return False, f"HEAD is tagged ({', '.join(tags)})"
|
|
47
|
+
|
|
48
|
+
branch_res = _git("rev-parse", "--abbrev-ref", "HEAD", cwd=git_root)
|
|
49
|
+
branch = branch_res.stdout.strip() if branch_res.returncode == 0 else ""
|
|
50
|
+
if not branch or branch == "HEAD":
|
|
51
|
+
return False, "detached HEAD"
|
|
52
|
+
|
|
53
|
+
upstream_res = _git(
|
|
54
|
+
"rev-parse", "--abbrev-ref", "@{upstream}", cwd=git_root
|
|
55
|
+
)
|
|
56
|
+
if upstream_res.returncode != 0 or not upstream_res.stdout.strip():
|
|
57
|
+
return True, "local branch has no tracking upstream"
|
|
58
|
+
upstream = upstream_res.stdout.strip()
|
|
59
|
+
|
|
60
|
+
counts = _git(
|
|
61
|
+
"rev-list", "--count", f"{upstream}..HEAD", cwd=git_root
|
|
62
|
+
)
|
|
63
|
+
if counts.returncode != 0:
|
|
64
|
+
return False, f"could not compare HEAD to {upstream}"
|
|
65
|
+
try:
|
|
66
|
+
ahead = int(counts.stdout.strip())
|
|
67
|
+
except ValueError:
|
|
68
|
+
return False, "invalid rev-list output"
|
|
69
|
+
|
|
70
|
+
if ahead <= 0:
|
|
71
|
+
return (
|
|
72
|
+
False,
|
|
73
|
+
f"HEAD is already published to {upstream} (ahead count: {ahead})",
|
|
74
|
+
)
|
|
75
|
+
return True, f"HEAD is ahead of {upstream} by {ahead} unpushed commit(s)"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _stage_version_files(git_root: Path, project_root: Path) -> List[str]:
|
|
79
|
+
staged: List[str] = []
|
|
80
|
+
for name in _VERSION_FILES:
|
|
81
|
+
target = project_root / name
|
|
82
|
+
if target.exists():
|
|
83
|
+
res = _git("add", str(target), cwd=git_root)
|
|
84
|
+
if res.returncode == 0:
|
|
85
|
+
try:
|
|
86
|
+
rel = str(target.relative_to(git_root))
|
|
87
|
+
except ValueError:
|
|
88
|
+
rel = name
|
|
89
|
+
staged.append(rel)
|
|
90
|
+
return staged
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _increment_semver(current_v: str, part: str) -> str:
|
|
94
|
+
"""Increment a semver string (e.g. 0.1.0 -> 0.1.1) in pure Python."""
|
|
95
|
+
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(.*)$", current_v.strip())
|
|
96
|
+
if not match:
|
|
97
|
+
raise ValueError(f"Version {current_v!r} is not a valid semver X.Y.Z string")
|
|
98
|
+
|
|
99
|
+
major, minor, patch, extra = (
|
|
100
|
+
int(match.group(1)),
|
|
101
|
+
int(match.group(2)),
|
|
102
|
+
int(match.group(3)),
|
|
103
|
+
match.group(4),
|
|
104
|
+
)
|
|
105
|
+
if part == "major":
|
|
106
|
+
major += 1
|
|
107
|
+
minor = 0
|
|
108
|
+
patch = 0
|
|
109
|
+
elif part == "minor":
|
|
110
|
+
minor += 1
|
|
111
|
+
patch = 0
|
|
112
|
+
elif part == "patch":
|
|
113
|
+
patch += 1
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError(f"Unknown bump part: {part!r}")
|
|
116
|
+
|
|
117
|
+
return f"{major}.{minor}.{patch}{extra}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _bump_project_version(part: str, project_root: Path, source: str) -> str:
|
|
121
|
+
info = get_project_version(project_root)
|
|
122
|
+
if info.version == "unknown":
|
|
123
|
+
raise RuntimeError("Cannot bump version: current project version is unknown.")
|
|
124
|
+
|
|
125
|
+
new_v = _increment_semver(info.version, part)
|
|
126
|
+
|
|
127
|
+
if source == "pyproject.toml":
|
|
128
|
+
file_path = project_root / "pyproject.toml"
|
|
129
|
+
content = file_path.read_text(encoding="utf-8")
|
|
130
|
+
updated = re.sub(
|
|
131
|
+
r'(version\s*=\s*)"(.*?)"', f'\\1"{new_v}"', content, count=1
|
|
132
|
+
)
|
|
133
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
134
|
+
|
|
135
|
+
# Also sync uv.lock if present
|
|
136
|
+
uv_lock = project_root / "uv.lock"
|
|
137
|
+
if uv_lock.exists():
|
|
138
|
+
try:
|
|
139
|
+
lock_text = uv_lock.read_text(encoding="utf-8")
|
|
140
|
+
# Update package version entry in uv.lock if matching current version
|
|
141
|
+
pattern = rf'(name\s*=\s*"{re.escape(project_root.name)}"\nversion\s*=\s*)"{re.escape(info.version)}"'
|
|
142
|
+
if re.search(pattern, lock_text):
|
|
143
|
+
lock_updated = re.sub(pattern, f'\\1"{new_v}"', lock_text, count=1)
|
|
144
|
+
uv_lock.write_text(lock_updated, encoding="utf-8")
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
elif source == "package.json":
|
|
149
|
+
file_path = project_root / "package.json"
|
|
150
|
+
content = file_path.read_text(encoding="utf-8")
|
|
151
|
+
updated = re.sub(
|
|
152
|
+
r'("version"\s*:\s*)"(.*?)"', f'\\1"{new_v}"', content, count=1
|
|
153
|
+
)
|
|
154
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
155
|
+
|
|
156
|
+
elif source == "Cargo.toml":
|
|
157
|
+
file_path = project_root / "Cargo.toml"
|
|
158
|
+
content = file_path.read_text(encoding="utf-8")
|
|
159
|
+
updated = re.sub(
|
|
160
|
+
r'^(version\s*=\s*)"(.*?)"',
|
|
161
|
+
f'\\1"{new_v}"',
|
|
162
|
+
content,
|
|
163
|
+
count=1,
|
|
164
|
+
flags=re.MULTILINE,
|
|
165
|
+
)
|
|
166
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
elif source.endswith(".csproj"):
|
|
169
|
+
file_path = project_root / source
|
|
170
|
+
content = file_path.read_text(encoding="utf-8")
|
|
171
|
+
updated = re.sub(
|
|
172
|
+
r"(<Version>)(.*?)(</Version>)", f"\\1{new_v}\\3", content, count=1
|
|
173
|
+
)
|
|
174
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
175
|
+
|
|
176
|
+
elif source == "pom.xml":
|
|
177
|
+
file_path = project_root / "pom.xml"
|
|
178
|
+
content = file_path.read_text(encoding="utf-8")
|
|
179
|
+
updated = re.sub(
|
|
180
|
+
r"(<version>)(.*?)(</version>)", f"\\1{new_v}\\3", content, count=1
|
|
181
|
+
)
|
|
182
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
elif "gradle" in source:
|
|
185
|
+
file_path = project_root / source
|
|
186
|
+
content = file_path.read_text(encoding="utf-8")
|
|
187
|
+
updated = re.sub(
|
|
188
|
+
r'(version\s*=\s*["\'])(.*?)(["\'])', f"\\1{new_v}\\3", content, count=1
|
|
189
|
+
)
|
|
190
|
+
file_path.write_text(updated, encoding="utf-8")
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
raise RuntimeError(
|
|
194
|
+
f"Cannot bump version: unsupported project file source {source!r}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
verify_info = get_project_version(project_root)
|
|
198
|
+
if verify_info.version != new_v:
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
f"Version bump failed: expected {new_v}, got {verify_info.version}"
|
|
201
|
+
)
|
|
202
|
+
return new_v
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _commit_version_bump(
|
|
206
|
+
console: Optional[Console],
|
|
207
|
+
git_root: Path,
|
|
208
|
+
project_root: Path,
|
|
209
|
+
new_v: str,
|
|
210
|
+
*,
|
|
211
|
+
allow_amend: bool = True,
|
|
212
|
+
) -> str:
|
|
213
|
+
staged = _stage_version_files(git_root, project_root)
|
|
214
|
+
if not staged:
|
|
215
|
+
raise RuntimeError("No version files found to stage after bump.")
|
|
216
|
+
|
|
217
|
+
diff_staged = _git("diff", "--cached", "--name-only", cwd=git_root)
|
|
218
|
+
if diff_staged.returncode == 0 and diff_staged.stdout.strip():
|
|
219
|
+
staged_all = set(diff_staged.stdout.strip().splitlines())
|
|
220
|
+
unrelated = staged_all - set(staged)
|
|
221
|
+
if unrelated:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"Refusing to commit version bump: unrelated files are staged: "
|
|
224
|
+
+ ", ".join(sorted(unrelated))
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
amend = False
|
|
228
|
+
if allow_amend:
|
|
229
|
+
ok, reason = _can_amend_version_safely(git_root)
|
|
230
|
+
if ok:
|
|
231
|
+
amend = True
|
|
232
|
+
msg_res = _git("log", "-1", "--format=%B", cwd=git_root)
|
|
233
|
+
existing_msg = (
|
|
234
|
+
msg_res.stdout.strip()
|
|
235
|
+
if msg_res.returncode == 0
|
|
236
|
+
else "chore: bump version"
|
|
237
|
+
)
|
|
238
|
+
commit_res = _git("commit", "--amend", "-m", existing_msg, cwd=git_root)
|
|
239
|
+
if commit_res.returncode != 0:
|
|
240
|
+
err = (commit_res.stderr or commit_res.stdout or "").strip()
|
|
241
|
+
raise RuntimeError(f"Failed to amend version bump: {err}")
|
|
242
|
+
if console:
|
|
243
|
+
console.print(
|
|
244
|
+
f"[dim]Amended version {new_v} into HEAD ({reason})[/dim]"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not amend:
|
|
248
|
+
msg = f"chore(release): v{new_v}"
|
|
249
|
+
commit_res = _git("commit", "-m", msg, cwd=git_root)
|
|
250
|
+
if commit_res.returncode != 0:
|
|
251
|
+
err = (commit_res.stderr or commit_res.stdout or "").strip()
|
|
252
|
+
if "nothing to commit" in err.lower():
|
|
253
|
+
if console:
|
|
254
|
+
console.print(
|
|
255
|
+
f"[dim]Version bump for {new_v} produced no changes to commit.[/dim]"
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
raise RuntimeError(f"Failed to commit version bump: {err}")
|
|
259
|
+
else:
|
|
260
|
+
if console:
|
|
261
|
+
console.print(f"[bold green]Committed version bump: {msg}[/bold green]")
|
|
262
|
+
|
|
263
|
+
committed_info = inspect_committed(git_root)
|
|
264
|
+
if committed_info.version != new_v:
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
f"Post-commit verification failed: HEAD has version {committed_info.version!r}, "
|
|
267
|
+
f"expected {new_v!r}."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return "amend" if amend else "commit"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def promote_version(
|
|
274
|
+
part: str,
|
|
275
|
+
*,
|
|
276
|
+
project_root: Optional[Path] = None,
|
|
277
|
+
console: Optional[Console] = None,
|
|
278
|
+
allow_amend: bool = True,
|
|
279
|
+
) -> str:
|
|
280
|
+
"""Bump project version and commit it. Returns the new version string."""
|
|
281
|
+
project_root = (project_root or Path.cwd()).resolve()
|
|
282
|
+
git_root = _find_git_root(project_root)
|
|
283
|
+
if not git_root:
|
|
284
|
+
raise RuntimeError("Not inside a git repository.")
|
|
285
|
+
|
|
286
|
+
working_info = get_project_version(project_root)
|
|
287
|
+
if not working_info.source or working_info.version == "unknown":
|
|
288
|
+
raise RuntimeError(
|
|
289
|
+
"Could not find a project version file (pyproject.toml / package.json)."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
pre_committed_info = inspect_committed(git_root)
|
|
293
|
+
if console:
|
|
294
|
+
console.print(
|
|
295
|
+
f"[blue]Bumping {part} from working {working_info.version} "
|
|
296
|
+
f"(HEAD {pre_committed_info.version}) via {working_info.source}...[/blue]"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
new_v = _bump_project_version(part, project_root, working_info.source)
|
|
300
|
+
if console:
|
|
301
|
+
console.print(f"[bold green]Promoted to version {new_v}[/bold green]")
|
|
302
|
+
_commit_version_bump(
|
|
303
|
+
console, git_root, project_root, new_v, allow_amend=allow_amend
|
|
304
|
+
)
|
|
305
|
+
return new_v
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from verkit.inspector import get_project_version, inspect_committed
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _git(*args: str, cwd: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
12
|
+
cmd = ["git"]
|
|
13
|
+
if cwd is not None:
|
|
14
|
+
cmd += ["-C", str(cwd)]
|
|
15
|
+
cmd.extend(args)
|
|
16
|
+
return subprocess.run(
|
|
17
|
+
cmd,
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
check=False,
|
|
21
|
+
shell=(os.name == "nt"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _find_git_root(path: Path) -> Optional[Path]:
|
|
26
|
+
res = _git("rev-parse", "--show-toplevel", cwd=path)
|
|
27
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
28
|
+
return Path(res.stdout.strip()).resolve()
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _git_head_sha(git_root: Path) -> Optional[str]:
|
|
33
|
+
res = _git("rev-parse", "HEAD", cwd=git_root)
|
|
34
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
35
|
+
return res.stdout.strip()
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _git_tag_target(git_root: Path, tag_name: str) -> Optional[str]:
|
|
40
|
+
res = _git("rev-parse", f"refs/tags/{tag_name}^{{}}", cwd=git_root)
|
|
41
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
42
|
+
return res.stdout.strip()
|
|
43
|
+
res2 = _git("rev-parse", f"refs/tags/{tag_name}", cwd=git_root)
|
|
44
|
+
if res2.returncode == 0 and res2.stdout.strip():
|
|
45
|
+
return res2.stdout.strip()
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _push_branch_and_tag(
|
|
50
|
+
console: Optional[Console],
|
|
51
|
+
git_root: Path,
|
|
52
|
+
tag_name: str,
|
|
53
|
+
*,
|
|
54
|
+
push_branch: bool = True,
|
|
55
|
+
):
|
|
56
|
+
if push_branch:
|
|
57
|
+
branch_res = _git("rev-parse", "--abbrev-ref", "HEAD", cwd=git_root)
|
|
58
|
+
branch = branch_res.stdout.strip() if branch_res.returncode == 0 else ""
|
|
59
|
+
if not branch or branch == "HEAD":
|
|
60
|
+
raise RuntimeError("Cannot push branch from detached HEAD state.")
|
|
61
|
+
if console:
|
|
62
|
+
console.print(f"[blue]Pushing branch {branch} to origin...[/blue]")
|
|
63
|
+
res = _git("push", "origin", branch, cwd=git_root)
|
|
64
|
+
if res.returncode != 0:
|
|
65
|
+
err = (res.stderr or res.stdout or "").strip()
|
|
66
|
+
raise RuntimeError(f"Failed to push branch {branch}: {err}")
|
|
67
|
+
if console:
|
|
68
|
+
console.print(f"[bold green]Pushed branch {branch}[/bold green]")
|
|
69
|
+
|
|
70
|
+
if console:
|
|
71
|
+
console.print(f"[blue]Pushing tag {tag_name} to origin...[/blue]")
|
|
72
|
+
res = _git("push", "origin", tag_name, cwd=git_root)
|
|
73
|
+
if res.returncode != 0:
|
|
74
|
+
err = (res.stderr or res.stdout or "").strip()
|
|
75
|
+
raise RuntimeError(f"Failed to push tag {tag_name}: {err}")
|
|
76
|
+
if console:
|
|
77
|
+
console.print(f"[bold green]Pushed tag {tag_name}[/bold green]")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tag_version(
|
|
81
|
+
*,
|
|
82
|
+
project_root: Optional[Path] = None,
|
|
83
|
+
console: Optional[Console] = None,
|
|
84
|
+
push: bool = True,
|
|
85
|
+
push_branch: bool = True,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Create ``vX.Y.Z`` on HEAD from the *committed* version and optionally push."""
|
|
88
|
+
project_root = (project_root or Path.cwd()).resolve()
|
|
89
|
+
git_root = _find_git_root(project_root)
|
|
90
|
+
if not git_root:
|
|
91
|
+
raise RuntimeError("Not inside a git repository.")
|
|
92
|
+
|
|
93
|
+
committed_info = inspect_committed(git_root)
|
|
94
|
+
if committed_info.version == "unknown":
|
|
95
|
+
raise RuntimeError("Could not read version from HEAD. Promote first.")
|
|
96
|
+
|
|
97
|
+
working_info = get_project_version(project_root)
|
|
98
|
+
if working_info.version != "unknown" and working_info.version != committed_info.version:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
f"Working tree version {working_info.version} differs from HEAD {committed_info.version}. "
|
|
101
|
+
"Commit or discard local version edits before tagging."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
tag_name = f"v{committed_info.version}"
|
|
105
|
+
head = _git_head_sha(git_root)
|
|
106
|
+
if not head:
|
|
107
|
+
raise RuntimeError("Could not resolve HEAD.")
|
|
108
|
+
|
|
109
|
+
existing = _git_tag_target(git_root, tag_name)
|
|
110
|
+
if existing:
|
|
111
|
+
head_full = _git("rev-parse", head, cwd=git_root).stdout.strip() or head
|
|
112
|
+
existing_full = (
|
|
113
|
+
_git("rev-parse", existing, cwd=git_root).stdout.strip() or existing
|
|
114
|
+
)
|
|
115
|
+
if existing_full == head_full:
|
|
116
|
+
if console:
|
|
117
|
+
console.print(f"[dim]Tag {tag_name} already points at HEAD[/dim]")
|
|
118
|
+
else:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
f"Tag {tag_name} already exists but points at {existing_full[:12]}, "
|
|
121
|
+
f"not HEAD ({head_full[:12]})."
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
res = _git("tag", tag_name, cwd=git_root)
|
|
125
|
+
if res.returncode != 0:
|
|
126
|
+
err = (res.stderr or res.stdout or "").strip()
|
|
127
|
+
raise RuntimeError(f"Failed to create tag {tag_name}: {err}")
|
|
128
|
+
if console:
|
|
129
|
+
console.print(
|
|
130
|
+
f"[bold green]Tagged HEAD as {tag_name}[/bold green] "
|
|
131
|
+
f"[dim]({head[:12]})[/dim]"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if push:
|
|
135
|
+
_push_branch_and_tag(console, git_root, tag_name, push_branch=push_branch)
|
|
136
|
+
elif console:
|
|
137
|
+
console.print(
|
|
138
|
+
f"[dim]Skipped push. When ready: git push && git push origin {tag_name}[/dim]"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return tag_name
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def release_version(
|
|
145
|
+
part: str,
|
|
146
|
+
*,
|
|
147
|
+
project_root: Optional[Path] = None,
|
|
148
|
+
console: Optional[Console] = None,
|
|
149
|
+
push: bool = True,
|
|
150
|
+
push_branch: bool = True,
|
|
151
|
+
) -> Tuple[str, str]:
|
|
152
|
+
"""Atomic promote + tag + push release workflow."""
|
|
153
|
+
from verkit.promoter import promote_version
|
|
154
|
+
|
|
155
|
+
new_v = promote_version(part, project_root=project_root, console=console)
|
|
156
|
+
tag_name = tag_version(
|
|
157
|
+
project_root=project_root,
|
|
158
|
+
console=console,
|
|
159
|
+
push=push,
|
|
160
|
+
push_branch=push_branch,
|
|
161
|
+
)
|
|
162
|
+
return new_v, tag_name
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from verkit.inspector import inspect_project
|
|
2
|
+
|
|
3
|
+
def test_inspect_python_project(tmp_path):
|
|
4
|
+
pyproject = tmp_path / "pyproject.toml"
|
|
5
|
+
pyproject.write_text('[project]\nname = "myproject"\nversion = "1.2.3"\n')
|
|
6
|
+
info = inspect_project(tmp_path)
|
|
7
|
+
assert info.version == "1.2.3"
|
|
8
|
+
assert info.source == "pyproject.toml"
|
|
9
|
+
|
|
10
|
+
def test_inspect_node_project(tmp_path):
|
|
11
|
+
pkg = tmp_path / "package.json"
|
|
12
|
+
pkg.write_text('{"name": "my-node-app", "version": "2.0.0"}')
|
|
13
|
+
info = inspect_project(tmp_path)
|
|
14
|
+
assert info.version == "2.0.0"
|
|
15
|
+
assert info.source == "package.json"
|
|
16
|
+
|
|
17
|
+
def test_inspect_unknown_project(tmp_path):
|
|
18
|
+
info = inspect_project(tmp_path)
|
|
19
|
+
assert info.version == "unknown"
|
|
20
|
+
assert info.source is None
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from verkit.promoter import _increment_semver
|
|
2
|
+
|
|
3
|
+
def test_increment_semver():
|
|
4
|
+
assert _increment_semver("0.1.0", "patch") == "0.1.1"
|
|
5
|
+
assert _increment_semver("0.1.0", "minor") == "0.2.0"
|
|
6
|
+
assert _increment_semver("0.1.0", "major") == "1.0.0"
|
|
7
|
+
assert _increment_semver("1.2.3-rc1", "patch") == "1.2.4-rc1"
|
verkit-0.1.1/uv.lock
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.9"
|
|
4
|
+
resolution-markers = [
|
|
5
|
+
"python_full_version >= '3.10'",
|
|
6
|
+
"python_full_version < '3.10'",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
[[package]]
|
|
10
|
+
name = "markdown-it-py"
|
|
11
|
+
version = "3.0.0"
|
|
12
|
+
source = { registry = "https://pypi.org/simple" }
|
|
13
|
+
resolution-markers = [
|
|
14
|
+
"python_full_version < '3.10'",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
{ name = "mdurl" },
|
|
18
|
+
]
|
|
19
|
+
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
|
20
|
+
wheels = [
|
|
21
|
+
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[package]]
|
|
25
|
+
name = "markdown-it-py"
|
|
26
|
+
version = "4.2.0"
|
|
27
|
+
source = { registry = "https://pypi.org/simple" }
|
|
28
|
+
resolution-markers = [
|
|
29
|
+
"python_full_version >= '3.10'",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
{ name = "mdurl" },
|
|
33
|
+
]
|
|
34
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
35
|
+
wheels = [
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "mdurl"
|
|
41
|
+
version = "0.1.2"
|
|
42
|
+
source = { registry = "https://pypi.org/simple" }
|
|
43
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
44
|
+
wheels = [
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "pygments"
|
|
50
|
+
version = "2.20.0"
|
|
51
|
+
source = { registry = "https://pypi.org/simple" }
|
|
52
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
53
|
+
wheels = [
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "rich"
|
|
59
|
+
version = "15.0.0"
|
|
60
|
+
source = { registry = "https://pypi.org/simple" }
|
|
61
|
+
dependencies = [
|
|
62
|
+
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
63
|
+
{ name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
|
64
|
+
{ name = "pygments" },
|
|
65
|
+
]
|
|
66
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
67
|
+
wheels = [
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "verkit"
|
|
73
|
+
version = "0.1.1"
|
|
74
|
+
source = { editable = "." }
|
|
75
|
+
dependencies = [
|
|
76
|
+
{ name = "rich" },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[package.metadata]
|
|
80
|
+
requires-dist = [{ name = "rich", specifier = ">=10.0.0" }]
|