tinysemver 2.0.8__tar.gz → 2.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.
- {tinysemver-2.0.8/tinysemver.egg-info → tinysemver-2.1.1}/PKG-INFO +20 -5
- {tinysemver-2.0.8 → tinysemver-2.1.1}/README.md +15 -2
- {tinysemver-2.0.8 → tinysemver-2.1.1}/pyproject.toml +4 -3
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver/tinysemver.py +275 -27
- {tinysemver-2.0.8 → tinysemver-2.1.1/tinysemver.egg-info}/PKG-INFO +20 -5
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver.egg-info/SOURCES.txt +1 -0
- tinysemver-2.1.1/tinysemver.egg-info/requires.txt +2 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/LICENSE +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/setup.cfg +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver/__init__.py +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver.egg-info/dependency_links.txt +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver.egg-info/entry_points.txt +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.1}/tinysemver.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinysemver
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.1.1
|
|
4
|
+
Summary: Semantic-Versioning with LLMs and without 300,000 lines of JS
|
|
5
5
|
Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
|
|
6
6
|
License: Apache License
|
|
7
7
|
Version 2.0, January 2004
|
|
@@ -210,7 +210,7 @@ Project-URL: Documentation, https://github.com/ashvardanian/tinysemver/blob/main
|
|
|
210
210
|
Project-URL: Repository, https://github.com/ashvardanian/tinysemver.git
|
|
211
211
|
Project-URL: Issues, https://github.com/ashvardanian/tinysemver/issues
|
|
212
212
|
Project-URL: Changelog, https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md
|
|
213
|
-
Classifier: Development Status ::
|
|
213
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
214
214
|
Classifier: Intended Audience :: Developers
|
|
215
215
|
Classifier: Topic :: Software Development :: Build Tools
|
|
216
216
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -223,10 +223,12 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
223
223
|
Classifier: Programming Language :: Python :: 3.11
|
|
224
224
|
Description-Content-Type: text/markdown
|
|
225
225
|
License-File: LICENSE
|
|
226
|
+
Requires-Dist: openai>=1.54.4
|
|
227
|
+
Requires-Dist: rich>=13.8.0
|
|
226
228
|
|
|
227
|
-

|
|
229
|
+

|
|
228
230
|
|
|
229
|
-
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
231
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
|
|
230
232
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
231
233
|
Here is how to integrate it into your project CI:
|
|
232
234
|
|
|
@@ -304,6 +306,19 @@ Alternatively, you can just ask for `--help`:
|
|
|
304
306
|
$ tinysemver --help
|
|
305
307
|
```
|
|
306
308
|
|
|
309
|
+
## AI and Rock-n-Roll
|
|
310
|
+
|
|
311
|
+
TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
$ tinysemver --verbose \
|
|
315
|
+
--github-repository 'ashvardanian/tinysemver' \
|
|
316
|
+
--openai-base-url 'https://api.groq.com/openai/v1' \
|
|
317
|
+
--openai-api-key 'GET_YOURSELF_A_KEY' \
|
|
318
|
+
--openai-model 'llama-3.2-11b-text-preview' \
|
|
319
|
+
--dry-run
|
|
320
|
+
```
|
|
321
|
+
|
|
307
322
|
## Usage Details for the GitHub CI Action
|
|
308
323
|
|
|
309
324
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-

