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.
- github_overlord-0.8.0/PKG-INFO +116 -0
- github_overlord-0.8.0/README.md +93 -0
- github_overlord-0.8.0/data/release_analysis_prompt.j2 +46 -0
- {github_overlord-0.7.1 → github_overlord-0.8.0}/github_overlord/__init__.py +79 -0
- github_overlord-0.8.0/github_overlord/config.py +17 -0
- github_overlord-0.8.0/github_overlord/release_checker.py +314 -0
- {github_overlord-0.7.1 → github_overlord-0.8.0}/github_overlord/stale_commenter.py +1 -1
- {github_overlord-0.7.1 → github_overlord-0.8.0}/pyproject.toml +12 -9
- github_overlord-0.7.1/PKG-INFO +0 -57
- github_overlord-0.7.1/README.md +0 -36
- {github_overlord-0.7.1 → github_overlord-0.8.0}/LICENSE.md +0 -0
- {github_overlord-0.7.1 → github_overlord-0.8.0}/github_overlord/patch.py +0 -0
- {github_overlord-0.7.1 → github_overlord-0.8.0}/github_overlord/utils.py +0 -0
|
@@ -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
|
+
[](https://github.com/iloveitaly/github-overlord/releases) [](https://pepy.tech/project/github-overlord) [](https://pypi.org/project/github-overlord)  [](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
|
+
[](https://github.com/iloveitaly/github-overlord/releases) [](https://pepy.tech/project/github-overlord) [](https://pypi.org/project/github-overlord)  [](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
|
|
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.
|
|
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.
|
|
13
|
+
pygithub = "^2.4.0"
|
|
13
14
|
click = "^8.1.7"
|
|
14
15
|
python-decouple-typed = "^3.11.0"
|
|
15
|
-
funcy-pipe = "
|
|
16
|
-
structlog = "
|
|
16
|
+
funcy-pipe = ">=0.11.1,<0.15.0"
|
|
17
|
+
structlog = ">=24.4,<26.0"
|
|
17
18
|
apscheduler = "^3.10.4"
|
|
18
|
-
openai = "
|
|
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 = "
|
|
31
|
-
black = "
|
|
32
|
-
pyright = "^1.1.
|
|
33
|
-
pylint = "
|
|
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"]
|
github_overlord-0.7.1/PKG-INFO
DELETED
|
@@ -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
|
-
[](https://github.com/iloveitaly/github-overlord/releases) [](https://pepy.tech/project/github-overlord) [](https://pypi.org/project/github-overlord)  [](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`.
|
github_overlord-0.7.1/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
[](https://github.com/iloveitaly/github-overlord/releases) [](https://pepy.tech/project/github-overlord) [](https://pypi.org/project/github-overlord)  [](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`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|