github-overlord 0.9.2__tar.gz → 0.10.1__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.9.2 → github_overlord-0.10.1}/PKG-INFO +6 -4
- {github_overlord-0.9.2 → github_overlord-0.10.1}/README.md +5 -3
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/__init__.py +94 -60
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/dependabot.py +17 -9
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/release_checker.py +162 -17
- {github_overlord-0.9.2 → github_overlord-0.10.1}/pyproject.toml +1 -1
- {github_overlord-0.9.2 → github_overlord-0.10.1}/pyproject.toml.orig +1 -1
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/ai.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/config.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/data/release_analysis_prompt.j2 +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/notifications.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/patch.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/stale_commenter.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/utils.py +0 -0
- {github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: github-overlord
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Automatically merge Dependabot PRs in public repositories
|
|
5
5
|
Keywords: github,dependabot,automation
|
|
6
6
|
Author: Michael Bianco
|
|
@@ -93,13 +93,15 @@ github-overlord generate-releases --topic auto-release --dry-run
|
|
|
93
93
|
* `--topic` flag or `RELEASE_CHECKER_TOPIC` - Topic to filter repositories (required unless using `--repo`)
|
|
94
94
|
* `--repo` flag or `RELEASE_CHECKER_REPO` - Single repository to process (useful for testing)
|
|
95
95
|
* `RELEASE_CHECKER_MIN_GAP` - Minimum time between releases for the same repo (defaults to `2w`, e.g. `14d`, `48h`)
|
|
96
|
+
* `RELEASE_CHECKER_GLOBAL_MIN_GAP` - Minimum time between releases created by this tool across the repositories in that run (defaults to `1w`; set to `0` to disable). The clock is the newest GitHub release, among those repos, whose notes include this tool's `Generated-by` line, using that release's publish time. Manual releases do not reset it. While this gap is active, a run creates at most one release even if `--max-releases` is higher.
|
|
96
97
|
|
|
97
98
|
**How it works:**
|
|
98
99
|
|
|
99
100
|
1. Finds repositories matching the specified topic
|
|
100
|
-
2.
|
|
101
|
-
3.
|
|
102
|
-
4.
|
|
101
|
+
2. Finds the newest release published by this tool across those repositories. If that publish time is newer than `RELEASE_CHECKER_GLOBAL_MIN_GAP`, the run stops
|
|
102
|
+
3. For each remaining repo, skips it when its latest release is newer than `RELEASE_CHECKER_MIN_GAP`, then reads commits since that release (or since the repo was created)
|
|
103
|
+
4. Analyzes up to the last 50 commits using the configured AI model (defaulting to `gemini-3.8-flash`) to determine if a release is warranted
|
|
104
|
+
5. If the LLM recommends a release, automatically creates one with:
|
|
103
105
|
|
|
104
106
|
* Auto-incremented semantic version (patch/minor/major based on changes)
|
|
105
107
|
* AI-generated release notes highlighting key changes
|
|
@@ -65,13 +65,15 @@ github-overlord generate-releases --topic auto-release --dry-run
|
|
|
65
65
|
* `--topic` flag or `RELEASE_CHECKER_TOPIC` - Topic to filter repositories (required unless using `--repo`)
|
|
66
66
|
* `--repo` flag or `RELEASE_CHECKER_REPO` - Single repository to process (useful for testing)
|
|
67
67
|
* `RELEASE_CHECKER_MIN_GAP` - Minimum time between releases for the same repo (defaults to `2w`, e.g. `14d`, `48h`)
|
|
68
|
+
* `RELEASE_CHECKER_GLOBAL_MIN_GAP` - Minimum time between releases created by this tool across the repositories in that run (defaults to `1w`; set to `0` to disable). The clock is the newest GitHub release, among those repos, whose notes include this tool's `Generated-by` line, using that release's publish time. Manual releases do not reset it. While this gap is active, a run creates at most one release even if `--max-releases` is higher.
|
|
68
69
|
|
|
69
70
|
**How it works:**
|
|
70
71
|
|
|
71
72
|
1. Finds repositories matching the specified topic
|
|
72
|
-
2.
|
|
73
|
-
3.
|
|
74
|
-
4.
|
|
73
|
+
2. Finds the newest release published by this tool across those repositories. If that publish time is newer than `RELEASE_CHECKER_GLOBAL_MIN_GAP`, the run stops
|
|
74
|
+
3. For each remaining repo, skips it when its latest release is newer than `RELEASE_CHECKER_MIN_GAP`, then reads commits since that release (or since the repo was created)
|
|
75
|
+
4. Analyzes up to the last 50 commits using the configured AI model (defaulting to `gemini-3.8-flash`) to determine if a release is warranted
|
|
76
|
+
5. If the LLM recommends a release, automatically creates one with:
|
|
75
77
|
|
|
76
78
|
* Auto-incremented semantic version (patch/minor/major based on changes)
|
|
77
79
|
* AI-generated release notes highlighting key changes
|
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
|
-
from github import Github
|
|
4
|
+
from github import Github, GithubException
|
|
5
5
|
|
|
6
6
|
import github_overlord.patch # noqa: F401
|
|
7
7
|
|
|
8
8
|
from .ai import get_expected_ai_key_var, is_ai_key_configured
|
|
9
9
|
from .dependabot import dependabot
|
|
10
10
|
from .notifications import notifications
|
|
11
|
-
from .release_checker import
|
|
11
|
+
from .release_checker import (
|
|
12
|
+
check_repo_for_release,
|
|
13
|
+
get_global_min_release_gap,
|
|
14
|
+
release_creation_blocked_by_global_gap,
|
|
15
|
+
should_stop_for_global_gap,
|
|
16
|
+
)
|
|
12
17
|
from .stale_commenter import inspect_stale_prs
|
|
13
18
|
from .utils import extract_repo_reference_from_github_url, log
|
|
14
19
|
from .version import __version__
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
def _log_release_run_summary(
|
|
23
|
+
dry_run: bool, *, checked: int, created: int, skipped: int, failed: int
|
|
24
|
+
) -> None:
|
|
25
|
+
if dry_run:
|
|
26
|
+
log.info(
|
|
27
|
+
"release check complete",
|
|
28
|
+
dry_run=True,
|
|
29
|
+
checked=checked,
|
|
30
|
+
would_create=created,
|
|
31
|
+
skipped=skipped,
|
|
32
|
+
errors=failed,
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
log.info(
|
|
37
|
+
"release check complete",
|
|
38
|
+
checked=checked,
|
|
39
|
+
created=created,
|
|
40
|
+
skipped=skipped,
|
|
41
|
+
failed=failed,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
17
45
|
def _ensure_ai_key_configured(command_action: str) -> None:
|
|
18
46
|
if not is_ai_key_configured():
|
|
19
47
|
expected_var = get_expected_ai_key_var()
|
|
@@ -120,9 +148,48 @@ def generate_releases(dry_run, topic, repo, max_releases):
|
|
|
120
148
|
|
|
121
149
|
repo = extract_repo_reference_from_github_url(repo)
|
|
122
150
|
|
|
151
|
+
# Topic is required when not specifying a single repo
|
|
152
|
+
if not repo and not topic:
|
|
153
|
+
raise click.UsageError(
|
|
154
|
+
"Topic is required when not specifying a single repository (use --topic or set RELEASE_CHECKER_TOPIC)"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
g = Github(token)
|
|
123
158
|
if repo:
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
candidates = [g.get_repo(repo)]
|
|
160
|
+
else:
|
|
161
|
+
user = g.get_user()
|
|
162
|
+
log.info("filtering by topic", topic=topic)
|
|
163
|
+
# Public repos owned by the user with the specified topic.
|
|
164
|
+
candidates = list(
|
|
165
|
+
g.search_repositories(
|
|
166
|
+
f"user:{user.login} topic:{topic} fork:false is:public",
|
|
167
|
+
sort="updated",
|
|
168
|
+
order="desc",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
if not candidates:
|
|
172
|
+
log.warning("no repositories found with topic", topic=topic)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
global_gap = get_global_min_release_gap()
|
|
176
|
+
try:
|
|
177
|
+
blocked = release_creation_blocked_by_global_gap(candidates, gap=global_gap)
|
|
178
|
+
except GithubException:
|
|
179
|
+
log.error(
|
|
180
|
+
"skipping release creation because global gap state could not be read"
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if blocked:
|
|
185
|
+
if repo:
|
|
186
|
+
log.info("release check complete - no release needed", dry_run=dry_run)
|
|
187
|
+
else:
|
|
188
|
+
_log_release_run_summary(dry_run, checked=0, created=0, skipped=0, failed=0)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if repo:
|
|
192
|
+
result = check_repo_for_release(candidates[0], dry_run)
|
|
126
193
|
if result["created"]:
|
|
127
194
|
log.info("release check complete - created 1 release", dry_run=dry_run)
|
|
128
195
|
elif result["failed"]:
|
|
@@ -133,32 +200,10 @@ def generate_releases(dry_run, topic, repo, max_releases):
|
|
|
133
200
|
log.info("release check complete - no release needed", dry_run=dry_run)
|
|
134
201
|
return
|
|
135
202
|
|
|
136
|
-
# Topic is required when not specifying a single repo
|
|
137
|
-
if not topic:
|
|
138
|
-
raise click.UsageError(
|
|
139
|
-
"Topic is required when not specifying a single repository (use --topic or set RELEASE_CHECKER_TOPIC)"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
g = Github(token)
|
|
143
|
-
user = g.get_user()
|
|
144
|
-
|
|
145
|
-
log.info("filtering by topic", topic=topic)
|
|
146
|
-
|
|
147
|
-
# Get all public repos owned by user with the specified topic
|
|
148
|
-
repos = g.search_repositories(
|
|
149
|
-
f"user:{user.login} topic:{topic} fork:false is:public",
|
|
150
|
-
sort="updated",
|
|
151
|
-
order="desc",
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
def _check_repo_with_log(r):
|
|
155
|
-
log.info("checking repo for release", repo=r.full_name, dry_run=dry_run)
|
|
156
|
-
return check_repo_for_release(r, dry_run=dry_run)
|
|
157
|
-
|
|
158
203
|
results = []
|
|
159
204
|
created_count = 0
|
|
160
205
|
|
|
161
|
-
for
|
|
206
|
+
for candidate in candidates:
|
|
162
207
|
if max_releases > 0 and created_count >= max_releases:
|
|
163
208
|
log.info(
|
|
164
209
|
"reached max releases limit for this run",
|
|
@@ -166,40 +211,29 @@ def generate_releases(dry_run, topic, repo, max_releases):
|
|
|
166
211
|
)
|
|
167
212
|
break
|
|
168
213
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
214
|
+
log.info("checking repo for release", repo=candidate.full_name, dry_run=dry_run)
|
|
215
|
+
result = check_repo_for_release(candidate, dry_run=dry_run)
|
|
216
|
+
results.append(result)
|
|
217
|
+
if result["created"]:
|
|
172
218
|
created_count += 1
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
total_checked = sum(1 for
|
|
181
|
-
total_skipped = sum(1 for
|
|
182
|
-
total_created = sum(1 for
|
|
183
|
-
total_failed = sum(1 for
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
would_create=total_created,
|
|
192
|
-
skipped=total_skipped,
|
|
193
|
-
errors=total_failed,
|
|
194
|
-
)
|
|
195
|
-
else:
|
|
196
|
-
log.info(
|
|
197
|
-
"release check complete",
|
|
198
|
-
checked=total_checked,
|
|
199
|
-
created=total_created,
|
|
200
|
-
skipped=total_skipped,
|
|
201
|
-
failed=total_failed,
|
|
202
|
-
)
|
|
219
|
+
if should_stop_for_global_gap(global_gap, max_releases, created_count):
|
|
220
|
+
log.info(
|
|
221
|
+
"stopping after one auto-generated release to honor global minimum gap",
|
|
222
|
+
min_gap_seconds=int(global_gap.total_seconds()),
|
|
223
|
+
)
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
total_checked = sum(1 for result in results if result["checked"])
|
|
227
|
+
total_skipped = sum(1 for result in results if result["skipped"])
|
|
228
|
+
total_created = sum(1 for result in results if result["created"])
|
|
229
|
+
total_failed = sum(1 for result in results if result["failed"])
|
|
230
|
+
_log_release_run_summary(
|
|
231
|
+
dry_run,
|
|
232
|
+
checked=total_checked,
|
|
233
|
+
created=total_created,
|
|
234
|
+
skipped=total_skipped,
|
|
235
|
+
failed=total_failed,
|
|
236
|
+
)
|
|
203
237
|
|
|
204
238
|
|
|
205
239
|
cli.add_command(dependabot)
|
|
@@ -210,8 +210,8 @@ def handle_stale_dependabot_pr(pr: PullRequest) -> None:
|
|
|
210
210
|
Handle a dependabot PR that has been open for at least 30 days and has conflicts
|
|
211
211
|
"""
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
if not pr.user or pr.user.login != "dependabot[bot]":
|
|
214
|
+
return
|
|
215
215
|
|
|
216
216
|
if pr.body is None:
|
|
217
217
|
return
|
|
@@ -221,25 +221,33 @@ def handle_stale_dependabot_pr(pr: PullRequest) -> None:
|
|
|
221
221
|
"PR has disabled automatic rebases, manually commenting", url=pr.html_url
|
|
222
222
|
)
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
try:
|
|
225
|
+
pr.create_issue_comment("@dependabot rebase")
|
|
226
|
+
except GithubException as error:
|
|
227
|
+
log.error(
|
|
228
|
+
"failed to comment on stale PR",
|
|
229
|
+
pr=pr.html_url,
|
|
230
|
+
status=error.status,
|
|
231
|
+
error=error.data.get("message", str(error)),
|
|
232
|
+
)
|
|
225
233
|
|
|
226
234
|
|
|
227
235
|
def is_eligible_for_merge(pr: PullRequest) -> bool:
|
|
228
|
-
resolve_async_status(pr, "mergeable")
|
|
229
|
-
|
|
230
236
|
if pr.state == "closed":
|
|
231
237
|
log.debug("PR is closed", url=pr.html_url)
|
|
232
238
|
return False
|
|
233
239
|
|
|
240
|
+
if not pr.user or pr.user.login != "dependabot[bot]":
|
|
241
|
+
log.debug("PR is not from dependabot", url=pr.html_url)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
resolve_async_status(pr, "mergeable")
|
|
245
|
+
|
|
234
246
|
if not pr.mergeable:
|
|
235
247
|
log.debug("PR is not mergeable", url=pr.html_url)
|
|
236
248
|
handle_stale_dependabot_pr(pr)
|
|
237
249
|
return False
|
|
238
250
|
|
|
239
|
-
if pr.user.login != "dependabot[bot]":
|
|
240
|
-
log.debug("PR is not from dependabot", url=pr.html_url)
|
|
241
|
-
return False
|
|
242
|
-
|
|
243
251
|
last_commit = pr.get_commits().reversed[0]
|
|
244
252
|
combined_status = last_commit.get_combined_status()
|
|
245
253
|
status = combined_status.state
|
|
@@ -7,6 +7,8 @@ calculates semantic version bumps, and generates formatted release notes.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from dataclasses import dataclass
|
|
10
12
|
from datetime import UTC, datetime, timedelta
|
|
11
13
|
|
|
12
14
|
from github import GithubException
|
|
@@ -21,6 +23,9 @@ from github_overlord.ai import (
|
|
|
21
23
|
from github_overlord.config import JINJA_ENV
|
|
22
24
|
from github_overlord.utils import log
|
|
23
25
|
|
|
26
|
+
# Release notes include this line. It is how a later run recognizes its own releases.
|
|
27
|
+
GENERATED_BY_MARKER = "**Generated-by**: https://github.com/iloveitaly/github-overlord"
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
def _parse_duration_to_timedelta(value: str) -> timedelta:
|
|
26
31
|
"""Parse a simple duration string into a timedelta.
|
|
@@ -56,29 +61,168 @@ def _parse_duration_to_timedelta(value: str) -> timedelta:
|
|
|
56
61
|
raise ValueError(f"unsupported duration unit: {unit!r}")
|
|
57
62
|
|
|
58
63
|
|
|
59
|
-
def
|
|
60
|
-
|
|
64
|
+
def _get_configured_gap(
|
|
65
|
+
env_var: str,
|
|
66
|
+
default: str,
|
|
67
|
+
*,
|
|
68
|
+
invalid_event: str,
|
|
69
|
+
negative_event: str,
|
|
70
|
+
) -> timedelta:
|
|
71
|
+
raw = os.getenv(env_var, default)
|
|
61
72
|
try:
|
|
62
73
|
gap = _parse_duration_to_timedelta(raw)
|
|
63
74
|
except ValueError:
|
|
64
|
-
log.warning(
|
|
65
|
-
|
|
66
|
-
value=raw,
|
|
67
|
-
default="2w",
|
|
68
|
-
)
|
|
69
|
-
gap = timedelta(weeks=2)
|
|
75
|
+
log.warning(invalid_event, value=raw, default=default)
|
|
76
|
+
gap = _parse_duration_to_timedelta(default)
|
|
70
77
|
|
|
71
78
|
if gap.total_seconds() < 0:
|
|
72
|
-
log.warning(
|
|
73
|
-
|
|
74
|
-
value=raw,
|
|
75
|
-
default="2w",
|
|
76
|
-
)
|
|
77
|
-
return timedelta(weeks=2)
|
|
79
|
+
log.warning(negative_event, value=raw, default=default)
|
|
80
|
+
return _parse_duration_to_timedelta(default)
|
|
78
81
|
|
|
79
82
|
return gap
|
|
80
83
|
|
|
81
84
|
|
|
85
|
+
def _get_min_release_gap() -> timedelta:
|
|
86
|
+
return _get_configured_gap(
|
|
87
|
+
"RELEASE_CHECKER_MIN_GAP",
|
|
88
|
+
"2w",
|
|
89
|
+
invalid_event="invalid release_checker_min_gap; falling back to default",
|
|
90
|
+
negative_event="negative release_checker_min_gap; falling back to default",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_global_min_release_gap() -> timedelta:
|
|
95
|
+
"""Minimum time between auto-generated releases across the selected repos.
|
|
96
|
+
|
|
97
|
+
Defaults to one week. ``0`` disables the limit.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
return _get_configured_gap(
|
|
101
|
+
"RELEASE_CHECKER_GLOBAL_MIN_GAP",
|
|
102
|
+
"1w",
|
|
103
|
+
invalid_event="invalid release_checker_global_min_gap; falling back to default",
|
|
104
|
+
negative_event="negative release_checker_global_min_gap; falling back to default",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _as_utc(value: datetime) -> datetime:
|
|
109
|
+
if value.tzinfo is None:
|
|
110
|
+
return value.replace(tzinfo=UTC)
|
|
111
|
+
return value.astimezone(UTC)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_generated_release(release: object) -> bool:
|
|
115
|
+
body = getattr(release, "body", None) or ""
|
|
116
|
+
if not isinstance(body, str):
|
|
117
|
+
return False
|
|
118
|
+
return GENERATED_BY_MARKER in body
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _published_at(release: object) -> datetime | None:
|
|
122
|
+
published = getattr(release, "published_at", None)
|
|
123
|
+
if not isinstance(published, datetime):
|
|
124
|
+
return None
|
|
125
|
+
return _as_utc(published)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class GeneratedRelease:
|
|
130
|
+
"""An auto-generated GitHub release, identified by its release notes."""
|
|
131
|
+
|
|
132
|
+
repo_full_name: str
|
|
133
|
+
tag_name: str
|
|
134
|
+
published_at: datetime
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def latest_generated_release_since(
|
|
138
|
+
repos: Iterable[Repository], since: datetime
|
|
139
|
+
) -> GeneratedRelease | None:
|
|
140
|
+
"""Return the newest auto-generated release published after ``since``.
|
|
141
|
+
|
|
142
|
+
GitHub lists releases by ``created_at``, and that field is the tagged
|
|
143
|
+
commit's timestamp, not when the release was published. A release published
|
|
144
|
+
today for an older commit can sort behind a newer commit, so this compares
|
|
145
|
+
``published_at`` across the full list. Manual releases are ignored.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
since = _as_utc(since)
|
|
149
|
+
latest: GeneratedRelease | None = None
|
|
150
|
+
|
|
151
|
+
for repo in repos:
|
|
152
|
+
try:
|
|
153
|
+
for release in repo.get_releases():
|
|
154
|
+
published_at = _published_at(release)
|
|
155
|
+
if published_at is None or published_at <= since:
|
|
156
|
+
continue
|
|
157
|
+
if latest is not None and published_at <= latest.published_at:
|
|
158
|
+
continue
|
|
159
|
+
if not _is_generated_release(release):
|
|
160
|
+
continue
|
|
161
|
+
latest = GeneratedRelease(
|
|
162
|
+
repo_full_name=repo.full_name,
|
|
163
|
+
tag_name=release.tag_name,
|
|
164
|
+
published_at=published_at,
|
|
165
|
+
)
|
|
166
|
+
except GithubException as error:
|
|
167
|
+
log.error(
|
|
168
|
+
"failed to list releases while checking global gap",
|
|
169
|
+
repo=getattr(repo, "full_name", None),
|
|
170
|
+
error=str(error),
|
|
171
|
+
)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
return latest
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def release_creation_blocked_by_global_gap(
|
|
178
|
+
repos: Iterable[Repository],
|
|
179
|
+
*,
|
|
180
|
+
now: datetime | None = None,
|
|
181
|
+
gap: timedelta | None = None,
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""Return whether an auto-generated release is still inside the global gap.
|
|
184
|
+
|
|
185
|
+
A gap of zero disables the check. The clock is the newest published
|
|
186
|
+
auto-generated release among ``repos``.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
gap = get_global_min_release_gap() if gap is None else gap
|
|
190
|
+
if gap.total_seconds() <= 0:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
now = _as_utc(now or datetime.now(UTC))
|
|
194
|
+
latest = latest_generated_release_since(repos, now - gap)
|
|
195
|
+
if latest is None:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
time_since = now - latest.published_at
|
|
199
|
+
log.info(
|
|
200
|
+
"skipping release check due to global minimum gap",
|
|
201
|
+
last_release=latest.tag_name,
|
|
202
|
+
last_release_repo=latest.repo_full_name,
|
|
203
|
+
time_since_release_seconds=int(time_since.total_seconds()),
|
|
204
|
+
min_gap_seconds=int(gap.total_seconds()),
|
|
205
|
+
)
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def should_stop_for_global_gap(
|
|
210
|
+
gap: timedelta, max_releases: int, created_count: int
|
|
211
|
+
) -> bool:
|
|
212
|
+
"""Return whether this run should stop after creating an auto-generated release.
|
|
213
|
+
|
|
214
|
+
A positive global gap allows one new auto-generated release per interval.
|
|
215
|
+
``max_releases`` still caps a run on its own when the global gap is disabled.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
if gap.total_seconds() <= 0 or created_count < 1:
|
|
219
|
+
return False
|
|
220
|
+
# max_releases <= 0 means unlimited for the run; the global gap is the cap.
|
|
221
|
+
if max_releases <= 0:
|
|
222
|
+
return True
|
|
223
|
+
return created_count < max_releases
|
|
224
|
+
|
|
225
|
+
|
|
82
226
|
class ReleaseAnalysis(BaseModel):
|
|
83
227
|
"""Structured output from LLM analysis of commits."""
|
|
84
228
|
|
|
@@ -339,9 +483,7 @@ def generate_release_notes(
|
|
|
339
483
|
changelog_url = f"https://github.com/{repo.full_name}/commits/{new_tag}"
|
|
340
484
|
|
|
341
485
|
notes_parts.append(f"**Full Changelog**: {changelog_url}")
|
|
342
|
-
notes_parts.append(
|
|
343
|
-
"**Generated-by**: https://github.com/iloveitaly/github-overlord"
|
|
344
|
-
)
|
|
486
|
+
notes_parts.append(GENERATED_BY_MARKER)
|
|
345
487
|
|
|
346
488
|
return "\n".join(notes_parts)
|
|
347
489
|
|
|
@@ -381,6 +523,9 @@ def check_repo_for_release(repo: Repository, dry_run: bool) -> dict:
|
|
|
381
523
|
"""
|
|
382
524
|
Check a single repository and create a release if recommended.
|
|
383
525
|
|
|
526
|
+
The cross-repo ``RELEASE_CHECKER_GLOBAL_MIN_GAP`` is enforced by the
|
|
527
|
+
generate-releases command before this function is called.
|
|
528
|
+
|
|
384
529
|
Returns:
|
|
385
530
|
dict with keys: checked, skipped, created, failed
|
|
386
531
|
"""
|
|
File without changes
|
|
File without changes
|
{github_overlord-0.9.2 → github_overlord-0.10.1}/github_overlord/data/release_analysis_prompt.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|