|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
3
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
|
|
4
4
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
5
5
|
Here is how to integrate it into your project CI:
|
|
6
6
|
|
|
@@ -78,6 +78,19 @@ Alternatively, you can just ask for `--help`:
|
|
|
78
78
|
$ tinysemver --help
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
## AI and Rock-n-Roll
|
|
82
|
+
|
|
83
|
+
TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
$ tinysemver --verbose \
|
|
87
|
+
--github-repository 'ashvardanian/tinysemver' \
|
|
88
|
+
--openai-base-url 'https://api.groq.com/openai/v1' \
|
|
89
|
+
--openai-api-key 'GET_YOURSELF_A_KEY' \
|
|
90
|
+
--openai-model 'llama-3.2-11b-text-preview' \
|
|
91
|
+
--dry-run
|
|
92
|
+
```
|
|
93
|
+
|
|
81
94
|
## Usage Details for the GitHub CI Action
|
|
82
95
|
|
|
83
96
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tinysemver"
|
|
7
|
-
version = "2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.1.1"
|
|
8
|
+
description = "Semantic-Versioning with LLMs and without 300,000 lines of JS"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
11
11
|
authors = [
|
|
@@ -13,7 +13,7 @@ authors = [
|
|
|
13
13
|
{ name = "Guillaume de Rouville", email = "31691250+grouville@users.noreply.github.com" },
|
|
14
14
|
]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Topic :: Software Development :: Build Tools",
|
|
19
19
|
"License :: OSI Approved :: Apache Software License",
|
|
@@ -25,6 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3.10",
|
|
26
26
|
"Programming Language :: Python :: 3.11",
|
|
27
27
|
]
|
|
28
|
+
dependencies = ["openai >= 1.54.4", "rich >= 13.8.0"]
|
|
28
29
|
|
|
29
30
|
[project.scripts]
|
|
30
31
|
tinysemver = "tinysemver.tinysemver:main"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
"""TinySemVer is a tiny
|
|
3
|
+
"""TinySemVer is a tiny but mighty Semantic Versioning tool with AI, Drugs, and Rock-n-Roll.
|
|
4
4
|
|
|
5
5
|
This module traces the commit history of a Git repository after the last tag, and based
|
|
6
6
|
on the commit messages, it determines the type of version bump (major, minor, or patch).
|
|
@@ -15,6 +15,7 @@ Example:
|
|
|
15
15
|
--minor-verbs 'feature,minor,add,new' \
|
|
16
16
|
--patch-verbs 'fix,patch,bug,improve,docs,make' \
|
|
17
17
|
--changelog-file 'CHANGELOG.md' \
|
|
18
|
+
--guide-file 'CONTRIBUTING.md' \
|
|
18
19
|
--version-file 'VERSION' \
|
|
19
20
|
--update-version-in 'package.json' '"version": "(.*)"' \
|
|
20
21
|
--update-version-in 'CITATION.cff' '^version: (.*)' \
|
|
@@ -32,6 +33,7 @@ By default, the following conventions are used:
|
|
|
32
33
|
- The repository must be a Git repository.
|
|
33
34
|
- It must contain a "VERSION" and "CHANGELOG.md" files in the root directory.
|
|
34
35
|
- The changelog is append-only - sorted in chronological order.
|
|
36
|
+
- The contibution guide is in the "CONTRIBUTING.md" file.
|
|
35
37
|
|
|
36
38
|
Setting up a new project:
|
|
37
39
|
|
|
@@ -51,10 +53,13 @@ from typing import List, Tuple, Literal, Union, Optional, NamedTuple
|
|
|
51
53
|
from datetime import datetime
|
|
52
54
|
import traceback
|
|
53
55
|
|
|
56
|
+
|
|
54
57
|
SemVer = Tuple[int, int, int]
|
|
55
58
|
BumpType = Literal["major", "minor", "patch"]
|
|
56
59
|
PathLike = Union[str, os.PathLike]
|
|
57
60
|
Commit = NamedTuple("Commit", [("hash", str), ("message", str)])
|
|
61
|
+
ChangeDiff = str
|
|
62
|
+
|
|
58
63
|
|
|
59
64
|
class NoNewCommitsError(Exception):
|
|
60
65
|
"""Raised when no new commits are found since the last tag."""
|
|
@@ -62,6 +67,69 @@ class NoNewCommitsError(Exception):
|
|
|
62
67
|
pass
|
|
63
68
|
|
|
64
69
|
|
|
70
|
+
class MayContainVulnerability(Warning):
|
|
71
|
+
"""Raised when the commit may contain a vulnerability."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MayContainLogicalBugs(Warning):
|
|
77
|
+
"""Raised when the commit may contain logical bugs."""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MayContainBreakingChange(Warning):
|
|
83
|
+
"""Raised when the commit may contain a breaking change."""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MayLackDocumentation(Warning):
|
|
89
|
+
"""Raised when the commit may lack documentation updates."""
|
|
90
|
+
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class UnknownCommitWarning(Warning):
|
|
95
|
+
"""Something in the commit doesn't add up."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_console = None # Initialize global console variable
|
|
101
|
+
_console_is_rich = True # Initialize global console type variable
|
|
102
|
+
_openai_client = None # Initialize global OpenAI client variable
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_to_console(message: str) -> None:
|
|
106
|
+
"""Print a message to the console."""
|
|
107
|
+
global _console, _console_is_rich
|
|
108
|
+
if _console == None and _console_is_rich:
|
|
109
|
+
try:
|
|
110
|
+
from rich import Console
|
|
111
|
+
|
|
112
|
+
_console = Console()
|
|
113
|
+
except ImportError:
|
|
114
|
+
_console = None
|
|
115
|
+
_console_is_rich = False
|
|
116
|
+
|
|
117
|
+
if _console_is_rich:
|
|
118
|
+
_console.print(message)
|
|
119
|
+
else:
|
|
120
|
+
print(message)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_open_ai_client(base_url: str, api_key: str):
|
|
124
|
+
# Create a global variable for the client
|
|
125
|
+
global _openai_client
|
|
126
|
+
if not _openai_client:
|
|
127
|
+
from openai import OpenAI
|
|
128
|
+
|
|
129
|
+
_openai_client = OpenAI(base_url=base_url, api_key=api_key)
|
|
130
|
+
return _openai_client
|
|
131
|
+
|
|
132
|
+
|
|
65
133
|
def get_last_tag(repository_path: PathLike) -> str:
|
|
66
134
|
"""Retrieve the last Git tag name from the repository."""
|
|
67
135
|
result = subprocess.run(
|
|
@@ -75,7 +143,6 @@ def get_last_tag(repository_path: PathLike) -> str:
|
|
|
75
143
|
return result.stdout.strip().decode("utf-8")
|
|
76
144
|
|
|
77
145
|
|
|
78
|
-
|
|
79
146
|
def get_commits_since_tag(repository_path: PathLike, tag: str) -> List[Commit]:
|
|
80
147
|
"""Get commit hashes and messages since the specified Git tag."""
|
|
81
148
|
result = subprocess.run(
|
|
@@ -93,6 +160,20 @@ def get_commits_since_tag(repository_path: PathLike, tag: str) -> List[Commit]:
|
|
|
93
160
|
return [Commit(h, m) for h, m in zip(hashes, messages)]
|
|
94
161
|
|
|
95
162
|
|
|
163
|
+
def get_diff_for_commit(repository_path: PathLike, commit_hash: str) -> ChangeDiff:
|
|
164
|
+
"""Retrieve the diff for a specific commit and parse into Change named tuples."""
|
|
165
|
+
result = subprocess.run(
|
|
166
|
+
["git", "diff", f"{commit_hash}^", commit_hash, "--unified=3"],
|
|
167
|
+
stdout=subprocess.PIPE,
|
|
168
|
+
stderr=subprocess.PIPE,
|
|
169
|
+
cwd=repository_path,
|
|
170
|
+
)
|
|
171
|
+
if result.returncode != 0:
|
|
172
|
+
raise RuntimeError(f"Failed to retrieve diff for commit: {commit_hash}")
|
|
173
|
+
|
|
174
|
+
return result.stdout.decode("utf-8")
|
|
175
|
+
|
|
176
|
+
|
|
96
177
|
def parse_version(tag: str) -> SemVer:
|
|
97
178
|
"""Parse a version string from a Git tag name, assuming it looks like `v1.2.3`."""
|
|
98
179
|
match = re.match(r"v?(\d+)\.(\d+)\.(\d+)", tag)
|
|
@@ -211,7 +292,7 @@ def create_tag(
|
|
|
211
292
|
).stdout.strip()
|
|
212
293
|
|
|
213
294
|
subprocess.run(["git", "tag", "-a", tag, "-m", message, new_commit_sha], cwd=repository_path, env=env)
|
|
214
|
-
|
|
295
|
+
print_to_console(f"[bold green]Created new tag:[/bold green] {tag}")
|
|
215
296
|
if push:
|
|
216
297
|
url = None
|
|
217
298
|
if github_token and github_repository:
|
|
@@ -222,11 +303,6 @@ def create_tag(
|
|
|
222
303
|
assert github_repository and not github_token, "You can't provide the GitHub token without the repository"
|
|
223
304
|
url = f"https://github.com/{github_repository}"
|
|
224
305
|
|
|
225
|
-
# Pull the latest changes from the remote repository
|
|
226
|
-
# pull_result = subprocess.run(["git", "pull", "--merge", url], cwd=repository_path, env=env)
|
|
227
|
-
# if pull_result.returncode != 0:
|
|
228
|
-
# raise RuntimeError("Failed to pull the latest changes from the remote repository")
|
|
229
|
-
|
|
230
306
|
# Push both commits and the tag
|
|
231
307
|
push_result = subprocess.run(
|
|
232
308
|
["git", "push", url, f"{new_commit_sha}:{default_branch}"],
|
|
@@ -236,15 +312,15 @@ def create_tag(
|
|
|
236
312
|
)
|
|
237
313
|
if push_result.returncode != 0:
|
|
238
314
|
raise RuntimeError(
|
|
239
|
-
f"Failed to push the new commits to the remote repository: '{url}' with error: {push_result.stderr}"
|
|
315
|
+
f"Failed to push the new commits to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
|
|
240
316
|
)
|
|
241
317
|
|
|
242
318
|
push_result = subprocess.run(["git", "push", url, "--tag"], cwd=repository_path, capture_output=True, env=env)
|
|
243
319
|
if push_result.returncode != 0:
|
|
244
320
|
raise RuntimeError(
|
|
245
|
-
f"Failed to push the new tag to the remote repository: '{url}' with error: {push_result.stderr}"
|
|
321
|
+
f"Failed to push the new tag to the remote repository: '{url}' with error: {push_result.stderr.decode('utf-8')}"
|
|
246
322
|
)
|
|
247
|
-
|
|
323
|
+
print_to_console(f"[bold green]Pushed to:[/bold green] {url}")
|
|
248
324
|
|
|
249
325
|
# Create a release using GitHub CLI if available
|
|
250
326
|
if create_release and github_repository:
|
|
@@ -274,13 +350,15 @@ def create_tag(
|
|
|
274
350
|
)
|
|
275
351
|
|
|
276
352
|
if release_result.returncode == 0:
|
|
277
|
-
|
|
353
|
+
print_to_console(f"[bold green]Created GitHub release for tag:[/bold green] {tag}")
|
|
278
354
|
else:
|
|
279
|
-
|
|
355
|
+
print_to_console(
|
|
356
|
+
f"[bold red]Failed to create GitHub release:[/bold red] {release_result.stderr.strip()}"
|
|
357
|
+
)
|
|
280
358
|
except subprocess.CalledProcessError:
|
|
281
|
-
|
|
359
|
+
print_to_console("[bold yellow]GitHub CLI not available. Skipping release creation.[/bold yellow]")
|
|
282
360
|
except Exception as e:
|
|
283
|
-
|
|
361
|
+
print_to_console(f"[bold red]An error occurred while creating the release:[/bold red] {str(e)}")
|
|
284
362
|
|
|
285
363
|
|
|
286
364
|
def patch_with_regex(
|
|
@@ -307,8 +385,7 @@ def patch_with_regex(
|
|
|
307
385
|
)
|
|
308
386
|
return updated
|
|
309
387
|
|
|
310
|
-
#
|
|
311
|
-
# the ^ and $ anchors match the start and end of the whole string.
|
|
388
|
+
# Compile the regex pattern with multiline support
|
|
312
389
|
regex_pattern = re.compile(regex_pattern, re.MULTILINE)
|
|
313
390
|
matches = list(re.finditer(regex_pattern, old_content))
|
|
314
391
|
new_content = re.sub(regex_pattern, replace_first_group, old_content, 1)
|
|
@@ -322,13 +399,122 @@ def patch_with_regex(
|
|
|
322
399
|
new_slice = re.sub(regex_pattern, replace_first_group, old_slice, 1)
|
|
323
400
|
|
|
324
401
|
if verbose:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
402
|
+
print_to_console(f"[bold cyan]Will update file:[/bold cyan] {file_path}:{match_line}")
|
|
403
|
+
print_to_console(f"[red]- {old_slice}[/red]")
|
|
404
|
+
print_to_console(f"[green]+ {new_slice}[/green]")
|
|
328
405
|
|
|
329
406
|
if not dry_run:
|
|
330
407
|
with open(file_path, "w") as file:
|
|
331
408
|
file.write(new_content)
|
|
409
|
+
if verbose:
|
|
410
|
+
print_to_console(f"[bold green]File updated successfully:[/bold green] {file_path}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def validate_commit_with_llms(
|
|
414
|
+
base_url: str,
|
|
415
|
+
api_key: str,
|
|
416
|
+
model: Optional[str],
|
|
417
|
+
commit: Commit,
|
|
418
|
+
change: ChangeDiff,
|
|
419
|
+
) -> Optional[Warning]:
|
|
420
|
+
prompt = f"""
|
|
421
|
+
You are a professional coding assistant helping me to validate a commit message and its changes.
|
|
422
|
+
|
|
423
|
+
1. Check if the changed code is likely to introduce logical bugs.
|
|
424
|
+
2. Check if the commit message is likely to contain a vulnerability.
|
|
425
|
+
3. Check if it contains a breaking change that is likely to affect users.
|
|
426
|
+
4. Check if the documentation wasn't updated for the changes made.
|
|
427
|
+
|
|
428
|
+
Don't let the commit message fool you, just look at the changes made in the code.
|
|
429
|
+
Reply by choosing any number from 0 to 4 and describe the issue, if anything is found.
|
|
430
|
+
|
|
431
|
+
0. No issues found.
|
|
432
|
+
1. The commit may introduce logical bugs, such as the overflow in ...
|
|
433
|
+
2. The commit may contain a vulnerability, such as the SQL injection in ...
|
|
434
|
+
3. The commit may contain a breaking change, such as the removal of ...
|
|
435
|
+
4. The commit may lack documentation updates for the changes made in function ...
|
|
436
|
+
"""
|
|
437
|
+
message = f"""
|
|
438
|
+
#{commit.hash}: {commit.message}
|
|
439
|
+
|
|
440
|
+
Changes:
|
|
441
|
+
|
|
442
|
+
{change}
|
|
443
|
+
"""
|
|
444
|
+
client = get_open_ai_client(base_url=base_url, api_key=api_key)
|
|
445
|
+
response = client.chat.completions.create(
|
|
446
|
+
messages=[
|
|
447
|
+
{"role": "system", "content": prompt},
|
|
448
|
+
{"role": "user", "content": message},
|
|
449
|
+
],
|
|
450
|
+
max_tokens=256,
|
|
451
|
+
model=model,
|
|
452
|
+
stream=False,
|
|
453
|
+
)
|
|
454
|
+
response_text = response.choices[0].message.content
|
|
455
|
+
if response_text.startswith("0."):
|
|
456
|
+
return None
|
|
457
|
+
if response_text.startswith("1."):
|
|
458
|
+
return MayContainLogicalBugs(response_text)
|
|
459
|
+
if response_text.startswith("2."):
|
|
460
|
+
return MayContainVulnerability(response_text)
|
|
461
|
+
if response_text.startswith("3."):
|
|
462
|
+
return MayContainBreakingChange(response_text)
|
|
463
|
+
if response_text.startswith("4."):
|
|
464
|
+
return MayLackDocumentation(response_text)
|
|
465
|
+
return UnknownCommitWarning(response_text)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def aggregate_release_notes_with_llms(
|
|
469
|
+
base_url: str,
|
|
470
|
+
api_key: str,
|
|
471
|
+
model: Optional[str],
|
|
472
|
+
github_repository: str,
|
|
473
|
+
commits: List[Commit],
|
|
474
|
+
changes: List[ChangeDiff],
|
|
475
|
+
) -> str:
|
|
476
|
+
|
|
477
|
+
commits_with_hashes = "\n".join(f"- {commit.hash}: {commit.message}" for commit in commits)
|
|
478
|
+
prompt = f"""
|
|
479
|
+
You are a release notes generator for an advanced software project.
|
|
480
|
+
|
|
481
|
+
Aggregate the release notes for the upcoming version based on the commits and their
|
|
482
|
+
changes replying in a GitHub-flavored Markdown format.
|
|
483
|
+
|
|
484
|
+
- Mention the new features, improvements, and bug fixes.
|
|
485
|
+
- Warn about potential breaking changes and vulnerabilities.
|
|
486
|
+
- Tag people and teams responsible for the changes.
|
|
487
|
+
- For the most important commits provide a link to the issue or pull request and
|
|
488
|
+
a URL to that commit, like [commit](https://github.com/{github_repository}/commit/...).
|
|
489
|
+
- Don't guess anything, only use the information from the commits and their changes.
|
|
490
|
+
- Use inline math notation, like $\\beta=1$ or math blocks like the following, when needed:
|
|
491
|
+
|
|
492
|
+
```math
|
|
493
|
+
\\S_i(A, B, \\alpha, \\beta) = \\alpha \\cdot A_i + \\beta \\cdot B_i
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
- Use alerting quotes, like: [!CAUTION] or [!TIP], when needed.
|
|
497
|
+
|
|
498
|
+
Most importantly, be concise and informative.
|
|
499
|
+
"""
|
|
500
|
+
header_message = f"""Commits:
|
|
501
|
+
|
|
502
|
+
{commits_with_hashes}
|
|
503
|
+
"""
|
|
504
|
+
changes_messages = [f"#{commit.hash}: {commit.message}\n{change}" for commit, change in zip(commits, changes)]
|
|
505
|
+
|
|
506
|
+
client = get_open_ai_client(base_url=base_url, api_key=api_key)
|
|
507
|
+
response = client.chat.completions.create(
|
|
508
|
+
messages=[
|
|
509
|
+
{"role": "system", "content": prompt},
|
|
510
|
+
{"role": "user", "content": header_message},
|
|
511
|
+
*[{"role": "user", "content": message} for message in changes_messages],
|
|
512
|
+
],
|
|
513
|
+
max_tokens=2048,
|
|
514
|
+
model=model,
|
|
515
|
+
stream=False,
|
|
516
|
+
)
|
|
517
|
+
return response.choices[0].message.content
|
|
332
518
|
|
|
333
519
|
|
|
334
520
|
def bump(
|
|
@@ -350,6 +536,9 @@ def bump(
|
|
|
350
536
|
push: bool = True,
|
|
351
537
|
github_token: Optional[str] = None,
|
|
352
538
|
github_repository: Optional[str] = None,
|
|
539
|
+
openai_base_url: str = "https://api.openai.com",
|
|
540
|
+
openai_api_key: Optional[str] = None,
|
|
541
|
+
openai_model: Optional[str] = None,
|
|
353
542
|
default_branch: str = "main",
|
|
354
543
|
create_release: bool = False,
|
|
355
544
|
) -> SemVer:
|
|
@@ -375,6 +564,9 @@ def bump(
|
|
|
375
564
|
push (bool): If True, pushes the changes to the remote GitHub repository. Defaults to True.
|
|
376
565
|
github_token (Optional[str]): The GitHub token for authentication. If None, it attempts to use the GH_TOKEN environment variable. Defaults to None.
|
|
377
566
|
github_repository (Optional[str]): The GitHub repository in 'owner/repo' format. If None, it attempts to use the GH_REPOSITORY environment variable. Defaults to None.
|
|
567
|
+
openai_base_url (str): The OpenAI API base URL. Defaults to "https://api.openai.com".
|
|
568
|
+
openai_api_key (Optional[str]): The OpenAI API key. Defaults to None.
|
|
569
|
+
openai_model (Optional[str]): The OpenAI model to use for text generation. Defaults to None.
|
|
378
570
|
default_branch (str): The default branch to push the changes to. Defaults to "main".
|
|
379
571
|
|
|
380
572
|
Returns:
|
|
@@ -431,16 +623,16 @@ def bump(
|
|
|
431
623
|
|
|
432
624
|
current_version = parse_version(last_tag)
|
|
433
625
|
if verbose:
|
|
434
|
-
|
|
626
|
+
print_to_console(f"Current version: {current_version[0]}.{current_version[1]}.{current_version[2]}")
|
|
435
627
|
|
|
436
628
|
commits = get_commits_since_tag(repository_path, last_tag)
|
|
437
629
|
if not len(commits):
|
|
438
630
|
raise NoNewCommitsError(f"No new commits since the last {last_tag} tag")
|
|
439
631
|
|
|
440
632
|
if verbose:
|
|
441
|
-
|
|
633
|
+
print_to_console(f"? Commits since last tag: {len(commits)}")
|
|
442
634
|
for hash, commit in commits:
|
|
443
|
-
|
|
635
|
+
print_to_console(f"# {hash}: {commit}")
|
|
444
636
|
|
|
445
637
|
major_commits, minor_commits, patch_commits = group_commits(commits, major_verbs, minor_verbs, patch_verbs)
|
|
446
638
|
assert (
|
|
@@ -455,7 +647,7 @@ def bump(
|
|
|
455
647
|
bump_type = "patch"
|
|
456
648
|
new_version = bump_version(current_version, bump_type)
|
|
457
649
|
if verbose:
|
|
458
|
-
|
|
650
|
+
print_to_console(f"Next version: {new_version[0]}.{new_version[1]}.{new_version[2]} (type: {bump_type})")
|
|
459
651
|
|
|
460
652
|
new_version_str = f"{new_version[0]}.{new_version[1]}.{new_version[2]}"
|
|
461
653
|
if version_file:
|
|
@@ -466,12 +658,12 @@ def bump(
|
|
|
466
658
|
changes = f"\n## {now:%B %d, %Y}: v{new_version_str}\n"
|
|
467
659
|
changes += convert_commits_to_message(major_commits, minor_commits, patch_commits)
|
|
468
660
|
|
|
469
|
-
|
|
661
|
+
print_to_console(f"Will update file: {changelog_file}")
|
|
470
662
|
if verbose:
|
|
471
663
|
changes_lines = changes.count("\n") + 1
|
|
472
|
-
|
|
664
|
+
print_to_console(f"? Appending {changes_lines} lines")
|
|
473
665
|
for line in changes.split("\n"):
|
|
474
|
-
|
|
666
|
+
print_to_console(f"+ {line}")
|
|
475
667
|
|
|
476
668
|
if not dry_run:
|
|
477
669
|
with open(changelog_file, "a") as file:
|
|
@@ -490,6 +682,43 @@ def bump(
|
|
|
490
682
|
for file_path, regex_pattern in update_patch_version_in:
|
|
491
683
|
patch_with_regex(file_path, regex_pattern, str(new_version[2]), dry_run=dry_run, verbose=verbose)
|
|
492
684
|
|
|
685
|
+
# Now log the potential issues with the commits
|
|
686
|
+
if openai_api_key:
|
|
687
|
+
warnings_commits = []
|
|
688
|
+
warnings = []
|
|
689
|
+
changes = [get_diff_for_commit(repository_path, commit.hash) for commit in commits]
|
|
690
|
+
for commit, change in zip(commits, changes):
|
|
691
|
+
try:
|
|
692
|
+
warning = validate_commit_with_llms(
|
|
693
|
+
base_url=openai_base_url,
|
|
694
|
+
api_key=openai_api_key,
|
|
695
|
+
model=openai_model,
|
|
696
|
+
commit=commit,
|
|
697
|
+
change=change,
|
|
698
|
+
)
|
|
699
|
+
if warning:
|
|
700
|
+
warnings_commits.append(commit)
|
|
701
|
+
warnings.append(warning)
|
|
702
|
+
except Exception as e:
|
|
703
|
+
print_to_console(f"Failed to validate commit: {commit.hash} with error: {str(e)}")
|
|
704
|
+
traceback.print_exc()
|
|
705
|
+
|
|
706
|
+
if len(warnings):
|
|
707
|
+
print_to_console("## Potential issues")
|
|
708
|
+
for commit, warning in zip(warnings_commits, warnings):
|
|
709
|
+
print_to_console(f"- Commit #{commit.hash}: {warning}")
|
|
710
|
+
|
|
711
|
+
release_notes = aggregate_release_notes_with_llms(
|
|
712
|
+
base_url=openai_base_url,
|
|
713
|
+
api_key=openai_api_key,
|
|
714
|
+
model=openai_model,
|
|
715
|
+
github_repository=github_repository,
|
|
716
|
+
commits=commits,
|
|
717
|
+
changes=changes,
|
|
718
|
+
)
|
|
719
|
+
print_to_console("## Generated release notes:")
|
|
720
|
+
print_to_console(release_notes)
|
|
721
|
+
|
|
493
722
|
if not dry_run:
|
|
494
723
|
create_tag(
|
|
495
724
|
repository_path=repository_path,
|
|
@@ -607,6 +836,19 @@ def main():
|
|
|
607
836
|
"--github-repository",
|
|
608
837
|
help="GitHub repository in the 'owner/repo' format, if not set will use GH_REPOSITORY env var",
|
|
609
838
|
)
|
|
839
|
+
parser.add_argument(
|
|
840
|
+
"--openai-base-url",
|
|
841
|
+
default="https://api.openai.com",
|
|
842
|
+
help="OpenAI API base URL",
|
|
843
|
+
)
|
|
844
|
+
parser.add_argument(
|
|
845
|
+
"--openai-api-key",
|
|
846
|
+
help="OpenAI API key for text generation",
|
|
847
|
+
)
|
|
848
|
+
parser.add_argument(
|
|
849
|
+
"--openai-model",
|
|
850
|
+
help="OpenAI model to use for text generation (e.g., 'text-davinci-003' or 'text-davinci-002')",
|
|
851
|
+
)
|
|
610
852
|
parser.add_argument(
|
|
611
853
|
"--create-release",
|
|
612
854
|
action="store_true",
|
|
@@ -654,6 +896,9 @@ def main():
|
|
|
654
896
|
args.git_user_email = os.environ.get("TINYSEMVER_GIT_USER_EMAIL", "tinysemver@ashvardanian.com")
|
|
655
897
|
args.github_token = os.environ.get("GITHUB_TOKEN")
|
|
656
898
|
args.github_repository = os.environ.get("GITHUB_REPOSITORY")
|
|
899
|
+
args.openai_base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com")
|
|
900
|
+
args.openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
901
|
+
args.openai_model = os.environ.get("OPENAI_MODEL")
|
|
657
902
|
args.create_release = os.environ.get("TINYSEMVER_CREATE_RELEASE", "").lower() == "true"
|
|
658
903
|
|
|
659
904
|
# It's common for a CI pipeline to have multiple broken settings or missing files.
|
|
@@ -682,6 +927,9 @@ def main():
|
|
|
682
927
|
git_user_email=args.git_user_email,
|
|
683
928
|
github_token=args.github_token,
|
|
684
929
|
github_repository=args.github_repository,
|
|
930
|
+
openai_base_url=args.openai_base_url,
|
|
931
|
+
openai_api_key=args.openai_api_key,
|
|
932
|
+
openai_model=args.openai_model,
|
|
685
933
|
push=args.push,
|
|
686
934
|
create_release=args.create_release,
|
|
687
935
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinysemver
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.1.1
|
|
4
|
+
Summary: Semantic-Versioning with LLMs and without 300,000 lines of JS
|
|
5
5
|
Author-email: Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>, Guillaume de Rouville <31691250+grouville@users.noreply.github.com>
|
|
6
6
|
License: Apache License
|
|
7
7
|
Version 2.0, January 2004
|
|
@@ -210,7 +210,7 @@ Project-URL: Documentation, https://github.com/ashvardanian/tinysemver/blob/main
|
|
|
210
210
|
Project-URL: Repository, https://github.com/ashvardanian/tinysemver.git
|
|
211
211
|
Project-URL: Issues, https://github.com/ashvardanian/tinysemver/issues
|
|
212
212
|
Project-URL: Changelog, https://github.com/ashvardanian/tinysemver/blob/main/CHANGELOG.md
|
|
213
|
-
Classifier: Development Status ::
|
|
213
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
214
214
|
Classifier: Intended Audience :: Developers
|
|
215
215
|
Classifier: Topic :: Software Development :: Build Tools
|
|
216
216
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -223,10 +223,12 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
223
223
|
Classifier: Programming Language :: Python :: 3.11
|
|
224
224
|
Description-Content-Type: text/markdown
|
|
225
225
|
License-File: LICENSE
|
|
226
|
+
Requires-Dist: openai>=1.54.4
|
|
227
|
+
Requires-Dist: rich>=13.8.0
|
|
226
228
|
|
|
227
|
-

|
|
229
|
+

|
|
228
230
|
|
|
229
|
-
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file.
|
|
231
|
+
__TinySemVer__ is a minimalistic [Semantic Versioning](https://semver.org/) package for projects following [Conventional Commits](https://www.conventionalcommits.org/) in a single short Python file, capable of generating changelogs and release notes with the use of OpenAI-compatible LLM APIs, or without them.
|
|
230
232
|
In plain English, if your commit messages look like `feat: add new feature` or `fix: bugfix`, this package will automate releasing new "GIT tags" based on the commit messages.
|
|
231
233
|
Here is how to integrate it into your project CI:
|
|
232
234
|
|
|
@@ -304,6 +306,19 @@ Alternatively, you can just ask for `--help`:
|
|
|
304
306
|
$ tinysemver --help
|
|
305
307
|
```
|
|
306
308
|
|
|
309
|
+
## AI and Rock-n-Roll
|
|
310
|
+
|
|
311
|
+
TinySemVer can leverage a language model to validate the commits and generate clean and infromative release notes.
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
$ tinysemver --verbose \
|
|
315
|
+
--github-repository 'ashvardanian/tinysemver' \
|
|
316
|
+
--openai-base-url 'https://api.groq.com/openai/v1' \
|
|
317
|
+
--openai-api-key 'GET_YOURSELF_A_KEY' \
|
|
318
|
+
--openai-model 'llama-3.2-11b-text-preview' \
|
|
319
|
+
--dry-run
|
|
320
|
+
```
|
|
321
|
+
|
|
307
322
|
## Usage Details for the GitHub CI Action
|
|
308
323
|
|
|
309
324
|
TinySemVer can be easily integrated into your GitHub Actions CI pipeline.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|