alissa-tools-github-revloop 0.31.22__tar.gz → 0.31.24__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.
- {alissa_tools_github_revloop-0.31.22/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.24}/PKG-INFO +14 -1
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/README.md +13 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/janitor.py +145 -12
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/loop.py +39 -13
- alissa_tools_github_revloop-0.31.24/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +14 -1
- alissa_tools_github_revloop-0.31.22/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alissa-tools-github-revloop
|
|
3
|
-
Version: 0.31.
|
|
3
|
+
Version: 0.31.24
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-REVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -58,6 +58,19 @@ flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
|
58
58
|
the skip off: every discovery PR is then reviewed like any other. See
|
|
59
59
|
*Discovery PRs* in the repository README.
|
|
60
60
|
|
|
61
|
+
## Integration-check PRs are not reviewed
|
|
62
|
+
|
|
63
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
64
|
+
body carries `<!-- alissa-integration-check v1 -->` on a line of its own and
|
|
65
|
+
whose every changed path is under `integration-check/` gets **no review
|
|
66
|
+
round** either — the same skip, beside the discovery one, with one
|
|
67
|
+
activity-comment line per head. There is no light round. A marked PR that
|
|
68
|
+
touches anything else (or whose file list cannot be read in full) is reviewed
|
|
69
|
+
under the code directive, unchanged. The fleet flag `integration_check_review`
|
|
70
|
+
(`ALISSA_REVIEW_INTEGRATION_CHECK_REVIEW`, default off) turns the skip off:
|
|
71
|
+
every integration-check PR is then reviewed like any other. The two flags are
|
|
72
|
+
independent. See *Integration-check PRs* in the repository README.
|
|
73
|
+
|
|
61
74
|
## Install
|
|
62
75
|
|
|
63
76
|
```sh
|
|
@@ -36,6 +36,19 @@ flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
|
36
36
|
the skip off: every discovery PR is then reviewed like any other. See
|
|
37
37
|
*Discovery PRs* in the repository README.
|
|
38
38
|
|
|
39
|
+
## Integration-check PRs are not reviewed
|
|
40
|
+
|
|
41
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
42
|
+
body carries `<!-- alissa-integration-check v1 -->` on a line of its own and
|
|
43
|
+
whose every changed path is under `integration-check/` gets **no review
|
|
44
|
+
round** either — the same skip, beside the discovery one, with one
|
|
45
|
+
activity-comment line per head. There is no light round. A marked PR that
|
|
46
|
+
touches anything else (or whose file list cannot be read in full) is reviewed
|
|
47
|
+
under the code directive, unchanged. The fleet flag `integration_check_review`
|
|
48
|
+
(`ALISSA_REVIEW_INTEGRATION_CHECK_REVIEW`, default off) turns the skip off:
|
|
49
|
+
every integration-check PR is then reviewed like any other. The two flags are
|
|
50
|
+
independent. See *Integration-check PRs* in the repository README.
|
|
51
|
+
|
|
39
52
|
## Install
|
|
40
53
|
|
|
41
54
|
```sh
|
|
@@ -42,6 +42,9 @@ Three properties everything here is written around:
|
|
|
42
42
|
and the grace is a day by default. A held folder is kept whatever its age.
|
|
43
43
|
Under disk PRESSURE (issue #177) the grace drops -- to two hours by default
|
|
44
44
|
-- oldest first, only while the volume is still at or above its warn line.
|
|
45
|
+
A round that names no review task yet holds only its hub's checkouts made
|
|
46
|
+
since it spawned (issue #185), and at or above `BACKSTOP_PCT` no such
|
|
47
|
+
hub-wide hold reaches a checkout older than the stale-round window.
|
|
45
48
|
* **Nothing raises past its own concern.** A folder that vanishes mid-sweep,
|
|
46
49
|
an unreadable hub, a failed removal: logged and skipped, because this runs
|
|
47
50
|
at the end of a poll pass that must complete.
|
|
@@ -104,6 +107,20 @@ SUPPORTED_TEMPLATE = "{root}/{repo}/main"
|
|
|
104
107
|
_PROBE_OWNERS = ("janitor-probe-owner-a", "janitor-probe-owner-b")
|
|
105
108
|
_PROBE_REPO = "janitor-probe-repo"
|
|
106
109
|
|
|
110
|
+
# How far before a round's spawn time a checkout may be stamped and still
|
|
111
|
+
# be held by that round when its review-task ref is unknown (issue #185):
|
|
112
|
+
# clock skew between the ledger's stamp and the filesystem's.
|
|
113
|
+
HOLD_SKEW_SECONDS = 5 * 60
|
|
114
|
+
|
|
115
|
+
# The volume used percentage at or above which the pressure backstop applies
|
|
116
|
+
# (issue #185): a hub-wide hold is void for checkouts older than the stale-
|
|
117
|
+
# round window, and one WARNING names what holds the most bytes.
|
|
118
|
+
BACKSTOP_PCT = 95.0
|
|
119
|
+
|
|
120
|
+
# The two reasons a hub-wide hold gives, so the sweep summary groups them.
|
|
121
|
+
HUB_HOLD_BOUNDED = "a live round in this hub names no review task and spawned before this checkout"
|
|
122
|
+
HUB_HOLD_UNBOUNDED = "a live round in this hub names no review task or spawn time"
|
|
123
|
+
|
|
107
124
|
Runner = Callable[..., str]
|
|
108
125
|
|
|
109
126
|
|
|
@@ -138,25 +155,43 @@ class Guard:
|
|
|
138
155
|
`review-pr-<n>` session names no repo, and holds its PR number anywhere.
|
|
139
156
|
|
|
140
157
|
A round whose review-task ref is unknown cannot be mapped to a single
|
|
141
|
-
REVIEW- checkout
|
|
158
|
+
REVIEW- checkout -- every fresh round starts that way, since the reviewer
|
|
159
|
+
creates its review task after it spawns. Such a round holds the checkouts
|
|
160
|
+
in its hub that are NEWER than its spawn time less `HOLD_SKEW_SECONDS`
|
|
161
|
+
(issue #185): a checkout older than every unknown-ref round in the hub
|
|
162
|
+
cannot belong to any of them. `all_checkouts` keeps, per hub, the
|
|
163
|
+
earliest such bound; None there means a round whose spawn time is not
|
|
164
|
+
known either, and holds every checkout in the hub, as before issue #185
|
|
165
|
+
-- the hold that, with studio reviews running near-continuously, kept 78
|
|
166
|
+
dead checkouts (~44 GB) and filled the fleet's volume on 2026-10-21.
|
|
142
167
|
"""
|
|
143
168
|
|
|
144
169
|
numbers: "dict[str | None, set[int]]" = field(default_factory=dict)
|
|
145
170
|
tasks: "dict[str | None, set[str]]" = field(default_factory=dict)
|
|
146
|
-
all_checkouts: "
|
|
171
|
+
all_checkouts: "dict[str | None, float | None]" = field(default_factory=dict)
|
|
147
172
|
|
|
148
173
|
def hold(
|
|
149
|
-
self,
|
|
174
|
+
self,
|
|
175
|
+
repo: "str | None",
|
|
176
|
+
number: int,
|
|
177
|
+
task_ref: "str | None" = None,
|
|
178
|
+
since: "float | None" = None,
|
|
150
179
|
) -> None:
|
|
151
180
|
"""Hold PR `number` of `repo` (`owner/name`, a bare name, or a
|
|
152
|
-
session-name slug; None = any hub) and the checkout its task names.
|
|
181
|
+
session-name slug; None = any hub) and the checkout its task names.
|
|
182
|
+
With no task ref, `since` (the round's spawn time, epoch seconds)
|
|
183
|
+
bounds the checkouts held in the hub; without it, every one is."""
|
|
153
184
|
slug = None if repo is None else session_repo_slug(repo.rpartition("/")[2])
|
|
154
185
|
self.numbers.setdefault(slug, set()).add(int(number))
|
|
155
186
|
ref = (task_ref or "").strip()
|
|
156
187
|
if ref:
|
|
157
188
|
self.tasks.setdefault(slug, set()).add(ref)
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
return
|
|
190
|
+
bound = None if since is None else float(since) - HOLD_SKEW_SECONDS
|
|
191
|
+
if slug in self.all_checkouts:
|
|
192
|
+
prev = self.all_checkouts[slug]
|
|
193
|
+
bound = None if prev is None or bound is None else min(prev, bound)
|
|
194
|
+
self.all_checkouts[slug] = bound
|
|
160
195
|
|
|
161
196
|
def _in(self, table: "dict[str | None, set]", slug: str, value: object) -> bool:
|
|
162
197
|
return value in table.get(slug, ()) or value in table.get(None, ())
|
|
@@ -164,8 +199,19 @@ class Guard:
|
|
|
164
199
|
def _any(self, slug: str) -> bool:
|
|
165
200
|
return bool(self.numbers.get(slug) or self.numbers.get(None))
|
|
166
201
|
|
|
167
|
-
def holds(
|
|
168
|
-
|
|
202
|
+
def holds(
|
|
203
|
+
self,
|
|
204
|
+
candidate: Candidate,
|
|
205
|
+
mtime: "float | None" = None,
|
|
206
|
+
void_before: "float | None" = None,
|
|
207
|
+
) -> "str | None":
|
|
208
|
+
"""Why `candidate` must stay, or None when nothing holds it.
|
|
209
|
+
|
|
210
|
+
`mtime` is the candidate's age stamp (`newest_mtime`); a review
|
|
211
|
+
checkout with none is held by every hub-wide hold, bounded or not.
|
|
212
|
+
`void_before` is the pressure backstop (issue #185): a hub-wide hold
|
|
213
|
+
does not reach a checkout whose stamp is older than it. A known task
|
|
214
|
+
ref's hold and a scratch folder's are never voided."""
|
|
169
215
|
slug = candidate.hub_slug
|
|
170
216
|
if candidate.kind == KIND_SCRATCH:
|
|
171
217
|
if candidate.number is None:
|
|
@@ -173,10 +219,18 @@ class Guard:
|
|
|
173
219
|
if self._in(self.numbers, slug, candidate.number):
|
|
174
220
|
return f"PR #{candidate.number} has a live session or open round"
|
|
175
221
|
return None
|
|
176
|
-
if slug in self.all_checkouts or None in self.all_checkouts:
|
|
177
|
-
return "a live round in this hub names no review task"
|
|
178
222
|
if candidate.task_ref and self._in(self.tasks, slug, candidate.task_ref):
|
|
179
223
|
return f"{candidate.task_ref} has a live session or open round"
|
|
224
|
+
if mtime is not None and void_before is not None and mtime < void_before:
|
|
225
|
+
return None
|
|
226
|
+
for key in (slug, None):
|
|
227
|
+
if key not in self.all_checkouts:
|
|
228
|
+
continue
|
|
229
|
+
bound = self.all_checkouts[key]
|
|
230
|
+
if bound is None:
|
|
231
|
+
return HUB_HOLD_UNBOUNDED
|
|
232
|
+
if mtime is None or mtime >= bound:
|
|
233
|
+
return HUB_HOLD_BOUNDED
|
|
180
234
|
return None
|
|
181
235
|
|
|
182
236
|
|
|
@@ -199,11 +253,27 @@ class SweepResult:
|
|
|
199
253
|
# in a dry run, would-be removals) only that shorter grace allowed.
|
|
200
254
|
pressure: bool = False
|
|
201
255
|
pressure_removed: int = 0
|
|
256
|
+
# Issue #185: bytes held per "<hub slug>: <reason>", so held growth is
|
|
257
|
+
# visible before it fills the disk; held folders not sized because the
|
|
258
|
+
# sweep's time budget ran out; and, when the backstop applied, how many
|
|
259
|
+
# checkouts only it released from a hub-wide hold.
|
|
260
|
+
held_bytes: "dict[str, int]" = field(default_factory=dict)
|
|
261
|
+
held_unsized: int = 0
|
|
262
|
+
backstop: bool = False
|
|
263
|
+
backstop_released: int = 0
|
|
202
264
|
|
|
203
265
|
@property
|
|
204
266
|
def freed(self) -> int:
|
|
205
267
|
return sum(size for _, size in self.removed)
|
|
206
268
|
|
|
269
|
+
@property
|
|
270
|
+
def held_total(self) -> int:
|
|
271
|
+
return sum(self.held_bytes.values())
|
|
272
|
+
|
|
273
|
+
def top_held(self, limit: int = 5) -> "list[tuple[str, int]]":
|
|
274
|
+
"""The reasons holding the most bytes, largest first."""
|
|
275
|
+
return sorted(self.held_bytes.items(), key=lambda kv: (-kv[1], kv[0]))[:limit]
|
|
276
|
+
|
|
207
277
|
@property
|
|
208
278
|
def left(self) -> int:
|
|
209
279
|
"""Candidate folders still on disk after a live sweep: inside the
|
|
@@ -211,6 +281,29 @@ class SweepResult:
|
|
|
211
281
|
return self.fresh + len(self.held) + self.deferred + len(self.failed)
|
|
212
282
|
|
|
213
283
|
|
|
284
|
+
def held_reasons(pairs: "Iterable[tuple[str, int]]") -> str:
|
|
285
|
+
"""`; <hub>: <reason> = <size>, ...` for the log lines, or '' when none."""
|
|
286
|
+
parts = [f"{key} = {human(size)}" for key, size in pairs]
|
|
287
|
+
return ("; " + ", ".join(parts)) if parts else ""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# How many held reasons the sweep summary names before folding the rest.
|
|
291
|
+
HELD_REASONS_LOGGED = 10
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def held_summary(result: SweepResult) -> str:
|
|
295
|
+
"""The sweep summary's held-bytes tail (issue #185): every hub/reason
|
|
296
|
+
holding bytes, largest first, at most HELD_REASONS_LOGGED of them, and
|
|
297
|
+
how many held folders went unsized."""
|
|
298
|
+
tail = held_reasons(result.top_held(HELD_REASONS_LOGGED))
|
|
299
|
+
rest = len(result.held_bytes) - HELD_REASONS_LOGGED
|
|
300
|
+
if rest > 0:
|
|
301
|
+
tail += f", +{rest} more reason(s)"
|
|
302
|
+
if result.held_unsized:
|
|
303
|
+
tail += f"; {result.held_unsized} held folder(s) not sized this sweep"
|
|
304
|
+
return tail
|
|
305
|
+
|
|
306
|
+
|
|
214
307
|
def layout_supported(
|
|
215
308
|
hub_for: "Callable[[str, str], Path]", workspace_root: "Path | str"
|
|
216
309
|
) -> bool:
|
|
@@ -434,6 +527,8 @@ def sweep(
|
|
|
434
527
|
clock: "Callable[[], float]" = time.monotonic,
|
|
435
528
|
pressure_grace_seconds: "float | None" = None,
|
|
436
529
|
under_pressure: "Callable[[], bool] | None" = None,
|
|
530
|
+
stale_round_seconds: "float | None" = None,
|
|
531
|
+
sizes: "dict[Path, tuple[float, int]] | None" = None,
|
|
437
532
|
) -> SweepResult:
|
|
438
533
|
"""Remove every candidate under `hubs` older than `grace_seconds` that
|
|
439
534
|
`guard` does not hold, oldest first, then prune each hub's worktree
|
|
@@ -456,7 +551,18 @@ def sweep(
|
|
|
456
551
|
Dry-run removes nothing and prunes nothing: it logs each folder it WOULD
|
|
457
552
|
remove (with its size) and reports them in `would_remove`, under the
|
|
458
553
|
same bound -- so it reports what one live sweep would do (a dry run frees
|
|
459
|
-
nothing, so under pressure it reports every pressure-eligible folder).
|
|
554
|
+
nothing, so under pressure it reports every pressure-eligible folder).
|
|
555
|
+
|
|
556
|
+
The pressure backstop (issue #185): when `stale_round_seconds` is given
|
|
557
|
+
(the caller passes STALE_ROUND_SECONDS at or above `BACKSTOP_PCT`), a
|
|
558
|
+
hub-wide hold -- a round with no review-task ref -- is void for a
|
|
559
|
+
checkout older than that window: no live round can own a checkout older
|
|
560
|
+
than the stale window. `backstop_released` counts what only it freed.
|
|
561
|
+
|
|
562
|
+
Every held folder is sized into `held_bytes` per hub and reason. `sizes`
|
|
563
|
+
(path -> (mtime, bytes)), when given, caches those walks across sweeps
|
|
564
|
+
and is re-keyed to this sweep's held folders; sizing stops at the time
|
|
565
|
+
budget too, and what was left unsized is counted in `held_unsized`."""
|
|
460
566
|
result = SweepResult()
|
|
461
567
|
started = clock()
|
|
462
568
|
bounded = False
|
|
@@ -466,6 +572,11 @@ def sweep(
|
|
|
466
572
|
result.pressure = under_pressure()
|
|
467
573
|
if result.pressure:
|
|
468
574
|
shrunk = min(grace_seconds, pressure_grace_seconds)
|
|
575
|
+
void_before = None
|
|
576
|
+
if stale_round_seconds is not None:
|
|
577
|
+
result.backstop = True
|
|
578
|
+
void_before = now - stale_round_seconds
|
|
579
|
+
seen_sizes: "dict[Path, tuple[float, int]]" = {}
|
|
469
580
|
aged: "list[tuple[float, Candidate]]" = []
|
|
470
581
|
for hub in hub_list:
|
|
471
582
|
result.hubs += 1
|
|
@@ -480,10 +591,29 @@ def sweep(
|
|
|
480
591
|
if age < shrunk:
|
|
481
592
|
result.fresh += 1
|
|
482
593
|
continue
|
|
483
|
-
why = guard.holds(candidate)
|
|
594
|
+
why = guard.holds(candidate, mtime)
|
|
595
|
+
if why is not None and void_before is not None:
|
|
596
|
+
if guard.holds(candidate, mtime, void_before) is None:
|
|
597
|
+
result.backstop_released += 1
|
|
598
|
+
log.info(
|
|
599
|
+
"janitor: backstop releases %s (%.1f h old) from a hub-wide "
|
|
600
|
+
"hold — %s", candidate.path, age / 3600, why,
|
|
601
|
+
)
|
|
602
|
+
why = None
|
|
484
603
|
if why is not None:
|
|
485
604
|
log.debug("janitor: keeping %s — %s", candidate.path, why)
|
|
486
605
|
result.held.append((candidate.path, why))
|
|
606
|
+
key = f"{candidate.hub_slug}: {why}"
|
|
607
|
+
cached = (sizes or {}).get(candidate.path)
|
|
608
|
+
if cached is not None and cached[0] == mtime:
|
|
609
|
+
size = cached[1]
|
|
610
|
+
elif clock() - started >= max_seconds:
|
|
611
|
+
result.held_unsized += 1
|
|
612
|
+
continue
|
|
613
|
+
else:
|
|
614
|
+
size = dir_bytes(candidate.path)
|
|
615
|
+
seen_sizes[candidate.path] = (mtime, size)
|
|
616
|
+
result.held_bytes[key] = result.held_bytes.get(key, 0) + size
|
|
487
617
|
continue
|
|
488
618
|
early = age < grace_seconds
|
|
489
619
|
if early and not dry_run and not (under_pressure and under_pressure()):
|
|
@@ -520,6 +650,9 @@ def sweep(
|
|
|
520
650
|
result.removed.append((candidate.path, size))
|
|
521
651
|
if early:
|
|
522
652
|
result.pressure_removed += 1
|
|
653
|
+
if sizes is not None:
|
|
654
|
+
sizes.clear()
|
|
655
|
+
sizes.update(seen_sizes)
|
|
523
656
|
if dry_run:
|
|
524
657
|
return result
|
|
525
658
|
for hub in hub_list:
|
|
@@ -2847,6 +2847,9 @@ class ReviewWatcher:
|
|
|
2847
2847
|
# under the line closes it. In memory, so a restart under pressure
|
|
2848
2848
|
# opens (and reports) a new episode.
|
|
2849
2849
|
self._janitor_pressure_since: "float | None" = None
|
|
2850
|
+
# Held folders' sizes from the last sweep, path -> (mtime, bytes)
|
|
2851
|
+
# (issue #185): the held-bytes summary re-walks only what changed.
|
|
2852
|
+
self._janitor_sizes: "dict[Path, tuple[float, int]]" = {}
|
|
2850
2853
|
# The (repo, number, round) whose alive session the stale-round probe
|
|
2851
2854
|
# just killed as `wedged:first-run-dialog` (issue #136), so the
|
|
2852
2855
|
# respawn site logs its re-enqueue at INFO instead of a second
|
|
@@ -8363,8 +8366,9 @@ class ReviewWatcher:
|
|
|
8363
8366
|
stamp = time.time() if now is None else now
|
|
8364
8367
|
pct = self._volume_used_pct()
|
|
8365
8368
|
pressed = pct is not None and pct >= self.config.janitor_warn_pct
|
|
8369
|
+
backstop = pct is not None and pct >= janitor_mod.BACKSTOP_PCT
|
|
8366
8370
|
try:
|
|
8367
|
-
result = self._sweep_workspace(stamp, pressed=pressed)
|
|
8371
|
+
result = self._sweep_workspace(stamp, pressed=pressed, backstop=backstop)
|
|
8368
8372
|
except Exception as exc:
|
|
8369
8373
|
log.warning(
|
|
8370
8374
|
"janitor: sweep failed unexpectedly (%s: %s) — nothing more is "
|
|
@@ -8391,7 +8395,7 @@ class ReviewWatcher:
|
|
|
8391
8395
|
return pct is not None and pct >= self.config.janitor_warn_pct
|
|
8392
8396
|
|
|
8393
8397
|
def _sweep_workspace(
|
|
8394
|
-
self, now: float, *, pressed: bool = False
|
|
8398
|
+
self, now: float, *, pressed: bool = False, backstop: bool = False
|
|
8395
8399
|
) -> "janitor_mod.SweepResult | None":
|
|
8396
8400
|
if not janitor_mod.layout_supported(
|
|
8397
8401
|
self.config.hub_for, self.config.workspace_root
|
|
@@ -8426,7 +8430,17 @@ class ReviewWatcher:
|
|
|
8426
8430
|
self.config.janitor_pressure_grace_minutes * 60 if pressed else None
|
|
8427
8431
|
),
|
|
8428
8432
|
under_pressure=self._volume_pressed if pressed else None,
|
|
8433
|
+
stale_round_seconds=STALE_ROUND_SECONDS if backstop else None,
|
|
8434
|
+
sizes=self._janitor_sizes,
|
|
8429
8435
|
)
|
|
8436
|
+
if result.backstop:
|
|
8437
|
+
log.warning(
|
|
8438
|
+
"janitor: volume at or above %d%% — hub-wide holds are void for "
|
|
8439
|
+
"checkouts older than %d minute(s); %d released; %s held%s",
|
|
8440
|
+
int(janitor_mod.BACKSTOP_PCT), STALE_ROUND_SECONDS // 60,
|
|
8441
|
+
result.backstop_released, janitor_mod.human(result.held_total),
|
|
8442
|
+
janitor_mod.held_reasons(result.top_held()),
|
|
8443
|
+
)
|
|
8430
8444
|
if result.pressure:
|
|
8431
8445
|
log.info(
|
|
8432
8446
|
"janitor: volume at or above %d%% — the grace dropped to %d "
|
|
@@ -8439,20 +8453,22 @@ class ReviewWatcher:
|
|
|
8439
8453
|
if self.config.dry_run:
|
|
8440
8454
|
log.info(
|
|
8441
8455
|
"[dry-run] janitor: would remove %d folder(s) (%s) across %d "
|
|
8442
|
-
"hub(s); %d held by a live round, %d inside the grace, %d "
|
|
8443
|
-
"deferred to the next interval",
|
|
8456
|
+
"hub(s); %d held by a live round (%s), %d inside the grace, %d "
|
|
8457
|
+
"deferred to the next interval%s",
|
|
8444
8458
|
len(result.would_remove),
|
|
8445
8459
|
janitor_mod.human(sum(size for _, size in result.would_remove)),
|
|
8446
|
-
result.hubs, len(result.held),
|
|
8460
|
+
result.hubs, len(result.held), janitor_mod.human(result.held_total),
|
|
8461
|
+
result.fresh, result.deferred, janitor_mod.held_summary(result),
|
|
8447
8462
|
)
|
|
8448
8463
|
else:
|
|
8449
8464
|
log.info(
|
|
8450
8465
|
"janitor: removed %d folder(s), freed %s across %d hub(s); "
|
|
8451
|
-
"%d held by a live round, %d inside the grace, %d failed, %d "
|
|
8452
|
-
"deferred to the next interval",
|
|
8466
|
+
"%d held by a live round (%s), %d inside the grace, %d failed, %d "
|
|
8467
|
+
"deferred to the next interval%s",
|
|
8453
8468
|
len(result.removed), janitor_mod.human(result.freed),
|
|
8454
|
-
result.hubs, len(result.held),
|
|
8455
|
-
result.deferred,
|
|
8469
|
+
result.hubs, len(result.held), janitor_mod.human(result.held_total),
|
|
8470
|
+
result.fresh, len(result.failed), result.deferred,
|
|
8471
|
+
janitor_mod.held_summary(result),
|
|
8456
8472
|
)
|
|
8457
8473
|
return result
|
|
8458
8474
|
|
|
@@ -8465,7 +8481,11 @@ class ReviewWatcher:
|
|
|
8465
8481
|
Open rounds are the ledger's own definition (`in_flight_repos`): a
|
|
8466
8482
|
spawn younger than the stale-round window whose session was not
|
|
8467
8483
|
reaped, and a verdict post still owed. The roster is the pass's
|
|
8468
|
-
post-reap listing when the sweep produced one.
|
|
8484
|
+
post-reap listing when the sweep produced one.
|
|
8485
|
+
|
|
8486
|
+
Every hold carries its round's spawn time (issue #185), so a round
|
|
8487
|
+
whose review-task ref is not known yet holds only the checkouts in
|
|
8488
|
+
its hub made since it spawned."""
|
|
8469
8489
|
sessions = self._pass_roster
|
|
8470
8490
|
if sessions is None:
|
|
8471
8491
|
try:
|
|
@@ -8480,17 +8500,22 @@ class ReviewWatcher:
|
|
|
8480
8500
|
continue
|
|
8481
8501
|
row = self.state.find_spawn_by_session(ses.name)
|
|
8482
8502
|
if row is not None:
|
|
8483
|
-
guard.hold(
|
|
8503
|
+
guard.hold(
|
|
8504
|
+
str(row["repo"]), int(row["number"]), row["task_ref"],
|
|
8505
|
+
since=row["spawned_at"],
|
|
8506
|
+
)
|
|
8484
8507
|
else:
|
|
8485
8508
|
# No ledger row (a skill-shaped `review-pr-<n>`): the name is
|
|
8486
8509
|
# the only evidence, and a name without a repo holds its PR
|
|
8487
|
-
# number in every hub.
|
|
8510
|
+
# number in every hub. The roster reports no start time, so
|
|
8511
|
+
# its hold on the hub's checkouts stays unbounded (issue
|
|
8512
|
+
# #185); the >=95% backstop is what reaches past it.
|
|
8488
8513
|
guard.hold(ref.repo, ref.number)
|
|
8489
8514
|
for spawn_row in self.state.read_spawns(since=now - STALE_ROUND_SECONDS):
|
|
8490
8515
|
if not self.state.is_reaped(str(spawn_row["session"])):
|
|
8491
8516
|
guard.hold(
|
|
8492
8517
|
str(spawn_row["repo"]), int(spawn_row["number"]),
|
|
8493
|
-
spawn_row["task_ref"],
|
|
8518
|
+
spawn_row["task_ref"], since=spawn_row["spawned_at"],
|
|
8494
8519
|
)
|
|
8495
8520
|
for post in self.state.read_verdict_posts():
|
|
8496
8521
|
if post["posted_at"] is None and post["abandoned_at"] is None:
|
|
@@ -8500,6 +8525,7 @@ class ReviewWatcher:
|
|
|
8500
8525
|
guard.hold(
|
|
8501
8526
|
str(post["repo"]), int(post["number"]),
|
|
8502
8527
|
spawn["task_ref"] if spawn is not None else None,
|
|
8528
|
+
since=spawn["spawned_at"] if spawn is not None else None,
|
|
8503
8529
|
)
|
|
8504
8530
|
return guard
|
|
8505
8531
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.24
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alissa-tools-github-revloop
|
|
3
|
-
Version: 0.31.
|
|
3
|
+
Version: 0.31.24
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-REVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -58,6 +58,19 @@ flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
|
58
58
|
the skip off: every discovery PR is then reviewed like any other. See
|
|
59
59
|
*Discovery PRs* in the repository README.
|
|
60
60
|
|
|
61
|
+
## Integration-check PRs are not reviewed
|
|
62
|
+
|
|
63
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
64
|
+
body carries `<!-- alissa-integration-check v1 -->` on a line of its own and
|
|
65
|
+
whose every changed path is under `integration-check/` gets **no review
|
|
66
|
+
round** either — the same skip, beside the discovery one, with one
|
|
67
|
+
activity-comment line per head. There is no light round. A marked PR that
|
|
68
|
+
touches anything else (or whose file list cannot be read in full) is reviewed
|
|
69
|
+
under the code directive, unchanged. The fleet flag `integration_check_review`
|
|
70
|
+
(`ALISSA_REVIEW_INTEGRATION_CHECK_REVIEW`, default off) turns the skip off:
|
|
71
|
+
every integration-check PR is then reviewed like any other. The two flags are
|
|
72
|
+
independent. See *Integration-check PRs* in the repository README.
|
|
73
|
+
|
|
61
74
|
## Install
|
|
62
75
|
|
|
63
76
|
```sh
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.22
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.22 → alissa_tools_github_revloop-0.31.24}/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|