github-overlord 0.7.1__tar.gz → 0.8.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,116 @@
1
+ Metadata-Version: 2.1
2
+ Name: github-overlord
3
+ Version: 0.8.0
4
+ Summary: Automatically merge Dependabot PRs in public repositories
5
+ Home-page: https://github.com/iloveitaly/github-overlord
6
+ Keywords: github,dependabot,automation
7
+ Author: Michael Bianco
8
+ Author-email: mike@mikebian.co
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: apscheduler (>=3.10.4,<4.0.0)
14
+ Requires-Dist: click (>=8.1.7,<9.0.0)
15
+ Requires-Dist: funcy-pipe (>=0.11.1,<0.15.0)
16
+ Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
17
+ Requires-Dist: openai (>=1.52.2,<3.0.0)
18
+ Requires-Dist: pydantic-ai-slim[google] (>=0.0.18,<0.0.19)
19
+ Requires-Dist: pygithub (>=2.4.0,<3.0.0)
20
+ Requires-Dist: python-decouple-typed (>=3.11.0,<4.0.0)
21
+ Requires-Dist: structlog (>=24.4,<26.0)
22
+ Description-Content-Type: text/markdown
23
+
24
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
25
+
26
+ # GitHub Overlord
27
+
28
+ GitHub Overlord is a Python script that does a couple things to help manage open source projects on GitHub:
29
+
30
+ * Automatically merges Dependabot PRs in public repositories that have passed CI checks.
31
+ * Comment on PRs that are going to automatically be marked as stale
32
+ * Removes notifications from dependabot and releases on your own projects
33
+ * Automatically creates releases for repositories based on LLM analysis of recent commits
34
+
35
+ This simple project has also given me the chance to iterate on my [nixpacks github actions project](https://github.com/iloveitaly/github-action-nixpacks).
36
+
37
+ ## Installation
38
+
39
+ ```shell
40
+ pip install github-overlord
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```shell
46
+ Usage: github-overlord [OPTIONS] COMMAND [ARGS]...
47
+
48
+ Options:
49
+ --help Show this message and exit.
50
+
51
+ Commands:
52
+ check-releases Check repositories for release readiness using LLM analysis
53
+ dependabot Automatically merge dependabot PRs in public repos that...
54
+ keep-alive-prs Detect when a bot is about to close a PR for no good reason
55
+ notifications Look at notifications and mark them as read
56
+ ```
57
+
58
+ ### Automatic Release Creation
59
+
60
+ The `check-releases` command uses LLM analysis (via [Pydantic AI](https://ai.pydantic.dev/) with Google Gemini) to determine when repositories are ready for a new release. Pydantic AI makes it easy to swap between different LLM providers if needed. This is particularly useful for:
61
+
62
+ - Template repositories or starter projects that don't have automated release workflows
63
+ - Projects where you want an AI to decide when enough changes have accumulated
64
+ - Maintaining regular release cadence across multiple repositories
65
+
66
+ **Usage:**
67
+
68
+ ```shell
69
+ # Check all repos with a specific topic
70
+ github-overlord check-releases --topic template
71
+
72
+ # Check a single repository
73
+ github-overlord check-releases --repo owner/repo-name
74
+
75
+ # Dry run (see what would happen without creating releases)
76
+ github-overlord check-releases --topic starter --dry-run
77
+ ```
78
+
79
+ **Requirements:**
80
+ - `GITHUB_TOKEN` - GitHub token with repo write permissions
81
+ - `GOOGLE_API_KEY` - Google API key ([Get a free API key](https://ai.google.dev/))
82
+ - `--topic` flag or `RELEASE_CHECKER_TOPIC` - Topic to filter repositories (required unless using `--repo`)
83
+
84
+ **How it works:**
85
+ 1. Finds repositories matching the specified topic
86
+ 2. For each repo, gets commits since the last release (or since repo creation if no releases)
87
+ 3. Analyzes up to the last 50 commits using Gemini 2.0 Flash to determine if a release is warranted
88
+ 4. If the LLM recommends a release, automatically creates one with:
89
+ - Auto-incremented semantic version (patch/minor/major based on changes)
90
+ - AI-generated release notes highlighting key changes
91
+ - Link to full changelog
92
+
93
+ **Scheduling:**
94
+ To run this weekly, set the `SCHEDULE` environment variable:
95
+ ```bash
96
+ # Run every Monday at 9 AM
97
+ export SCHEDULE="0 9 * * 1"
98
+ export RELEASE_CHECKER_TOPIC="template"
99
+ export GITHUB_TOKEN="your-token"
100
+ export GOOGLE_API_KEY="your-api-key"
101
+
102
+ # All CLI commands will run on this schedule
103
+ python main.py
104
+ ```
105
+
106
+ **Troubleshooting:**
107
+ - **"No repositories found with topic"**: Make sure your repos have the correct topic tag in GitHub settings
108
+ - **"GOOGLE_API_KEY environment variable is required"**: Get a free API key from [ai.google.dev](https://ai.google.dev/)
109
+ - **Rate limiting**: The free tier has limits (15-60 requests/minute). Consider adding delays between repos if needed
110
+ - **"Failed to create release"**: Ensure `GITHUB_TOKEN` has `repo` scope permissions
111
+
112
+ ### Docker Cron
113
+
114
+ There's a docker container you can use to run this on a cron. [Fits nicely into a orange pi.](https://mikebian.co/pi-hole-tailscale-and-docker-on-an-orange-pi/)
115
+
116
+ Check out [docker-compose.yml](./docker-compose.yml) for an example, or `git pull ghcr.io/iloveitaly/github-overlord:latest`.
@@ -0,0 +1,93 @@
1
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ # GitHub Overlord
4
+
5
+ GitHub Overlord is a Python script that does a couple things to help manage open source projects on GitHub:
6
+
7
+ * Automatically merges Dependabot PRs in public repositories that have passed CI checks.
8
+ * Comment on PRs that are going to automatically be marked as stale
9
+ * Removes notifications from dependabot and releases on your own projects
10
+ * Automatically creates releases for repositories based on LLM analysis of recent commits
11
+
12
+ This simple project has also given me the chance to iterate on my [nixpacks github actions project](https://github.com/iloveitaly/github-action-nixpacks).
13
+
14
+ ## Installation
15
+
16
+ ```shell
17
+ pip install github-overlord
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```shell
23
+ Usage: github-overlord [OPTIONS] COMMAND [ARGS]...
24
+
25
+ Options:
26
+ --help Show this message and exit.
27
+
28
+ Commands:
29
+ check-releases Check repositories for release readiness using LLM analysis
30
+ dependabot Automatically merge dependabot PRs in public repos that...
31
+ keep-alive-prs Detect when a bot is about to close a PR for no good reason
32
+ notifications Look at notifications and mark them as read
33
+ ```
34
+
35
+ ### Automatic Release Creation
36
+
37
+ The `check-releases` command uses LLM analysis (via [Pydantic AI](https://ai.pydantic.dev/) with Google Gemini) to determine when repositories are ready for a new release. Pydantic AI makes it easy to swap between different LLM providers if needed. This is particularly useful for:
38
+
39
+ - Template repositories or starter projects that don't have automated release workflows
40
+ - Projects where you want an AI to decide when enough changes have accumulated
41
+ - Maintaining regular release cadence across multiple repositories
42
+
43
+ **Usage:**
44
+
45
+ ```shell
46
+ # Check all repos with a specific topic
47
+ github-overlord check-releases --topic template
48
+
49
+ # Check a single repository
50
+ github-overlord check-releases --repo owner/repo-name
51
+
52
+ # Dry run (see what would happen without creating releases)
53
+ github-overlord check-releases --topic starter --dry-run
54
+ ```
55
+
56
+ **Requirements:**
57
+ - `GITHUB_TOKEN` - GitHub token with repo write permissions
58
+ - `GOOGLE_API_KEY` - Google API key ([Get a free API key](https://ai.google.dev/))
59
+ - `--topic` flag or `RELEASE_CHECKER_TOPIC` - Topic to filter repositories (required unless using `--repo`)
60
+
61
+ **How it works:**
62
+ 1. Finds repositories matching the specified topic
63
+ 2. For each repo, gets commits since the last release (or since repo creation if no releases)
64
+ 3. Analyzes up to the last 50 commits using Gemini 2.0 Flash to determine if a release is warranted
65
+ 4. If the LLM recommends a release, automatically creates one with:
66
+ - Auto-incremented semantic version (patch/minor/major based on changes)
67
+ - AI-generated release notes highlighting key changes
68
+ - Link to full changelog
69
+
70
+ **Scheduling:**
71
+ To run this weekly, set the `SCHEDULE` environment variable:
72
+ ```bash
73
+ # Run every Monday at 9 AM
74
+ export SCHEDULE="0 9 * * 1"
75
+ export RELEASE_CHECKER_TOPIC="template"
76
+ export GITHUB_TOKEN="your-token"
77
+ export GOOGLE_API_KEY="your-api-key"
78
+
79
+ # All CLI commands will run on this schedule
80
+ python main.py
81
+ ```
82
+
83
+ **Troubleshooting:**
84
+ - **"No repositories found with topic"**: Make sure your repos have the correct topic tag in GitHub settings
85
+ - **"GOOGLE_API_KEY environment variable is required"**: Get a free API key from [ai.google.dev](https://ai.google.dev/)
86
+ - **Rate limiting**: The free tier has limits (15-60 requests/minute). Consider adding delays between repos if needed
87
+ - **"Failed to create release"**: Ensure `GITHUB_TOKEN` has `repo` scope permissions
88
+
89
+ ### Docker Cron
90
+
91
+ There's a docker container you can use to run this on a cron. [Fits nicely into a orange pi.](https://mikebian.co/pi-hole-tailscale-and-docker-on-an-orange-pi/)
92
+
93
+ Check out [docker-compose.yml](./docker-compose.yml) for an example, or `git pull ghcr.io/iloveitaly/github-overlord:latest`.
@@ -0,0 +1,46 @@
1
+ You are analyzing commits for an open-source project to determine if a new release should be created.
2
+
3
+ Consider these factors:
4
+ - Number of commits (more commits = more likely to release, but even 1-2 significant commits may warrant a release)
5
+ - Type of changes (features, bug fixes, refactoring, docs, tests, dependencies)
6
+ - Impact of changes (major features vs minor tweaks)
7
+ - Time since last release (longer time = more likely, but not the only factor)
8
+ - For template/starter projects: any meaningful updates warrant a release
9
+
10
+ Guidelines for decision:
11
+ - "yes" = clear value in creating a release (new features, important fixes, meaningful updates)
12
+ - "no" = minimal changes (typos, minor docs, CI tweaks only)
13
+ - "maybe" = borderline case (some value but not urgent)
14
+ - For dependency updates: patch bump
15
+ - For bug fixes: patch bump
16
+ - For new features: minor bump
17
+ - For breaking changes: major bump
18
+
19
+ Guidelines for release_notes:
20
+ - Write a complete markdown changelog suitable for a GitHub release
21
+ - Use markdown headers, lists, and formatting as appropriate
22
+ - Focus on user-facing changes and impacts
23
+ - Organize by type of change (Features, Bug Fixes, Documentation, etc.) if appropriate
24
+ - Be concise but informative
25
+ - Example format:
26
+ ```markdown
27
+ ## What's Changed
28
+
29
+ ### Features
30
+ - Added support for Python 3.12
31
+ - New configuration option for custom timeouts
32
+
33
+ ### Bug Fixes
34
+ - Fixed authentication bug when using OAuth tokens
35
+ - Resolved memory leak in background worker
36
+
37
+ ### Performance
38
+ - Improved commit analysis by 40%
39
+ ```
40
+
41
+ Repository: {{ repo_name }}
42
+ {{ last_release_info }}
43
+ Number of commits: {{ commit_count }}
44
+
45
+ Commits:
46
+ {{ commit_summary }}
@@ -12,6 +12,7 @@ from github.PullRequest import PullRequest
12
12
 
13
13
  import github_overlord.patch as _
14
14
 
15
+ from .release_checker import check_repo_for_release
15
16
  from .stale_commenter import inspect_repo_for_stale_prs
16
17
  from .utils import log
17
18
 
@@ -319,9 +320,87 @@ def notifications(token, dry_run, only_unread):
319
320
  log.info("marked owned closed PRs as done", count=len(owned_closed_pull_requests))
320
321
 
321
322
 
323
+ @click.command()
324
+ @click.option("--dry-run", is_flag=True, help="Run script without creating releases")
325
+ @click.option(
326
+ "--topic",
327
+ help="Only process repos with this topic (can also be set via RELEASE_CHECKER_TOPIC)",
328
+ default=os.getenv("RELEASE_CHECKER_TOPIC"),
329
+ )
330
+ @click.option("--repo", help="Only process a single repository")
331
+ def check_releases(dry_run, topic, repo):
332
+ """
333
+ Check repositories for release readiness using LLM analysis and create releases when appropriate
334
+ """
335
+
336
+ token = os.getenv("GITHUB_TOKEN")
337
+ assert token, "GITHUB_TOKEN environment variable is required"
338
+ assert os.getenv("GOOGLE_API_KEY"), "GOOGLE_API_KEY environment variable is required"
339
+
340
+ log.info("checking repositories for release readiness")
341
+
342
+ g = Github(token)
343
+ user = g.get_user()
344
+
345
+ repo = extract_repo_reference_from_github_url(repo)
346
+
347
+ if repo:
348
+ result = check_repo_for_release(g.get_repo(repo), dry_run)
349
+ if result["created"]:
350
+ log.info("Release check complete - created 1 release")
351
+ elif result["failed"]:
352
+ log.info("Release check complete - failed to create release")
353
+ else:
354
+ log.info("Release check complete - no release needed")
355
+ return
356
+
357
+ # Topic is required when not specifying a single repo
358
+ assert topic, "Topic is required when not specifying a single repository (use --topic or set RELEASE_CHECKER_TOPIC)"
359
+
360
+ log.info("filtering by topic", topic=topic)
361
+
362
+ # Get all public repos owned by user with the specified topic
363
+ repos = user.get_repos(type="public") | fp.filter(
364
+ lambda r: r.owner.login == user.login and not r.fork and topic in r.get_topics()
365
+ )
366
+
367
+ # Process each repo and collect results
368
+ results = repos | fp.map(fp.partial(check_repo_for_release, dry_run=dry_run)) | fp.to_list()
369
+
370
+ # Check if any repos were found
371
+ if not results:
372
+ log.warning("no repositories found with topic", topic=topic)
373
+ return
374
+
375
+ # Calculate statistics
376
+ total_checked = sum(1 for r in results if r["checked"])
377
+ total_skipped = sum(1 for r in results if r["skipped"])
378
+ total_created = sum(1 for r in results if r["created"])
379
+ total_failed = sum(1 for r in results if r["failed"])
380
+
381
+ # Log summary
382
+ if dry_run:
383
+ log.info(
384
+ "DRY RUN: Release check complete",
385
+ checked=total_checked,
386
+ would_create=total_created,
387
+ skipped=total_skipped,
388
+ errors=total_failed
389
+ )
390
+ else:
391
+ log.info(
392
+ "Release check complete",
393
+ checked=total_checked,
394
+ created=total_created,
395
+ skipped=total_skipped,
396
+ failed=total_failed
397
+ )
398
+
399
+
322
400
  cli.add_command(dependabot)
323
401
  cli.add_command(keep_alive_prs)
324
402
  cli.add_command(notifications)
403
+ cli.add_command(check_releases)
325
404
 
326
405
  if __name__ == "__main__":
327
406
  cli()
@@ -0,0 +1,17 @@
1
+ """Configuration for github-overlord."""
2
+ from pathlib import Path
3
+
4
+ import jinja2
5
+
6
+ # Set up paths
7
+ ROOT_DIRECTORY = Path(__file__).parent.parent.resolve()
8
+ DATA_DIRECTORY = ROOT_DIRECTORY / "data"
9
+ RELEASE_ANALYSIS_PROMPT_TEMPLATE = DATA_DIRECTORY / "release_analysis_prompt.j2"
10
+
11
+ # Jinja2 environment for templates
12
+ JINJA_ENV = jinja2.Environment(
13
+ loader=jinja2.FileSystemLoader(searchpath=str(DATA_DIRECTORY)),
14
+ autoescape=False, # No HTML escaping needed for prompts
15
+ trim_blocks=True,
16
+ lstrip_blocks=True
17
+ )
@@ -0,0 +1,314 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+
4
+ import funcy_pipe as fp
5
+ from github import GithubException
6
+ from github.Repository import Repository
7
+ from pydantic import BaseModel, Field
8
+ from pydantic_ai import Agent
9
+
10
+ from github_overlord.config import JINJA_ENV
11
+ from github_overlord.utils import log
12
+
13
+
14
+ class ReleaseAnalysis(BaseModel):
15
+ """Structured output from LLM analysis of commits."""
16
+
17
+ should_release: str = Field(description="yes, no, or maybe")
18
+ confidence: int = Field(ge=0, le=100, description="Confidence level 0-100")
19
+ reasoning: str = Field(description="Brief explanation in 1-2 sentences")
20
+ suggested_version_bump: str = Field(description="major, minor, or patch")
21
+ release_notes: str = Field(description="Full markdown changelog for the release")
22
+
23
+
24
+ class ReleaseDecision(BaseModel):
25
+ """Decision about whether to create a release."""
26
+
27
+ should_create: bool
28
+ suggested_version: str
29
+ release_notes: str
30
+
31
+
32
+ def should_create_release(repo: Repository) -> ReleaseDecision:
33
+ """
34
+ Analyze commits since last release and determine if a new release should be created.
35
+
36
+ Returns:
37
+ ReleaseDecision with should_create, suggested_version, and release_notes
38
+ """
39
+
40
+ # Get the last release
41
+ releases = list(repo.get_releases())
42
+
43
+ if releases:
44
+ last_release = releases[0]
45
+ baseline_date = last_release.created_at
46
+ baseline_tag = last_release.tag_name
47
+ log.debug("found last release", tag=baseline_tag, date=baseline_date)
48
+ else:
49
+ # No releases yet, use repo creation date
50
+ baseline_date = repo.created_at
51
+ baseline_tag = None
52
+ log.debug("no releases found, using repo creation date", date=baseline_date)
53
+
54
+ # Get commits since baseline (limit to 50)
55
+ try:
56
+ all_commits = list(repo.get_commits(since=baseline_date, sha=repo.default_branch))
57
+ except GithubException as e:
58
+ log.error("failed to get commits", error=str(e), code=e.status if hasattr(e, 'status') else None)
59
+ return ReleaseDecision(should_create=False, suggested_version="", release_notes="")
60
+
61
+ # Limit to last 50 commits
62
+ commits = all_commits[:50]
63
+
64
+ if not commits:
65
+ log.info("no commits since last release", last_release=baseline_tag or "none")
66
+ return ReleaseDecision(should_create=False, suggested_version="", release_notes="")
67
+
68
+ log.info("analyzing commits", count=len(commits), total_available=len(all_commits))
69
+
70
+ # Format commits for LLM
71
+ commit_summary = format_commits_for_llm(commits)
72
+
73
+ # Calculate days since last release
74
+ days_since_release = (datetime.now(timezone.utc) - baseline_date).days
75
+
76
+ # Call LLM to analyze
77
+ analysis = analyze_commits_with_llm(
78
+ repo=repo,
79
+ commit_summary=commit_summary,
80
+ commit_count=len(commits),
81
+ days_since_release=days_since_release,
82
+ last_tag=baseline_tag
83
+ )
84
+
85
+ if not analysis:
86
+ log.error("LLM analysis failed")
87
+ return ReleaseDecision(should_create=False, suggested_version="", release_notes="")
88
+
89
+ should_release = analysis.get("should_release", "no") in ["yes", "maybe"]
90
+
91
+ if should_release:
92
+ suggested_version = calculate_next_version(baseline_tag, analysis.get("suggested_version_bump", "patch"))
93
+ release_notes = generate_release_notes(repo, baseline_tag, suggested_version, analysis)
94
+
95
+ log.info(
96
+ "LLM recommends release",
97
+ decision=analysis.get("should_release"),
98
+ confidence=analysis.get("confidence", 0),
99
+ version=suggested_version,
100
+ bump=analysis.get("suggested_version_bump", "patch"),
101
+ reasoning=analysis.get("reasoning", "")
102
+ )
103
+
104
+ return ReleaseDecision(
105
+ should_create=True,
106
+ suggested_version=suggested_version,
107
+ release_notes=release_notes
108
+ )
109
+
110
+ log.info(
111
+ "LLM does not recommend release",
112
+ decision=analysis.get("should_release", "no"),
113
+ confidence=analysis.get("confidence", 0),
114
+ reasoning=analysis.get("reasoning", "")
115
+ )
116
+ return ReleaseDecision(should_create=False, suggested_version="", release_notes="")
117
+
118
+
119
+ def format_commits_for_llm(commits) -> str:
120
+ """Format commits into a readable summary for LLM analysis."""
121
+
122
+ commit_lines = []
123
+
124
+ for commit in commits[:50]: # Limit to avoid token limits
125
+ # Get first line of commit message
126
+ message_lines = commit.commit.message.strip().split("\n")
127
+ first_line = message_lines[0][:100] # Limit length
128
+
129
+ author = commit.commit.author.name
130
+ date = commit.commit.author.date.strftime("%Y-%m-%d")
131
+
132
+ commit_lines.append(f"- [{date}] {first_line} (@{author})")
133
+
134
+ return "\n".join(commit_lines)
135
+
136
+
137
+ def analyze_commits_with_llm(repo: Repository, commit_summary: str, commit_count: int, days_since_release: int, last_tag: str | None) -> dict:
138
+ """Use Gemini via Pydantic AI to analyze commits and determine if a release should be created."""
139
+
140
+ template = JINJA_ENV.get_template("release_analysis_prompt.j2")
141
+
142
+ last_release_info = f"Last release: {last_tag} ({days_since_release} days ago)" if last_tag else f"No previous releases (repo is {days_since_release} days old)"
143
+
144
+ prompt = template.render(
145
+ repo_name=repo.full_name,
146
+ last_release_info=last_release_info,
147
+ commit_count=commit_count,
148
+ commit_summary=commit_summary
149
+ )
150
+
151
+ try:
152
+ # Create agent with structured output
153
+ # Using gemini-flash which points to latest flash model
154
+ agent = Agent(
155
+ 'google-gla:gemini-flash',
156
+ result_type=ReleaseAnalysis,
157
+ )
158
+
159
+ result = agent.run_sync(prompt)
160
+
161
+ # Convert Pydantic model to dict for compatibility
162
+ return result.data.model_dump()
163
+
164
+ except Exception as e:
165
+ log.error("LLM API call failed", error=str(e))
166
+ return {}
167
+
168
+
169
+ def calculate_next_version(current_tag: str | None, bump_type: str) -> str:
170
+ """Calculate the next semantic version based on the current tag and bump type."""
171
+
172
+ if not current_tag:
173
+ # No previous releases, start with v1.0.0
174
+ return "v1.0.0"
175
+
176
+ # Remove 'v' prefix if present
177
+ version_str = current_tag.lstrip("v")
178
+
179
+ try:
180
+ parts = version_str.split(".")
181
+ major = int(parts[0]) if len(parts) > 0 else 0
182
+ minor = int(parts[1]) if len(parts) > 1 else 0
183
+ patch = int(parts[2]) if len(parts) > 2 else 0
184
+ except (ValueError, IndexError):
185
+ # Can't parse version, default to v1.0.0
186
+ log.warning("could not parse version", current_tag=current_tag)
187
+ return "v1.0.0"
188
+
189
+ if bump_type == "major":
190
+ major += 1
191
+ minor = 0
192
+ patch = 0
193
+ elif bump_type == "minor":
194
+ minor += 1
195
+ patch = 0
196
+ else: # patch
197
+ patch += 1
198
+
199
+ return f"v{major}.{minor}.{patch}"
200
+
201
+
202
+ def generate_release_notes(repo: Repository, baseline_tag: str | None, new_tag: str, analysis: dict) -> str:
203
+ """Generate release notes from LLM analysis and add changelog link."""
204
+
205
+ # Get the markdown changelog from LLM
206
+ llm_changelog = analysis.get("release_notes", "").strip()
207
+
208
+ # Build the full release notes
209
+ notes_parts = []
210
+
211
+ if llm_changelog:
212
+ notes_parts.append(llm_changelog)
213
+ notes_parts.append("") # Empty line before separator
214
+
215
+ # Add horizontal line and Full Changelog link
216
+ notes_parts.append("---")
217
+ notes_parts.append("")
218
+
219
+ if baseline_tag:
220
+ # Compare from last tag to new tag
221
+ changelog_url = f"https://github.com/{repo.full_name}/compare/{baseline_tag}...{new_tag}"
222
+ else:
223
+ # No previous release, link to all commits up to this tag
224
+ changelog_url = f"https://github.com/{repo.full_name}/commits/{new_tag}"
225
+
226
+ notes_parts.append(f"**Full Changelog**: {changelog_url}")
227
+
228
+ return "\n".join(notes_parts)
229
+
230
+
231
+ def create_release(repo: Repository, tag: str, notes: str, dry_run: bool) -> bool:
232
+ """Create a new GitHub release."""
233
+
234
+ if dry_run:
235
+ log.info(
236
+ "DRY RUN: would create release",
237
+ repo=repo.full_name,
238
+ tag=tag,
239
+ notes_preview=notes[:200] + "..." if len(notes) > 200 else notes
240
+ )
241
+ return True
242
+
243
+ try:
244
+ repo.create_git_release(
245
+ tag=tag,
246
+ name=tag,
247
+ message=notes,
248
+ draft=False,
249
+ prerelease=False,
250
+ target_commitish=repo.default_branch
251
+ )
252
+
253
+ log.info("created release", repo=repo.full_name, tag=tag)
254
+ return True
255
+
256
+ except GithubException as e:
257
+ log.error("failed to create release", repo=repo.full_name, error=str(e))
258
+ return False
259
+
260
+
261
+ def check_repo_for_release(repo: Repository, dry_run: bool) -> dict:
262
+ """
263
+ Check a single repository and create a release if recommended.
264
+
265
+ Returns:
266
+ dict with keys: checked, skipped, created, failed
267
+ """
268
+
269
+ result = {
270
+ "checked": False,
271
+ "skipped": False,
272
+ "created": False,
273
+ "failed": False
274
+ }
275
+
276
+ with log.context(repo=repo.full_name):
277
+ log.debug("checking repository for release")
278
+
279
+ # Skip archived repos
280
+ if repo.archived:
281
+ log.debug("skipping archived repo")
282
+ result["skipped"] = True
283
+ return result
284
+
285
+ # Skip empty repos
286
+ try:
287
+ if repo.size == 0:
288
+ log.debug("skipping empty repo")
289
+ result["skipped"] = True
290
+ return result
291
+ except Exception:
292
+ pass # If we can't determine size, continue anyway
293
+
294
+ result["checked"] = True
295
+
296
+ try:
297
+ decision = should_create_release(repo)
298
+
299
+ if decision.should_create:
300
+ success = create_release(repo, decision.suggested_version, decision.release_notes, dry_run)
301
+ if success:
302
+ result["created"] = True
303
+ else:
304
+ result["failed"] = True
305
+ else:
306
+ log.debug("no release needed")
307
+ except GithubException as e:
308
+ log.error("GitHub API error", error=str(e), status=e.status if hasattr(e, 'status') else None)
309
+ result["failed"] = True
310
+ except Exception as e:
311
+ log.error("unexpected error checking repository", error=str(e), error_type=type(e).__name__)
312
+ result["failed"] = True
313
+
314
+ return result
@@ -88,7 +88,7 @@ If the comment does not indicate that the pull request will be closed, respond w
88
88
 
89
89
  Do not:
90
90
 
91
- * Ask for an update. This sound demanding.
91
+ * Ask for an update. This sounds demanding.
92
92
  * Mention that the pull request will be closed.
93
93
  """
94
94
  comment_markdown = """
@@ -1,21 +1,24 @@
1
1
  [tool.poetry]
2
2
  name = "github-overlord"
3
- version = "0.7.1"
3
+ version = "0.8.0"
4
4
  description = "Automatically merge Dependabot PRs in public repositories"
5
5
  authors = ["Michael Bianco <mike@mikebian.co>"]
6
6
  readme = "README.md"
7
7
  keywords = ["github", "dependabot", "automation"]
8
8
  homepage = "https://github.com/iloveitaly/github-overlord"
9
+ include = ["data/*.j2"]
9
10
 
10
11
  [tool.poetry.dependencies]
11
12
  python = "^3.11"
12
- pygithub = "^2.3.0"
13
+ pygithub = "^2.4.0"
13
14
  click = "^8.1.7"
14
15
  python-decouple-typed = "^3.11.0"
15
- funcy-pipe = "^0.11.1"
16
- structlog = "^24.4.0"
16
+ funcy-pipe = ">=0.11.1,<0.15.0"
17
+ structlog = ">=24.4,<26.0"
17
18
  apscheduler = "^3.10.4"
18
- openai = "^1.41.1"
19
+ openai = ">=1.52.2,<3.0.0"
20
+ pydantic-ai-slim = {extras = ["google"], version = "^0.0.18"}
21
+ jinja2 = "^3.1.0"
19
22
 
20
23
  [tool.poetry.scripts]
21
24
  github-overlord = "github_overlord:cli"
@@ -27,10 +30,10 @@ profile = "black"
27
30
  float_to_top = true
28
31
 
29
32
  [tool.poetry.group.dev.dependencies]
30
- isort = "^5.13.2"
31
- black = "^24.8.0"
32
- pyright = "^1.1.376"
33
- pylint = "^3.2.6"
33
+ isort = ">=5.13.2,<8.0.0"
34
+ black = ">=24.10,<26.0"
35
+ pyright = "^1.1.386"
36
+ pylint = ">=3.3.1,<5.0.0"
34
37
 
35
38
  [build-system]
36
39
  requires = ["poetry-core"]
@@ -1,57 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: github-overlord
3
- Version: 0.7.1
4
- Summary: Automatically merge Dependabot PRs in public repositories
5
- Home-page: https://github.com/iloveitaly/github-overlord
6
- Keywords: github,dependabot,automation
7
- Author: Michael Bianco
8
- Author-email: mike@mikebian.co
9
- Requires-Python: >=3.11,<4.0
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: apscheduler (>=3.10.4,<4.0.0)
14
- Requires-Dist: click (>=8.1.7,<9.0.0)
15
- Requires-Dist: funcy-pipe (>=0.11.1,<0.12.0)
16
- Requires-Dist: openai (>=1.41.1,<2.0.0)
17
- Requires-Dist: pygithub (>=2.3.0,<3.0.0)
18
- Requires-Dist: python-decouple-typed (>=3.11.0,<4.0.0)
19
- Requires-Dist: structlog (>=24.4.0,<25.0.0)
20
- Description-Content-Type: text/markdown
21
-
22
- [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
23
-
24
- # GitHub Overlord
25
-
26
- GitHub Overlord is a Python script that does a couple things to help manage open source projects on GitHub:
27
-
28
- * Automatically merges Dependabot PRs in public repositories that have passed CI checks.
29
- * Comment on PRs that are going to automatically be marked as stale
30
- * Removes notifications from dependabot and releases on your own projects
31
-
32
- This simple project has also given me the chance to iterate on my [nixpacks github actions project](https://github.com/iloveitaly/github-action-nixpacks).
33
-
34
- ## Installation
35
-
36
- ```shell
37
- pip install github-overlord
38
- ```
39
-
40
- ## Usage
41
-
42
- ```shell
43
- Usage: github-overlord [OPTIONS] COMMAND [ARGS]...
44
-
45
- Options:
46
- --help Show this message and exit.
47
-
48
- Commands:
49
- dependabot Automatically merge dependabot PRs in public repos that...
50
- keep-alive-prs
51
- ```
52
-
53
- ### Docker Cron
54
-
55
- There's a docker container you can use to run this on a cron. [Fits nicely into a orange pi.](https://mikebian.co/pi-hole-tailscale-and-docker-on-an-orange-pi/)
56
-
57
- Check out [docker-compose.yml](./docker-compose.yml) for an example, or `git pull ghcr.io/iloveitaly/github-overlord:latest`.
@@ -1,36 +0,0 @@
1
- [![Release Notes](https://img.shields.io/github/release/iloveitaly/github-overlord)](https://github.com/iloveitaly/github-overlord/releases) [![Downloads](https://static.pepy.tech/badge/github-overlord/month)](https://pepy.tech/project/github-overlord) [![Python Versions](https://img.shields.io/pypi/pyversions/github-overlord)](https://pypi.org/project/github-overlord) ![GitHub CI Status](https://github.com/iloveitaly/github-overlord/actions/workflows/build_and_publish.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
-
3
- # GitHub Overlord
4
-
5
- GitHub Overlord is a Python script that does a couple things to help manage open source projects on GitHub:
6
-
7
- * Automatically merges Dependabot PRs in public repositories that have passed CI checks.
8
- * Comment on PRs that are going to automatically be marked as stale
9
- * Removes notifications from dependabot and releases on your own projects
10
-
11
- This simple project has also given me the chance to iterate on my [nixpacks github actions project](https://github.com/iloveitaly/github-action-nixpacks).
12
-
13
- ## Installation
14
-
15
- ```shell
16
- pip install github-overlord
17
- ```
18
-
19
- ## Usage
20
-
21
- ```shell
22
- Usage: github-overlord [OPTIONS] COMMAND [ARGS]...
23
-
24
- Options:
25
- --help Show this message and exit.
26
-
27
- Commands:
28
- dependabot Automatically merge dependabot PRs in public repos that...
29
- keep-alive-prs
30
- ```
31
-
32
- ### Docker Cron
33
-
34
- There's a docker container you can use to run this on a cron. [Fits nicely into a orange pi.](https://mikebian.co/pi-hole-tailscale-and-docker-on-an-orange-pi/)
35
-
36
- Check out [docker-compose.yml](./docker-compose.yml) for an example, or `git pull ghcr.io/iloveitaly/github-overlord:latest`.