releaseguru 0.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.
@@ -0,0 +1,11 @@
1
+ # Copy this to .env and fill in only the key you want to use
2
+ # Only ONE provider key is required, or you can pass them via CLI `--api-key`
3
+
4
+ GROQ_API_KEY=gsk_... # free at console.groq.com
5
+ OPENAI_API_KEY=sk-... # platform.openai.com
6
+ ANTHROPIC_API_KEY=sk-ant-... # console.anthropic.com
7
+ GEMINI_API_KEY=AIza... # aistudio.google.com (free)
8
+ XAI_API_KEY=xai-... # console.x.ai
9
+
10
+ # Optional token for automated GitHub Release Publishing
11
+ GITHUB_TOKEN=ghp_... # personal access token with 'repo' scope
@@ -0,0 +1,38 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.11"
22
+
23
+ - name: Install package and build tools
24
+ run: |
25
+ python -m pip install --upgrade pip build twine
26
+ python -m pip install -e .
27
+
28
+ - name: Run tests
29
+ run: python -m unittest discover -s tests
30
+
31
+ - name: Build package
32
+ run: python -m build
33
+
34
+ - name: Check package
35
+ run: python -m twine check dist/*
36
+
37
+ - name: Publish package
38
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .env
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ReleaseGuru Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.4
2
+ Name: releaseguru
3
+ Version: 0.1.0
4
+ Summary: AI release assistant CLI for git repositories.
5
+ Project-URL: Homepage, https://github.com/ArhamAzeem/ReleaseGuru
6
+ Project-URL: Repository, https://github.com/ArhamAzeem/ReleaseGuru
7
+ Project-URL: Issues, https://github.com/ArhamAzeem/ReleaseGuru/issues
8
+ Author: ReleaseGuru Contributors
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,changelog,cli,github,release,semver
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Build Tools
22
+ Classifier: Topic :: Software Development :: Version Control :: Git
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: anthropic>=0.30
25
+ Requires-Dist: click>=8.0
26
+ Requires-Dist: google-generativeai>=0.7
27
+ Requires-Dist: groq>=0.9
28
+ Requires-Dist: openai>=1.0
29
+ Requires-Dist: python-dotenv>=1.0
30
+ Requires-Dist: requests>=2.25
31
+ Description-Content-Type: text/markdown
32
+
33
+ # ReleaseGuru
34
+
35
+ ReleaseGuru is an AI release assistant CLI for git repositories.
36
+
37
+ Main feature: turn commit history into a release plan that tells the team what changed, what matters, what to test, and what can be published.
38
+
39
+ Secondary helpers: version bumping, release notes, GitHub Releases, JSON output, and GitHub Actions outputs.
40
+
41
+ ## Features
42
+
43
+ - AI release brief from git commits
44
+ - Release summary, highlights, risks, checks, and user-facing notes
45
+ - SemVer recommendation: `major`, `minor`, or `patch`
46
+ - Auto-version from latest git tag
47
+ - Manual version override
48
+ - Markdown or JSON output
49
+ - Notes-only mode for GitHub release bodies
50
+ - Dry-run mode for CI and review
51
+ - GitHub Release publish
52
+ - Draft and prerelease publish modes
53
+ - GitHub repo override with `--github-repo`
54
+ - Optional tag creation and push
55
+ - GitHub Actions output support with `--ci-output`
56
+ - Multi-provider support: Groq, Gemini, OpenAI, Anthropic, xAI
57
+
58
+ ## Install
59
+
60
+ After PyPI publish:
61
+
62
+ ```bash
63
+ pipx install releaseguru
64
+ ```
65
+
66
+ Or:
67
+
68
+ ```bash
69
+ pip install releaseguru
70
+ ```
71
+
72
+ During local development:
73
+
74
+ ```bash
75
+ cd ReleaseGuru
76
+ python -m venv venv
77
+ pip install -e .
78
+ cp .env.example .env
79
+ ```
80
+
81
+ Add one AI provider key to `.env`.
82
+
83
+ ```env
84
+ GROQ_API_KEY=
85
+ OPENAI_API_KEY=
86
+ ANTHROPIC_API_KEY=
87
+ GEMINI_API_KEY=
88
+ XAI_API_KEY=
89
+ GITHUB_TOKEN=
90
+ ```
91
+
92
+ `GITHUB_TOKEN` is only needed when publishing.
93
+
94
+ ## Basic Use
95
+
96
+ Generate release plan:
97
+
98
+ ```bash
99
+ releaseguru
100
+ ```
101
+
102
+ Use custom range:
103
+
104
+ ```bash
105
+ releaseguru --from v1.2.0 --to HEAD
106
+ ```
107
+
108
+ Save to file:
109
+
110
+ ```bash
111
+ releaseguru --output RELEASE.md
112
+ ```
113
+
114
+ Generate JSON for automation:
115
+
116
+ ```bash
117
+ releaseguru --format json --output release.json
118
+ ```
119
+
120
+ Only print release notes:
121
+
122
+ ```bash
123
+ releaseguru --notes-only
124
+ ```
125
+
126
+ Dry run publish flow:
127
+
128
+ ```bash
129
+ releaseguru --publish --dry-run --github-repo owner/repo
130
+ ```
131
+
132
+ Publish full release:
133
+
134
+ ```bash
135
+ releaseguru --publish
136
+ ```
137
+
138
+ Publish only notes body:
139
+
140
+ ```bash
141
+ releaseguru --publish --notes-only
142
+ ```
143
+
144
+ Publish draft:
145
+
146
+ ```bash
147
+ releaseguru --publish --draft
148
+ ```
149
+
150
+ Publish prerelease:
151
+
152
+ ```bash
153
+ releaseguru --publish --prerelease
154
+ ```
155
+
156
+ Publish without creating/pushing a local tag:
157
+
158
+ ```bash
159
+ releaseguru --publish --skip-tag
160
+ ```
161
+
162
+ ## GitHub Actions
163
+
164
+ Use this when you want manual release from Actions.
165
+
166
+ ```yaml
167
+ name: ReleaseGuru
168
+
169
+ on:
170
+ workflow_dispatch:
171
+
172
+ permissions:
173
+ contents: write
174
+
175
+ jobs:
176
+ release:
177
+ runs-on: ubuntu-latest
178
+
179
+ steps:
180
+ - uses: actions/checkout@v4
181
+ with:
182
+ fetch-depth: 0
183
+
184
+ - uses: actions/setup-python@v5
185
+ with:
186
+ python-version: "3.11"
187
+
188
+ - name: Install ReleaseGuru
189
+ run: pip install releaseguru
190
+ - name: Create release
191
+ id: releaseguru
192
+ env:
193
+ GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
194
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
195
+ run: releaseguru --publish --notes-only --ci-output
196
+
197
+ - name: Show result
198
+ run: |
199
+ echo "Version: ${{ steps.releaseguru.outputs.version }}"
200
+ echo "Bump: ${{ steps.releaseguru.outputs.bump_type }}"
201
+ echo "URL: ${{ steps.releaseguru.outputs.release_url }}"
202
+ ```
203
+
204
+ For review before publishing:
205
+
206
+ ```yaml
207
+ - name: Preview release
208
+ env:
209
+ GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
210
+ run: releaseguru --dry-run --format json --output release.json
211
+ ```
212
+
213
+ ## Testing
214
+
215
+ Syntax check:
216
+
217
+ ```bash
218
+ python -m py_compile releaseguru/cli.py releaseguru/providers.py
219
+ ```
220
+
221
+ Unit tests:
222
+
223
+ ```bash
224
+ python -m unittest discover -s tests
225
+ ```
226
+
227
+ CLI dry run with mocked AI response:
228
+
229
+ ```bash
230
+ releaseguru --mock-response tests/fixtures/ai_response.txt --dry-run
231
+ ```
232
+
233
+ Real AI test:
234
+
235
+ ```bash
236
+ releaseguru --provider groq --dry-run
237
+ ```
238
+
239
+ Real GitHub publish test:
240
+
241
+ ```bash
242
+ releaseguru --provider groq --publish --draft
243
+ ```
244
+
245
+ Use `--draft` first so you can inspect the release before making it public.
246
+
247
+ ## Release Ways
248
+
249
+ - Local preview: `releaseguru --dry-run`
250
+ - Local file: `releaseguru --output RELEASE.md`
251
+ - CI preview: `releaseguru --format json --ci-output`
252
+ - GitHub draft: `releaseguru --publish --draft`
253
+ - GitHub prerelease: `releaseguru --publish --prerelease`
254
+ - GitHub stable release: `releaseguru --publish`
255
+ - Notes-only release body: `releaseguru --publish --notes-only`
256
+ - Existing tag release: `releaseguru --publish --skip-tag --version v1.2.3`
257
+ - Manual version release: `releaseguru --version v2.0.0 --publish`
258
+
259
+ ## Publish To PyPI
260
+
261
+ Build package:
262
+
263
+ ```bash
264
+ python -m pip install --upgrade build twine
265
+ python -m build
266
+ ```
267
+
268
+ Check package:
269
+
270
+ ```bash
271
+ python -m twine check dist/*
272
+ ```
273
+
274
+ Upload to TestPyPI first:
275
+
276
+ ```bash
277
+ python -m twine upload --repository testpypi dist/*
278
+ ```
279
+
280
+ Install from TestPyPI:
281
+
282
+ ```bash
283
+ pipx install --index-url https://test.pypi.org/simple/ --pip-args="--extra-index-url https://pypi.org/simple/" releaseguru
284
+ ```
285
+
286
+ Upload to real PyPI:
287
+
288
+ ```bash
289
+ python -m twine upload dist/*
290
+ ```
291
+
292
+ GitHub Actions publishing is also included in `.github/workflows/publish-pypi.yml`.
293
+ For tokenless publishing, configure PyPI Trusted Publishing for this repository and publish a GitHub Release.
294
+
295
+ ## Provider Defaults
296
+
297
+ | Provider | Option | Default model |
298
+ |---|---|---|
299
+ | Groq | `--provider groq` | `llama-3.3-70b-versatile` |
300
+ | Gemini | `--provider gemini` | `gemini-1.5-flash` |
301
+ | OpenAI | `--provider openai` | `gpt-4o-mini` |
302
+ | Anthropic | `--provider anthropic` | `claude-3-5-haiku-20241022` |
303
+ | xAI | `--provider xai` | `grok-3-mini` |
@@ -0,0 +1,271 @@
1
+ # ReleaseGuru
2
+
3
+ ReleaseGuru is an AI release assistant CLI for git repositories.
4
+
5
+ Main feature: turn commit history into a release plan that tells the team what changed, what matters, what to test, and what can be published.
6
+
7
+ Secondary helpers: version bumping, release notes, GitHub Releases, JSON output, and GitHub Actions outputs.
8
+
9
+ ## Features
10
+
11
+ - AI release brief from git commits
12
+ - Release summary, highlights, risks, checks, and user-facing notes
13
+ - SemVer recommendation: `major`, `minor`, or `patch`
14
+ - Auto-version from latest git tag
15
+ - Manual version override
16
+ - Markdown or JSON output
17
+ - Notes-only mode for GitHub release bodies
18
+ - Dry-run mode for CI and review
19
+ - GitHub Release publish
20
+ - Draft and prerelease publish modes
21
+ - GitHub repo override with `--github-repo`
22
+ - Optional tag creation and push
23
+ - GitHub Actions output support with `--ci-output`
24
+ - Multi-provider support: Groq, Gemini, OpenAI, Anthropic, xAI
25
+
26
+ ## Install
27
+
28
+ After PyPI publish:
29
+
30
+ ```bash
31
+ pipx install releaseguru
32
+ ```
33
+
34
+ Or:
35
+
36
+ ```bash
37
+ pip install releaseguru
38
+ ```
39
+
40
+ During local development:
41
+
42
+ ```bash
43
+ cd ReleaseGuru
44
+ python -m venv venv
45
+ pip install -e .
46
+ cp .env.example .env
47
+ ```
48
+
49
+ Add one AI provider key to `.env`.
50
+
51
+ ```env
52
+ GROQ_API_KEY=
53
+ OPENAI_API_KEY=
54
+ ANTHROPIC_API_KEY=
55
+ GEMINI_API_KEY=
56
+ XAI_API_KEY=
57
+ GITHUB_TOKEN=
58
+ ```
59
+
60
+ `GITHUB_TOKEN` is only needed when publishing.
61
+
62
+ ## Basic Use
63
+
64
+ Generate release plan:
65
+
66
+ ```bash
67
+ releaseguru
68
+ ```
69
+
70
+ Use custom range:
71
+
72
+ ```bash
73
+ releaseguru --from v1.2.0 --to HEAD
74
+ ```
75
+
76
+ Save to file:
77
+
78
+ ```bash
79
+ releaseguru --output RELEASE.md
80
+ ```
81
+
82
+ Generate JSON for automation:
83
+
84
+ ```bash
85
+ releaseguru --format json --output release.json
86
+ ```
87
+
88
+ Only print release notes:
89
+
90
+ ```bash
91
+ releaseguru --notes-only
92
+ ```
93
+
94
+ Dry run publish flow:
95
+
96
+ ```bash
97
+ releaseguru --publish --dry-run --github-repo owner/repo
98
+ ```
99
+
100
+ Publish full release:
101
+
102
+ ```bash
103
+ releaseguru --publish
104
+ ```
105
+
106
+ Publish only notes body:
107
+
108
+ ```bash
109
+ releaseguru --publish --notes-only
110
+ ```
111
+
112
+ Publish draft:
113
+
114
+ ```bash
115
+ releaseguru --publish --draft
116
+ ```
117
+
118
+ Publish prerelease:
119
+
120
+ ```bash
121
+ releaseguru --publish --prerelease
122
+ ```
123
+
124
+ Publish without creating/pushing a local tag:
125
+
126
+ ```bash
127
+ releaseguru --publish --skip-tag
128
+ ```
129
+
130
+ ## GitHub Actions
131
+
132
+ Use this when you want manual release from Actions.
133
+
134
+ ```yaml
135
+ name: ReleaseGuru
136
+
137
+ on:
138
+ workflow_dispatch:
139
+
140
+ permissions:
141
+ contents: write
142
+
143
+ jobs:
144
+ release:
145
+ runs-on: ubuntu-latest
146
+
147
+ steps:
148
+ - uses: actions/checkout@v4
149
+ with:
150
+ fetch-depth: 0
151
+
152
+ - uses: actions/setup-python@v5
153
+ with:
154
+ python-version: "3.11"
155
+
156
+ - name: Install ReleaseGuru
157
+ run: pip install releaseguru
158
+ - name: Create release
159
+ id: releaseguru
160
+ env:
161
+ GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
162
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163
+ run: releaseguru --publish --notes-only --ci-output
164
+
165
+ - name: Show result
166
+ run: |
167
+ echo "Version: ${{ steps.releaseguru.outputs.version }}"
168
+ echo "Bump: ${{ steps.releaseguru.outputs.bump_type }}"
169
+ echo "URL: ${{ steps.releaseguru.outputs.release_url }}"
170
+ ```
171
+
172
+ For review before publishing:
173
+
174
+ ```yaml
175
+ - name: Preview release
176
+ env:
177
+ GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
178
+ run: releaseguru --dry-run --format json --output release.json
179
+ ```
180
+
181
+ ## Testing
182
+
183
+ Syntax check:
184
+
185
+ ```bash
186
+ python -m py_compile releaseguru/cli.py releaseguru/providers.py
187
+ ```
188
+
189
+ Unit tests:
190
+
191
+ ```bash
192
+ python -m unittest discover -s tests
193
+ ```
194
+
195
+ CLI dry run with mocked AI response:
196
+
197
+ ```bash
198
+ releaseguru --mock-response tests/fixtures/ai_response.txt --dry-run
199
+ ```
200
+
201
+ Real AI test:
202
+
203
+ ```bash
204
+ releaseguru --provider groq --dry-run
205
+ ```
206
+
207
+ Real GitHub publish test:
208
+
209
+ ```bash
210
+ releaseguru --provider groq --publish --draft
211
+ ```
212
+
213
+ Use `--draft` first so you can inspect the release before making it public.
214
+
215
+ ## Release Ways
216
+
217
+ - Local preview: `releaseguru --dry-run`
218
+ - Local file: `releaseguru --output RELEASE.md`
219
+ - CI preview: `releaseguru --format json --ci-output`
220
+ - GitHub draft: `releaseguru --publish --draft`
221
+ - GitHub prerelease: `releaseguru --publish --prerelease`
222
+ - GitHub stable release: `releaseguru --publish`
223
+ - Notes-only release body: `releaseguru --publish --notes-only`
224
+ - Existing tag release: `releaseguru --publish --skip-tag --version v1.2.3`
225
+ - Manual version release: `releaseguru --version v2.0.0 --publish`
226
+
227
+ ## Publish To PyPI
228
+
229
+ Build package:
230
+
231
+ ```bash
232
+ python -m pip install --upgrade build twine
233
+ python -m build
234
+ ```
235
+
236
+ Check package:
237
+
238
+ ```bash
239
+ python -m twine check dist/*
240
+ ```
241
+
242
+ Upload to TestPyPI first:
243
+
244
+ ```bash
245
+ python -m twine upload --repository testpypi dist/*
246
+ ```
247
+
248
+ Install from TestPyPI:
249
+
250
+ ```bash
251
+ pipx install --index-url https://test.pypi.org/simple/ --pip-args="--extra-index-url https://pypi.org/simple/" releaseguru
252
+ ```
253
+
254
+ Upload to real PyPI:
255
+
256
+ ```bash
257
+ python -m twine upload dist/*
258
+ ```
259
+
260
+ GitHub Actions publishing is also included in `.github/workflows/publish-pypi.yml`.
261
+ For tokenless publishing, configure PyPI Trusted Publishing for this repository and publish a GitHub Release.
262
+
263
+ ## Provider Defaults
264
+
265
+ | Provider | Option | Default model |
266
+ |---|---|---|
267
+ | Groq | `--provider groq` | `llama-3.3-70b-versatile` |
268
+ | Gemini | `--provider gemini` | `gemini-1.5-flash` |
269
+ | OpenAI | `--provider openai` | `gpt-4o-mini` |
270
+ | Anthropic | `--provider anthropic` | `claude-3-5-haiku-20241022` |
271
+ | xAI | `--provider xai` | `grok-3-mini` |
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "releaseguru"
7
+ version = "0.1.0"
8
+ description = "AI release assistant CLI for git repositories."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "ReleaseGuru Contributors" }
14
+ ]
15
+ keywords = ["release", "changelog", "github", "cli", "ai", "semver"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Version Control :: Git",
27
+ "Topic :: Software Development :: Build Tools",
28
+ ]
29
+ dependencies = [
30
+ "click>=8.0",
31
+ "groq>=0.9",
32
+ "openai>=1.0",
33
+ "anthropic>=0.30",
34
+ "google-generativeai>=0.7",
35
+ "python-dotenv>=1.0",
36
+ "requests>=2.25",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/ArhamAzeem/ReleaseGuru"
41
+ Repository = "https://github.com/ArhamAzeem/ReleaseGuru"
42
+ Issues = "https://github.com/ArhamAzeem/ReleaseGuru/issues"
43
+
44
+ [project.scripts]
45
+ releaseguru = "releaseguru.cli:main"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["releaseguru"]
@@ -0,0 +1,3 @@
1
+ """ReleaseGuru package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,368 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import subprocess
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+
8
+ import click
9
+ import requests
10
+ from dotenv import load_dotenv
11
+
12
+ from .providers import PROVIDERS, auto_detect_provider
13
+
14
+ load_dotenv()
15
+
16
+
17
+ @dataclass
18
+ class ReleaseResult:
19
+ version: str
20
+ bump_type: str
21
+ release_brief: str
22
+ release_notes: str
23
+ commit_count: int
24
+ from_ref: str | None
25
+ to_ref: str
26
+ provider: str
27
+ published_url: str | None = None
28
+
29
+
30
+ def run_git(args: list[str], check: bool = True) -> subprocess.CompletedProcess:
31
+ return subprocess.run(
32
+ ["git", *args],
33
+ capture_output=True,
34
+ text=True,
35
+ check=check,
36
+ )
37
+
38
+
39
+ def get_commits(from_ref: str | None, to_ref: str) -> list[str]:
40
+ range_spec = f"{from_ref}..{to_ref}" if from_ref else to_ref
41
+ result = run_git(["log", range_spec, "--pretty=format:%s (%an)"])
42
+ return [line.strip() for line in result.stdout.splitlines() if line.strip()]
43
+
44
+
45
+ def get_latest_tag() -> str | None:
46
+ result = run_git(["describe", "--tags", "--abbrev=0"], check=False)
47
+ return result.stdout.strip() if result.returncode == 0 else None
48
+
49
+
50
+ def get_github_repo_slug() -> str | None:
51
+ result = run_git(["remote", "get-url", "origin"], check=False)
52
+ if result.returncode != 0:
53
+ return None
54
+
55
+ match = re.search(r"github\.com[:/]([^/]+)/([^.]+)(?:\.git)?", result.stdout.strip())
56
+ if not match:
57
+ return None
58
+ return f"{match.group(1)}/{match.group(2)}"
59
+
60
+
61
+ def tag_exists(tag_name: str) -> bool:
62
+ return run_git(["rev-parse", "-q", "--verify", f"refs/tags/{tag_name}"], check=False).returncode == 0
63
+
64
+
65
+ def create_and_push_tag(tag_name: str, dry_run: bool) -> None:
66
+ if dry_run:
67
+ return
68
+
69
+ if not tag_exists(tag_name):
70
+ run_git(["tag", "-a", tag_name, "-m", f"Release {tag_name}"])
71
+
72
+ run_git(["push", "origin", tag_name])
73
+
74
+
75
+ def bump_version(version: str, bump_type: str) -> str:
76
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", version)
77
+ if not match:
78
+ return f"{version}-{bump_type}-bump"
79
+
80
+ major, minor, patch = map(int, match.groups())
81
+ if bump_type == "major":
82
+ major += 1
83
+ minor = 0
84
+ patch = 0
85
+ elif bump_type == "minor":
86
+ minor += 1
87
+ patch = 0
88
+ else:
89
+ patch += 1
90
+
91
+ prefix = "v" if version.lower().startswith("v") else ""
92
+ return f"{prefix}{major}.{minor}.{patch}"
93
+
94
+
95
+ def build_prompt(commits: list[str], current_version: str) -> str:
96
+ lines = "\n".join(f"- {commit}" for commit in commits)
97
+ return f"""You are a release assistant for software teams.
98
+
99
+ Analyze the git commits and prepare a release brief for humans. Help the team decide what this release is, what matters most, what needs attention, and what can be published.
100
+
101
+ You MUST start the response with this exact line:
102
+ __RECOMMENDED_BUMP__: <major|minor|patch>
103
+
104
+ After that, output valid Markdown with these sections in this order:
105
+ # Release Assistant Brief
106
+ ## Release Summary
107
+ ## Highlights
108
+ ## Risks & Checks
109
+ ## Suggested Release Notes
110
+
111
+ Rules:
112
+ - The release summary must be 2 to 4 sentences.
113
+ - Highlights must contain 3 to 6 bullets when possible.
114
+ - Risks & Checks must focus on validation, regressions, migrations, or rollout concerns.
115
+ - Suggested Release Notes must be user-facing and grouped under:
116
+ - ### Features
117
+ - ### Bug Fixes
118
+ - ### Breaking Changes
119
+ - ### Improvements
120
+ - Skip empty sections inside Suggested Release Notes.
121
+ - Use present tense.
122
+ - Avoid internal jargon unless needed for risk explanation.
123
+ - Mention breaking changes only when justified by commits.
124
+ - Choose major for breaking changes, minor for new visible functionality, patch for fixes/internal improvements only.
125
+
126
+ Current version context: {current_version}
127
+
128
+ Commits:
129
+ {lines}
130
+
131
+ Output only the marker line and Markdown."""
132
+
133
+
134
+ def parse_ai_response(ai_raw: str) -> tuple[str, str]:
135
+ bump_type = "patch"
136
+ body_lines: list[str] = []
137
+
138
+ for line in ai_raw.splitlines():
139
+ if line.startswith("__RECOMMENDED_BUMP__"):
140
+ match = re.search(r"__RECOMMENDED_BUMP__:\s*(major|minor|patch)", line, re.IGNORECASE)
141
+ if match:
142
+ bump_type = match.group(1).lower()
143
+ continue
144
+ body_lines.append(line)
145
+
146
+ return bump_type, "\n".join(body_lines).strip()
147
+
148
+
149
+ def extract_release_notes(release_brief: str) -> str:
150
+ marker = "## Suggested Release Notes"
151
+ if marker not in release_brief:
152
+ return release_brief.strip()
153
+ return release_brief.split(marker, 1)[1].strip()
154
+
155
+
156
+ def build_output_document(result: ReleaseResult) -> str:
157
+ header = [
158
+ f"# Release Plan {result.version}",
159
+ "",
160
+ f"- Recommended bump: `{result.bump_type}`",
161
+ f"- Commits analyzed: `{result.commit_count}`",
162
+ "",
163
+ ]
164
+ return "\n".join(header) + result.release_brief.strip() + "\n"
165
+
166
+
167
+ def publish_github_release(
168
+ repo_slug: str,
169
+ tag_name: str,
170
+ name: str,
171
+ body: str,
172
+ token: str,
173
+ draft: bool,
174
+ prerelease: bool,
175
+ dry_run: bool,
176
+ ) -> str:
177
+ if dry_run:
178
+ return f"https://github.com/{repo_slug}/releases/tag/{tag_name}"
179
+
180
+ url = f"https://api.github.com/repos/{repo_slug}/releases"
181
+ headers = {
182
+ "Authorization": f"Bearer {token}",
183
+ "Accept": "application/vnd.github+json",
184
+ "X-GitHub-Api-Version": "2022-11-28",
185
+ }
186
+ payload = {
187
+ "tag_name": tag_name,
188
+ "name": name,
189
+ "body": body,
190
+ "draft": draft,
191
+ "prerelease": prerelease,
192
+ }
193
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
194
+ if response.status_code not in (200, 201):
195
+ raise click.ClickException(f"GitHub API error: {response.status_code} - {response.text}")
196
+ return response.json().get("html_url", "")
197
+
198
+
199
+ def write_github_output(result: ReleaseResult) -> None:
200
+ output_path = os.environ.get("GITHUB_OUTPUT")
201
+ if not output_path:
202
+ return
203
+
204
+ lines = [
205
+ f"version={result.version}",
206
+ f"bump_type={result.bump_type}",
207
+ f"commit_count={result.commit_count}",
208
+ ]
209
+ if result.published_url:
210
+ lines.append(f"release_url={result.published_url}")
211
+
212
+ with open(output_path, "a", encoding="utf-8") as handle:
213
+ handle.write("\n".join(lines) + "\n")
214
+
215
+
216
+ def render_result(result: ReleaseResult, output_format: str, notes_only: bool) -> str:
217
+ if output_format == "json":
218
+ return json.dumps(asdict(result), indent=2)
219
+ if notes_only:
220
+ return result.release_notes + "\n"
221
+ return build_output_document(result)
222
+
223
+
224
+ def call_provider(provider: str, prompt: str, api_key: str | None, model: str | None, mock_response: str | None) -> str:
225
+ if mock_response:
226
+ return Path(mock_response).read_text(encoding="utf-8")
227
+ return PROVIDERS[provider](prompt, api_key=api_key, model=model)
228
+
229
+
230
+ @click.command()
231
+ @click.option("--from", "from_ref", default=None, help="Start tag/ref. Default: latest git tag")
232
+ @click.option("--to", "to_ref", default="HEAD", help="End ref. Default: HEAD")
233
+ @click.option("--version", default=None, help="Explicit release version")
234
+ @click.option(
235
+ "--provider",
236
+ default=None,
237
+ type=click.Choice(["groq", "openai", "anthropic", "gemini", "xai"]),
238
+ help="LLM provider. Default: auto-detect from env keys",
239
+ )
240
+ @click.option("--api-key", default=None, help="Custom API key for the selected provider")
241
+ @click.option("--model", default=None, help="Custom model name for the selected provider")
242
+ @click.option("--publish", is_flag=True, help="Create GitHub release")
243
+ @click.option("--dry-run", is_flag=True, help="Generate output without creating tags or GitHub releases")
244
+ @click.option("--draft", is_flag=True, help="Publish GitHub release as draft")
245
+ @click.option("--prerelease", is_flag=True, help="Publish GitHub release as prerelease")
246
+ @click.option("--skip-tag", is_flag=True, help="Do not create or push a git tag before publishing")
247
+ @click.option("--github-repo", default=None, help="GitHub repo slug, e.g. owner/repo")
248
+ @click.option("--github-token", default=None, help="GitHub token. Default: GITHUB_TOKEN env var")
249
+ @click.option("--output", type=click.Path(), default=None, help="Write output to file")
250
+ @click.option("--format", "output_format", type=click.Choice(["markdown", "json"]), default="markdown")
251
+ @click.option("--notes-only", is_flag=True, help="Print only Suggested Release Notes")
252
+ @click.option("--ci-output", is_flag=True, help="Write version, bump_type, commit_count, release_url to GITHUB_OUTPUT")
253
+ @click.option("--mock-response", default=None, hidden=True, help="Read AI response from file for tests")
254
+ def main(
255
+ from_ref,
256
+ to_ref,
257
+ version,
258
+ provider,
259
+ api_key,
260
+ model,
261
+ publish,
262
+ dry_run,
263
+ draft,
264
+ prerelease,
265
+ skip_tag,
266
+ github_repo,
267
+ github_token,
268
+ output,
269
+ output_format,
270
+ notes_only,
271
+ ci_output,
272
+ mock_response,
273
+ ):
274
+ """Prepare, inspect, and optionally publish an AI-assisted release."""
275
+ latest_tag = get_latest_tag()
276
+ if not from_ref:
277
+ from_ref = latest_tag
278
+ if from_ref:
279
+ click.echo(f"Using latest tag: {from_ref}", err=True)
280
+
281
+ if not provider:
282
+ if mock_response:
283
+ provider = "mock"
284
+ elif api_key:
285
+ raise click.ClickException("Use --provider when passing --api-key.")
286
+ else:
287
+ try:
288
+ provider = auto_detect_provider()
289
+ click.echo(f"Using provider: {provider}", err=True)
290
+ except EnvironmentError as exc:
291
+ raise click.ClickException(str(exc))
292
+
293
+ try:
294
+ commits = get_commits(from_ref, to_ref)
295
+ except subprocess.CalledProcessError as exc:
296
+ detail = exc.stderr.strip() if exc.stderr else "Could not execute git log."
297
+ raise click.ClickException(f"Git error: {detail}")
298
+
299
+ if not commits:
300
+ raise click.ClickException("No commits found in that range.")
301
+
302
+ click.echo(f"Found {len(commits)} commits. Building release brief...", err=True)
303
+
304
+ current_version = version or latest_tag or "v0.0.0"
305
+ prompt = build_prompt(commits, current_version)
306
+
307
+ try:
308
+ ai_raw = call_provider(provider, prompt, api_key, model, mock_response)
309
+ except Exception as exc:
310
+ raise click.ClickException(f"Error calling {provider} provider: {exc}")
311
+
312
+ bump_type, release_brief = parse_ai_response(ai_raw)
313
+ if not version:
314
+ version = bump_version(latest_tag, bump_type) if latest_tag else "v1.0.0"
315
+ click.echo(f"Resolved release version: {version}", err=True)
316
+
317
+ result = ReleaseResult(
318
+ version=version,
319
+ bump_type=bump_type,
320
+ release_brief=release_brief,
321
+ release_notes=extract_release_notes(release_brief),
322
+ commit_count=len(commits),
323
+ from_ref=from_ref,
324
+ to_ref=to_ref,
325
+ provider=provider,
326
+ )
327
+
328
+ if publish:
329
+ token = github_token or os.environ.get("GITHUB_TOKEN")
330
+ if not token and not dry_run:
331
+ raise click.ClickException("GitHub token not found. Set GITHUB_TOKEN or pass --github-token.")
332
+
333
+ repo_slug = github_repo or get_github_repo_slug()
334
+ if not repo_slug:
335
+ raise click.ClickException("Could not detect GitHub repo. Pass --github-repo owner/repo.")
336
+
337
+ if not skip_tag:
338
+ try:
339
+ create_and_push_tag(version, dry_run)
340
+ except subprocess.CalledProcessError as exc:
341
+ detail = exc.stderr.strip() if exc.stderr else "Tag create/push failed."
342
+ raise click.ClickException(f"Git tag error: {detail}")
343
+
344
+ result.published_url = publish_github_release(
345
+ repo_slug=repo_slug,
346
+ tag_name=version,
347
+ name=f"Release {version}",
348
+ body=result.release_notes if notes_only else build_output_document(result),
349
+ token=token or "",
350
+ draft=draft,
351
+ prerelease=prerelease,
352
+ dry_run=dry_run,
353
+ )
354
+ click.echo(f"Release URL: {result.published_url}", err=True)
355
+
356
+ rendered = render_result(result, output_format, notes_only)
357
+ if output:
358
+ Path(output).write_text(rendered, encoding="utf-8")
359
+ click.echo(f"Saved output to {output}", err=True)
360
+ else:
361
+ click.echo(rendered)
362
+
363
+ if ci_output:
364
+ write_github_output(result)
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()
@@ -0,0 +1,105 @@
1
+ import os
2
+
3
+
4
+ def call_groq(prompt: str, api_key: str | None = None, model: str | None = None) -> str:
5
+ from groq import Groq
6
+
7
+ key = api_key or os.environ.get("GROQ_API_KEY")
8
+ if not key:
9
+ raise ValueError("GROQ_API_KEY not found in environment or passed as argument.")
10
+
11
+ client = Groq(api_key=key)
12
+ response = client.chat.completions.create(
13
+ model=model or "llama-3.3-70b-versatile",
14
+ messages=[{"role": "user", "content": prompt}],
15
+ max_tokens=1024,
16
+ )
17
+ return response.choices[0].message.content
18
+
19
+
20
+ def call_openai(prompt: str, api_key: str | None = None, model: str | None = None) -> str:
21
+ from openai import OpenAI
22
+
23
+ key = api_key or os.environ.get("OPENAI_API_KEY")
24
+ if not key:
25
+ raise ValueError("OPENAI_API_KEY not found in environment or passed as argument.")
26
+
27
+ client = OpenAI(api_key=key)
28
+ response = client.chat.completions.create(
29
+ model=model or "gpt-4o-mini",
30
+ messages=[{"role": "user", "content": prompt}],
31
+ max_tokens=1024,
32
+ )
33
+ return response.choices[0].message.content
34
+
35
+
36
+ def call_anthropic(prompt: str, api_key: str | None = None, model: str | None = None) -> str:
37
+ import anthropic
38
+
39
+ key = api_key or os.environ.get("ANTHROPIC_API_KEY")
40
+ if not key:
41
+ raise ValueError("ANTHROPIC_API_KEY not found in environment or passed as argument.")
42
+
43
+ client = anthropic.Anthropic(api_key=key)
44
+ response = client.messages.create(
45
+ model=model or "claude-3-5-haiku-20241022",
46
+ max_tokens=1024,
47
+ messages=[{"role": "user", "content": prompt}],
48
+ )
49
+ return response.content[0].text
50
+
51
+
52
+ def call_gemini(prompt: str, api_key: str | None = None, model: str | None = None) -> str:
53
+ import google.generativeai as genai
54
+
55
+ key = api_key or os.environ.get("GEMINI_API_KEY")
56
+ if not key:
57
+ raise ValueError("GEMINI_API_KEY not found in environment or passed as argument.")
58
+
59
+ genai.configure(api_key=key)
60
+ model_instance = genai.GenerativeModel(model or "gemini-1.5-flash")
61
+ response = model_instance.generate_content(prompt)
62
+ return response.text
63
+
64
+
65
+ def call_xai(prompt: str, api_key: str | None = None, model: str | None = None) -> str:
66
+ from openai import OpenAI
67
+
68
+ key = api_key or os.environ.get("XAI_API_KEY")
69
+ if not key:
70
+ raise ValueError("XAI_API_KEY not found in environment or passed as argument.")
71
+
72
+ client = OpenAI(api_key=key, base_url="https://api.x.ai/v1")
73
+ response = client.chat.completions.create(
74
+ model=model or "grok-3-mini",
75
+ messages=[{"role": "user", "content": prompt}],
76
+ max_tokens=1024,
77
+ )
78
+ return response.choices[0].message.content
79
+
80
+
81
+ PROVIDERS = {
82
+ "groq": call_groq,
83
+ "openai": call_openai,
84
+ "anthropic": call_anthropic,
85
+ "gemini": call_gemini,
86
+ "xai": call_xai,
87
+ }
88
+
89
+
90
+ def auto_detect_provider() -> str:
91
+ for name, key in [
92
+ ("groq", "GROQ_API_KEY"),
93
+ ("gemini", "GEMINI_API_KEY"),
94
+ ("openai", "OPENAI_API_KEY"),
95
+ ("anthropic", "ANTHROPIC_API_KEY"),
96
+ ("xai", "XAI_API_KEY"),
97
+ ]:
98
+ if os.environ.get(key):
99
+ return name
100
+
101
+ raise EnvironmentError(
102
+ "No API key found. Set one of: GROQ_API_KEY, GEMINI_API_KEY, "
103
+ "OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY in your .env, "
104
+ "or pass a custom key via --api-key."
105
+ )
@@ -0,0 +1,7 @@
1
+ click>=8.0
2
+ groq>=0.9
3
+ openai>=1.0
4
+ anthropic>=0.30
5
+ google-generativeai>=0.7
6
+ python-dotenv>=1.0
7
+ requests>=2.25
@@ -0,0 +1,16 @@
1
+ __RECOMMENDED_BUMP__: minor
2
+ # Release Assistant Brief
3
+ ## Release Summary
4
+ This release adds export support and improves release automation. It should be validated around generated release notes and publish flow.
5
+ ## Highlights
6
+ - Add export support.
7
+ - Improve GitHub release publishing.
8
+ - Improve release validation.
9
+ ## Risks & Checks
10
+ - Verify generated notes before publishing.
11
+ - Confirm the target git tag is correct.
12
+ ## Suggested Release Notes
13
+ ### Features
14
+ - Add export support.
15
+ ### Improvements
16
+ - Improve release publishing flow.
@@ -0,0 +1,58 @@
1
+ import json
2
+ import unittest
3
+
4
+ from releaseguru import (
5
+ __version__,
6
+ )
7
+ from releaseguru.cli import ReleaseResult, bump_version, extract_release_notes, parse_ai_response, render_result
8
+
9
+
10
+ class ReleaseGuruTests(unittest.TestCase):
11
+ def test_version_exists(self):
12
+ self.assertEqual(__version__, "0.1.0")
13
+
14
+ def test_bump_version(self):
15
+ self.assertEqual(bump_version("v1.2.3", "major"), "v2.0.0")
16
+ self.assertEqual(bump_version("v1.2.3", "minor"), "v1.3.0")
17
+ self.assertEqual(bump_version("v1.2.3", "patch"), "v1.2.4")
18
+
19
+ def test_parse_ai_response(self):
20
+ bump, body = parse_ai_response("__RECOMMENDED_BUMP__: minor\n# Release Assistant Brief")
21
+
22
+ self.assertEqual(bump, "minor")
23
+ self.assertEqual(body, "# Release Assistant Brief")
24
+
25
+ def test_extract_release_notes(self):
26
+ notes = extract_release_notes(
27
+ "# Release Assistant Brief\n"
28
+ "## Release Summary\n"
29
+ "Text\n"
30
+ "## Suggested Release Notes\n"
31
+ "### Features\n"
32
+ "- Add export"
33
+ )
34
+
35
+ self.assertEqual(notes, "### Features\n- Add export")
36
+
37
+ def test_render_json(self):
38
+ result = ReleaseResult(
39
+ version="v1.0.0",
40
+ bump_type="minor",
41
+ release_brief="# Brief",
42
+ release_notes="### Features\n- Add export",
43
+ commit_count=2,
44
+ from_ref="v0.9.0",
45
+ to_ref="HEAD",
46
+ provider="mock",
47
+ )
48
+
49
+ rendered = render_result(result, "json", notes_only=False)
50
+ payload = json.loads(rendered)
51
+
52
+ self.assertEqual(payload["version"], "v1.0.0")
53
+ self.assertEqual(payload["bump_type"], "minor")
54
+ self.assertEqual(payload["commit_count"], 2)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ unittest.main()