releaseguru 0.1.0__py3-none-any.whl

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,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()
releaseguru/cli.py ADDED
@@ -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,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,9 @@
1
+ releaseguru/__init__.py,sha256=OWzbI7MeIDzzKb1GWwLDc-pyzF0EcwBG-OGMDxR6pNc,50
2
+ releaseguru/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
3
+ releaseguru/cli.py,sha256=medj_hSGY9PVG7lI4xVn2ZxyqtK7O5P5Go8qO-kNOx0,12274
4
+ releaseguru/providers.py,sha256=BvN8zw_UDz1RRE59lCpHj03wG4rwpHFqSc6yOej0tAA,3359
5
+ releaseguru-0.1.0.dist-info/METADATA,sha256=erzTcWo20wfKViScBNWbOyVW3qLfjJSXLGvfnM3hGkM,6574
6
+ releaseguru-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ releaseguru-0.1.0.dist-info/entry_points.txt,sha256=bLEuC472qvtq-D8Mx24IMgBZiWEuOh9sGJ6e_i0uHZ4,53
8
+ releaseguru-0.1.0.dist-info/licenses/LICENSE,sha256=E2qtczSgPoHzOU_YqSayrfszXHR0v5B8HrNYJ_-pBBc,1081
9
+ releaseguru-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ releaseguru = releaseguru.cli:main
@@ -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.