Compare commits

...
Author SHA1 Message Date
dependabot[bot] b03c13ae62 build(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 01:13:41 +00:00
sankalp e69de1d99a Merge pull request #63 from S4NKALP/dependabot/github_actions/main/actions/checkout-7
build(deps): bump actions/checkout from 6 to 7
2026-07-03 19:07:59 +05:45
dependabot[bot] e5f22bca85 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 12:45:02 +00:00
sankalp 277f197a71 fix(.github/workflows): change fail-on-scopes to runtime
Update CI to enforce runtime dependency reviews, improving coverage of actual runtime packages.
2026-07-03 18:26:35 +05:45
sankalp 2edbf95ed5 feat(provider): refactor Gemini provider to use struct schema
Improves validation and aligns with latest API changes.
2026-07-03 18:24:48 +05:45
sankalp 109fcd584f feat(root): bump version to 0.3.0 and update dependencies
Update metadata and pin production dependencies for reproducible builds.
2026-07-03 18:23:37 +05:45
sankalp 61e51b5187 chore(deps): bump dependency versions
Upgrade to latest stable releases for security and compatibility.
2026-07-03 18:21:18 +05:45
sankalp d4b694e77d fix(devgen): handle missing git binary gracefully
Ensure the function returns an empty list when git is not found, avoiding crashes.
2026-07-03 18:08:00 +05:45
sankalp 261d04c56f refactor(devgen/providers): centralize retry and quota handling in BaseProvider
Move retry logic and quota detection from provider-specific code to BaseProvider to reduce duplication and simplify error handling.
2026-07-03 18:07:43 +05:45
sankalp f7001d4243 feat(devgen/modules): streamline provider guard and enhance ManifestInspector flexibility
Add token‑limit error handling and switch git diff to null‑delimited output for reliable file detection.
2026-07-03 18:07:26 +05:45
sankalp 11a9db7986 feat(devgen): extend header regex to include deps and wip scopes and ignore markdown fences
Prevent false positives on markdown fences and enable parsing of dependency-related commit types.
2026-07-03 17:02:18 +05:45
sankalp 34f5d090ce chore(devgen): extend commit message regex to include perf and revert
Update the regex in devgen/utils.py to recognize `perf` and `revert` as valid Conventional Commit types, ensuring alignment with the specification and preventing false negatives in automated analysis.
2026-07-03 16:46:05 +05:45
sankalp d20a31bf38 feat(devgen/templates/commit): update commit_message.tpl with expanded conventional commit format
Add detailed format description, examples, and diff handling to improve commit message clarity.
2026-07-03 16:45:47 +05:45
sankalp a43e09e924 feat(.github/workflows): enable auto-merge for dependabot
Allow automatic merging of Dependabot PRs to keep the repository up‑to‑date without manual intervention.
2026-06-16 15:43:46 +05:45
17 changed files with 777 additions and 598 deletions

No files matched your search

+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
name: Lint & format name: Lint & format
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@v7
@@ -42,7 +42,7 @@ jobs:
matrix: matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@v7
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
language: [python] language: [python]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@v7
@@ -19,4 +19,5 @@ jobs:
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
target: minor target: minor
use-github-auto-merge: true
merge-method: squash merge-method: squash
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
name: Audit dependencies name: Audit dependencies
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@v5 uses: actions/dependency-review-action@v5
with: with:
fail-on-severity: high fail-on-severity: high
fail-on-scopes: direct fail-on-scopes: runtime
license-check: true license-check: true
# Project is GPL-3.0-or-later; flag licenses that would taint it. # Project is GPL-3.0-or-later; flag licenses that would taint it.
deny-licenses: AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0-only, AGPL-3.0-or-later, SSPL-1.0, BUSL-1.1 deny-licenses: AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0-only, AGPL-3.0-or-later, SSPL-1.0, BUSL-1.1
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
pre-commit: pre-commit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
permissions: permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: '3.10' python-version: '3.10'
cache: 'pip' cache: 'pip'
+15 -7
View File
@@ -33,6 +33,8 @@ from devgen.utils import (
extract_commit_messages, extract_commit_messages,
get_commit_dry_run_path, get_commit_dry_run_path,
is_file_recent, is_file_recent,
is_token_limit_error,
format_token_limit_error,
load_template, load_template,
render_template, render_template,
sanitize_ai_commit_message, sanitize_ai_commit_message,
@@ -241,9 +243,10 @@ class CommitEngine:
if not self._process_group(group, group_files, cache_map): if not self._process_group(group, group_files, cache_map):
failed.append(group) failed.append(group)
if len(groups) > 1 and i < len(groups) - 1: if len(groups) > 1 and i < len(groups) - 1:
import time if self.provider != "ollama":
import time
time.sleep(15) # Conservative 4 RPM guard for free-tier time.sleep(15) # Conservative 4 RPM guard for free-tier
return failed return failed
def _process_group( def _process_group(
@@ -265,7 +268,7 @@ class CommitEngine:
return self._apply_message(group, cache[group], files) return self._apply_message(group, cache[group], files)
try: try:
manifest_context = ManifestInspector.summary() manifest_context = ManifestInspector.summary(cwd=self.git.cwd)
if manifest_context: if manifest_context:
self.logger.info("Including manifest context in prompt") self.logger.info("Including manifest context in prompt")
with self.console.status( with self.console.status(
@@ -279,10 +282,15 @@ class CommitEngine:
) )
message = sanitize_ai_commit_message(raw) message = sanitize_ai_commit_message(raw)
except Exception as e: except Exception as e:
self.logger.error( if is_token_limit_error(e):
f"Failed to generate message for group {group!r}: {e}", msg = format_token_limit_error(self.provider, e, group=group)
exc_info=True, self.console.print(f"[bold red]{msg}[/bold red]")
) self.logger.error(msg)
else:
self.logger.error(
f"Failed to generate message for group {group!r}: {e}",
exc_info=True,
)
self.git.reset(files) self.git.reset(files)
return False return False
+11 -10
View File
@@ -143,13 +143,14 @@ class ManifestInspector:
""" """
@staticmethod @staticmethod
def summary() -> str: def summary(cwd: Optional[Path] = None) -> str:
cwd = cwd or get_git_root() or Path(".")
lines = [ lines = [
s s
for s in ( for s in (
ManifestInspector._pyproject(), ManifestInspector._pyproject(cwd),
ManifestInspector._package_json(), ManifestInspector._package_json(cwd),
ManifestInspector._requirements(), ManifestInspector._requirements(cwd),
) )
if s if s
] ]
@@ -164,8 +165,8 @@ class ManifestInspector:
return re.split(r"[><=!~\[]", spec, 1)[0].strip() return re.split(r"[><=!~\[]", spec, 1)[0].strip()
@staticmethod @staticmethod
def _pyproject() -> Optional[str]: def _pyproject(cwd: Path) -> Optional[str]:
path = Path("pyproject.toml") path = cwd / "pyproject.toml"
if not path.exists(): if not path.exists():
return None return None
try: try:
@@ -186,8 +187,8 @@ class ManifestInspector:
return f"[pyproject.toml] {' '.join(parts)}" if parts else None return f"[pyproject.toml] {' '.join(parts)}" if parts else None
@staticmethod @staticmethod
def _package_json() -> Optional[str]: def _package_json(cwd: Path) -> Optional[str]:
path = Path("package.json") path = cwd / "package.json"
if not path.exists(): if not path.exists():
return None return None
try: try:
@@ -203,8 +204,8 @@ class ManifestInspector:
return f"[package.json] {' '.join(parts)}" if parts else None return f"[package.json] {' '.join(parts)}" if parts else None
@staticmethod @staticmethod
def _requirements() -> Optional[str]: def _requirements(cwd: Path) -> Optional[str]:
path = Path("requirements.txt") path = cwd / "requirements.txt"
if not path.exists(): if not path.exists():
return None return None
try: try:
+4 -3
View File
@@ -91,15 +91,16 @@ class GitOperator:
[ [
"git", "git",
"ls-files", "ls-files",
"-z",
"--deleted", "--deleted",
"--modified", "--modified",
"--others", "--others",
"--exclude-standard", "--exclude-standard",
] ]
) )
files = {f.strip() for f in out.split("\n") if f.strip()} files = {f for f in out.split("\0") if f}
staged_out = self._run(["git", "diff", "--name-only", "--cached"]) staged_out = self._run(["git", "diff", "-z", "--name-only", "--cached"])
files.update(f.strip() for f in staged_out.split("\n") if f.strip()) files.update(f for f in staged_out.split("\0") if f)
return sorted(files) return sorted(files)
# ------------------------------------------------------------------ actions # ------------------------------------------------------------------ actions
+43 -20
View File
@@ -30,33 +30,56 @@ class BaseProvider(ABC):
model: str | None = None, model: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> str: ) -> str:
"""Public entry point. Validates input, dispatches, and wraps errors. """Public entry point. Validates input, dispatches, and wraps errors."""
import time
Args:
prompt: The prompt to send to the model.
api_key: Provider API key. May be ``None`` for local providers
(see :attr:`REQUIRES_API_KEY`).
model: Model id. Falls back to :attr:`DEFAULT_MODEL` when ``None``.
**kwargs: Forwarded to :meth:`_generate`.
Returns:
The generated text, stripped.
Raises:
ValueError: If the provider requires an API key and none was given.
RuntimeError: For any underlying failure, with a friendly message.
"""
if self.REQUIRES_API_KEY and not api_key: if self.REQUIRES_API_KEY and not api_key:
raise ValueError( raise ValueError(
f"{self.DISPLAY_NAME} API key is missing. " f"{self.DISPLAY_NAME} API key is missing. "
"Set it via `devgen setup config` or pass --api-key." "Set it via `devgen setup config` or pass --api-key."
) )
chosen_model = model or self.DEFAULT_MODEL chosen_model = model or self.DEFAULT_MODEL
try:
text = self._generate(prompt, api_key, chosen_model, **kwargs) max_retries = kwargs.get("max_retries", 5)
return (text or "").strip() base_delay = kwargs.get("retry_delay", 10)
except Exception as e:
self._handle_error(e) for attempt in range(max_retries + 1):
try:
text = self._generate(prompt, api_key, chosen_model, **kwargs)
return (text or "").strip()
except Exception as e:
if is_token_limit_error(e):
self._handle_error(e)
if self._is_quota_error(e) and attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"\n[{self.DISPLAY_NAME}] Quota/Rate limit hit (429). "
f"Waiting {delay}s and retrying... "
f"(Attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
if self._is_quota_error(e) and attempt >= max_retries:
raise RuntimeError(
f"{self.DISPLAY_NAME} API quota exhausted (429) after multiple retries."
) from e
self._handle_error(e)
return ""
@staticmethod
def _is_quota_error(error: Exception) -> bool:
text = str(error).upper()
return any(
marker in text
for marker in (
"429",
"RESOURCE_EXHAUSTED",
"QUOTA_EXCEEDED",
"THROTTLED",
"RATE_LIMIT_EXCEEDED",
"TOO_MANY_REQUESTS",
)
)
def _handle_error(self, error: Exception) -> None: def _handle_error(self, error: Exception) -> None:
"""Translate an internal exception into a user-facing RuntimeError. """Translate an internal exception into a user-facing RuntimeError.
+10 -48
View File
@@ -1,10 +1,7 @@
import time
from google import genai from google import genai
from google.genai import types from google.genai import types
from devgen.providers.base import BaseProvider from devgen.providers.base import BaseProvider
from devgen.utils import is_token_limit_error
class GeminiProvider(BaseProvider): class GeminiProvider(BaseProvider):
@@ -14,50 +11,15 @@ class GeminiProvider(BaseProvider):
DEFAULT_MODEL = "gemini-2.5-flash" DEFAULT_MODEL = "gemini-2.5-flash"
def _generate(self, prompt, api_key, model, **kwargs): def _generate(self, prompt, api_key, model, **kwargs):
max_retries = kwargs.get("max_retries", 5)
base_delay = kwargs.get("retry_delay", 10) # 10s base for 429 backoff
client = genai.Client(api_key=api_key) client = genai.Client(api_key=api_key)
response = client.models.generate_content(
for attempt in range(max_retries + 1): model=model,
try: contents=prompt,
response = client.models.generate_content( config=types.GenerateContentConfig(
model=model, temperature=kwargs.get("temperature", 0.7),
contents=prompt, top_p=kwargs.get("top_p", 0.95),
config=types.GenerateContentConfig( top_k=kwargs.get("top_k", 40),
temperature=kwargs.get("temperature", 0.7), max_output_tokens=kwargs.get("max_output_tokens", 2048),
top_p=kwargs.get("top_p", 0.95), ),
top_k=kwargs.get("top_k", 40),
max_output_tokens=kwargs.get("max_output_tokens", 2048),
),
)
return response.text or ""
except Exception as e:
if is_token_limit_error(e):
raise # base class handles with friendly message
if self._is_quota_error(e) and attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"\n[Gemini] Quota limit hit (429). This is often a temporary "
f"rate limit (RPM). Waiting {delay}s and retrying... "
f"(Attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
if self._is_quota_error(e):
raise RuntimeError(
"Gemini API quota exhausted (429) after multiple retries.\n\n"
"POSSIBLE CAUSES:\n"
"1. Rate Limit (RPM/RPD): You are sending requests too fast for the Free Tier.\n"
"2. Daily Limit: You have reached the maximum free requests allowed for today (typically 1,500).\n"
"3. Credit/Billing: If on a paid plan, your credit may be exhausted.\n\n"
"Please check your usage metrics at: https://aistudio.google.com/app/usage"
) from e
raise
@staticmethod
def _is_quota_error(error: Exception) -> bool:
text = str(error).upper()
return any(
marker in text
for marker in ("429", "RESOURCE_EXHAUSTED", "QUOTA_EXCEEDED", "THROTTLED")
) )
return response.text or ""
+21 -6
View File
@@ -1,14 +1,29 @@
You write Conventional Commits. Output ONLY the message — no preamble, no labels, no quotes, no markdown. You write Conventional Commits. Output ONLY the message — no preamble, no labels, no quotes, no markdown.
Format: Format:
<type>(<scope>): <summary> <type>[optional scope][optional !]: <description>
[optional 1-2 line body on WHY] [optional blank line]
[optional body]
[optional blank line]
[optional footer(s)]
types: feat fix refactor perf docs test build ci chore style types: feat fix refactor perf docs test build ci chore style
scope: {{ group_name }}{% if use_emoji %} (prefix summary with one matching emoji){% endif %} scope: {% if group_name and group_name != 'root' %}({{ group_name }}){% else %}optional{% endif %}{% if use_emoji %} (prefix description with one matching emoji){% endif %}
Example: Examples:
feat(auth): add login rate limit feat: add email notifications on new direct messages
Prevents brute-force attacks. feat(shopping cart): add the amazing button
feat!: remove ticket list endpoint
refers to JIRA-1337
BREAKING CHANGE: ticket endpoints no longer supports list all entities.
fix(shopping-cart): prevent order an empty shopping cart
fix(api): fix wrong calculation of request body checksum
fix: add missing parameter to service call
The error occurred due to <reasons>.
perf: decrease memory footprint for determine unique visitors by using HyperLogLog
build: update dependencies
build(release): bump version to 1.0.0
refactor: implement fibonacci number calculation as recursion
style: remove empty line
{{ diff_text }}{{ context }} {{ diff_text }}{{ context }}
+5 -2
View File
@@ -46,7 +46,7 @@ def sanitize_ai_commit_message(raw_text: str) -> str:
# - **fix: bug fix** # - **fix: bug fix**
# - chore(deps)!: breaking change # - chore(deps)!: breaking change
header_pattern = re.compile( header_pattern = re.compile(
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*", r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci|perf|revert|deps|wip)(\(.*\))?!?: .*",
re.IGNORECASE, re.IGNORECASE,
) )
@@ -75,6 +75,9 @@ def sanitize_ai_commit_message(raw_text: str) -> str:
# If we hit another header or a known separator, we stop # If we hit another header or a known separator, we stop
if "**Sponsor**" in line: if "**Sponsor**" in line:
break break
# Ignore trailing markdown code block markers
if line.strip() in ("```", "```md", "```markdown"):
continue
cleaned_lines.append(line) cleaned_lines.append(line)
if cleaned_lines: if cleaned_lines:
@@ -196,7 +199,7 @@ def get_git_staged_files() -> list[str]:
try: try:
output = run_git_command(["git", "diff", "--name-only", "--cached"]) output = run_git_command(["git", "diff", "--name-only", "--cached"])
return [f for f in output.splitlines() if f.strip()] return [f for f in output.splitlines() if f.strip()]
except subprocess.CalledProcessError: except (subprocess.CalledProcessError, FileNotFoundError):
return [] return []
+9 -9
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "devgen-cli" name = "devgen-cli"
version = "0.2.9" version = "0.3.0"
description = "A collection of developer tools" description = "A collection of developer tools"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -25,15 +25,15 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
] ]
dependencies = [ dependencies = [
"anthropic>=0.75.0", "anthropic>=0.116.0",
"google-genai>=0.1.0", "google-genai>=2.10.0",
"openai>=2.11.0", "openai>=2.44.0",
"pyyaml>=6.0.3", "pyyaml>=6.0.3",
"questionary>=2.1.1", "questionary>=2.1.1",
"requests>=2.32.5", "requests>=2.34.2",
"rich>=14.2.0", "rich>=15.0.0",
"toml>=0.10.2", "toml>=0.10.2",
"typer>=0.20.0", "typer>=0.26.8",
] ]
[project.urls] [project.urls]
Homepage = "https://github.com/S4NKALP/devgen" Homepage = "https://github.com/S4NKALP/devgen"
@@ -49,6 +49,6 @@ include = ["devgen*"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pre-commit>=4.5.0", "pre-commit>=4.6.0",
"ruff>=0.14.9", "ruff>=0.15.20",
] ]
+8 -8
View File
@@ -1,11 +1,11 @@
anthropic==0.76.0 anthropic==0.116.0
google-genai==1.60.0 google-genai==2.10.0
openai==2.15.0 openai==2.44.0
pre-commit==4.5.1 pre-commit==4.6.0
pyyaml==6.0.3 pyyaml==6.0.3
questionary==2.1.1 questionary==2.1.1
requests==2.32.5 requests==2.34.2
rich==14.2.0 rich==15.0.0
ruff==0.14.13 ruff==0.15.20
toml==0.10.2 toml==0.10.2
typer==0.21.1 typer==0.26.8
+1 -1
View File
@@ -6,7 +6,7 @@ from setuptools import find_packages, setup
setup( setup(
name="devgen-cli", name="devgen-cli",
version="0.2.9", version="0.3.0",
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
Generated
+641 -476
View File
File diff suppressed because it is too large. Load diff