tinysemver 2.0.8__tar.gz → 2.1.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.
- {tinysemver-2.0.8/tinysemver.egg-info → tinysemver-2.1.0}/PKG-INFO +20 -5
- {tinysemver-2.0.8 → tinysemver-2.1.0}/README.md +15 -2
- {tinysemver-2.0.8 → tinysemver-2.1.0}/pyproject.toml +4 -3
- {tinysemver-2.0.8 → tinysemver-2.1.0}/tinysemver/tinysemver.py +266 -20
- {tinysemver-2.0.8 → tinysemver-2.1.0/tinysemver.egg-info}/PKG-INFO +20 -5
- {tinysemver-2.0.8 → tinysemver-2.1.0}/tinysemver.egg-info/SOURCES.txt +1 -0
- tinysemver-2.1.0/tinysemver.egg-info/requires.txt +2 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/LICENSE +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/setup.cfg +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/tinysemver/__init__.py +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/tinysemver.egg-info/dependency_links.txt +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/tinysemver.egg-info/entry_points.txt +0 -0
- {tinysemver-2.0.8 → tinysemver-2.1.0}/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.0
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.1.0
|
|
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.0
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.1.0"
|
|
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 'CONTIBUTING.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,120 @@ 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
|
+
header_message = f"""Commits:
|
|
499
|
+
|
|
500
|
+
{commits_with_hashes}
|
|
501
|
+
"""
|
|
502
|
+
changes_messages = [f"#{commit.hash}: {commit.message}\n{change}" for commit, change in zip(commits, changes)]
|
|
503
|
+
|
|
504
|
+
client = OpenAI(base_url=base_url, api_key=api_key)
|
|
505
|
+
response = client.chat.completions.create(
|
|
506
|
+
messages=[
|
|
507
|
+
{"role": "system", "content": prompt},
|
|
508
|
+
{"role": "user", "content": header_message},
|
|
509
|
+
*[{"role": "user", "content": message} for message in changes_messages],
|
|
510
|
+
],
|
|
511
|
+
max_tokens=2048,
|
|
512
|
+
model=model,
|
|
513
|
+
stream=False,
|
|
514
|
+
)
|
|
515
|
+
return response.choices[0].message.content
|
|
332
516
|
|
|
333
517
|
|
|
334
518
|
def bump(
|
|
@@ -350,6 +534,9 @@ def bump(
|
|
|
350
534
|
push: bool = True,
|
|
351
535
|
github_token: Optional[str] = None,
|
|
352
536
|
github_repository: Optional[str] = None,
|
|
537
|
+
openai_base_url: str = "https://api.openai.com",
|
|
538
|
+
openai_api_key: Optional[str] = None,
|
|
539
|
+
openai_model: Optional[str] = None,
|
|
353
540
|
default_branch: str = "main",
|
|
354
541
|
create_release: bool = False,
|
|
355
542
|
) -> SemVer:
|
|
@@ -375,6 +562,9 @@ def bump(
|
|
|
375
562
|
push (bool): If True, pushes the changes to the remote GitHub repository. Defaults to True.
|
|
376
563
|
github_token (Optional[str]): The GitHub token for authentication. If None, it attempts to use the GH_TOKEN environment variable. Defaults to None.
|
|
377
564
|
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.
|
|
565
|
+
openai_base_url (str): The OpenAI API base URL. Defaults to "https://api.openai.com".
|
|
566
|
+
openai_api_key (Optional[str]): The OpenAI API key. Defaults to None.
|
|
567
|
+
openai_model (Optional[str]): The OpenAI model to use for text generation. Defaults to None.
|
|
378
568
|
default_branch (str): The default branch to push the changes to. Defaults to "main".
|
|
379
569
|
|
|
380
570
|
Returns:
|
|
@@ -490,6 +680,43 @@ def bump(
|
|
|
490
680
|
for file_path, regex_pattern in update_patch_version_in:
|
|
491
681
|
patch_with_regex(file_path, regex_pattern, str(new_version[2]), dry_run=dry_run, verbose=verbose)
|
|
492
682
|
|
|
683
|
+
# Now log the potential issues with the commits
|
|
684
|
+
if openai_api_key:
|
|
685
|
+
warnings_commits = []
|
|
686
|
+
warnings = []
|
|
687
|
+
changes = [get_diff_for_commit(repository_path, commit.hash) for commit in commits]
|
|
688
|
+
for commit, change in zip(commits, changes):
|
|
689
|
+
try:
|
|
690
|
+
warning = validate_commit_with_llms(
|
|
691
|
+
base_url=openai_base_url,
|
|
692
|
+
api_key=openai_api_key,
|
|
693
|
+
model=openai_model,
|
|
694
|
+
commit=commit,
|
|
695
|
+
change=change,
|
|
696
|
+
)
|
|
697
|
+
if warning:
|
|
698
|
+
warnings_commits.append(commit)
|
|
699
|
+
warnings.append(warning)
|
|
700
|
+
except Exception as e:
|
|
701
|
+
print(f"Failed to validate commit: {commit.hash} with error: {str(e)}")
|
|
702
|
+
traceback.print_exc()
|
|
703
|
+
|
|
704
|
+
if len(warnings):
|
|
705
|
+
print("## Potential issues")
|
|
706
|
+
for commit, warning in zip(warnings_commits, warnings):
|
|
707
|
+
print(f"- Commit #{commit.hash}: {warning}")
|
|
708
|
+
|
|
709
|
+
release_notes = aggregate_release_notes_with_llms(
|
|
710
|
+
base_url=openai_base_url,
|
|
711
|
+
api_key=openai_api_key,
|
|
712
|
+
model=openai_model,
|
|
713
|
+
github_repository=github_repository,
|
|
714
|
+
commits=commits,
|
|
715
|
+
changes=changes,
|
|
716
|
+
)
|
|
717
|
+
print("## Generated release notes:")
|
|
718
|
+
print(release_notes)
|
|
719
|
+
|
|
493
720
|
if not dry_run:
|
|
494
721
|
create_tag(
|
|
495
722
|
repository_path=repository_path,
|
|
@@ -607,6 +834,19 @@ def main():
|
|
|
607
834
|
"--github-repository",
|
|
608
835
|
help="GitHub repository in the 'owner/repo' format, if not set will use GH_REPOSITORY env var",
|
|
609
836
|
)
|
|
837
|
+
parser.add_argument(
|
|
838
|
+
"--openai-base-url",
|
|
839
|
+
default="https://api.openai.com",
|
|
840
|
+
help="OpenAI API base URL",
|
|
841
|
+
)
|
|
842
|
+
parser.add_argument(
|
|
843
|
+
"--openai-api-key",
|
|
844
|
+
help="OpenAI API key for text generation",
|
|
845
|
+
)
|
|
846
|
+
parser.add_argument(
|
|
847
|
+
"--openai-model",
|
|
848
|
+
help="OpenAI model to use for text generation (e.g., 'text-davinci-003' or 'text-davinci-002')",
|
|
849
|
+
)
|
|
610
850
|
parser.add_argument(
|
|
611
851
|
"--create-release",
|
|
612
852
|
action="store_true",
|
|
@@ -654,6 +894,9 @@ def main():
|
|
|
654
894
|
args.git_user_email = os.environ.get("TINYSEMVER_GIT_USER_EMAIL", "tinysemver@ashvardanian.com")
|
|
655
895
|
args.github_token = os.environ.get("GITHUB_TOKEN")
|
|
656
896
|
args.github_repository = os.environ.get("GITHUB_REPOSITORY")
|
|
897
|
+
args.openai_base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com")
|
|
898
|
+
args.openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
899
|
+
args.openai_model = os.environ.get("OPENAI_MODEL")
|
|
657
900
|
args.create_release = os.environ.get("TINYSEMVER_CREATE_RELEASE", "").lower() == "true"
|
|
658
901
|
|
|
659
902
|
# It's common for a CI pipeline to have multiple broken settings or missing files.
|
|
@@ -682,6 +925,9 @@ def main():
|
|
|
682
925
|
git_user_email=args.git_user_email,
|
|
683
926
|
github_token=args.github_token,
|
|
684
927
|
github_repository=args.github_repository,
|
|
928
|
+
openai_base_url=args.openai_base_url,
|
|
929
|
+
openai_api_key=args.openai_api_key,
|
|
930
|
+
openai_model=args.openai_model,
|
|
685
931
|
push=args.push,
|
|
686
932
|
create_release=args.create_release,
|
|
687
933
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinysemver
|
|
3
|
-
Version: 2.0
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.1.0
|
|
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
|