alissa-tools-github-reviewloop 0.5.0__tar.gz → 0.7.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.
- {alissa_tools_github_reviewloop-0.5.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.7.0}/PKG-INFO +1 -1
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/alissa.py +42 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/loop.py +48 -10
- alissa_tools_github_reviewloop-0.7.0/src/main/alissa/tools/github/reviewloop/version +1 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_reviewloop-0.5.0/src/main/alissa/tools/github/reviewloop/version +0 -1
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/MANIFEST.in +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/README.md +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/requirements.txt +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/setup.cfg +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/setup.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/state.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/top_level.txt +0 -0
|
@@ -154,6 +154,48 @@ class Alissa:
|
|
|
154
154
|
# lost its timestamp.
|
|
155
155
|
return max(found, key=lambda pair: pair[0])[1]
|
|
156
156
|
|
|
157
|
+
def count_verdicts(self, task_ref: str) -> int:
|
|
158
|
+
"""How many CR6 verdict envelopes are on the review task.
|
|
159
|
+
|
|
160
|
+
One envelope per completed round (CR7, append-only), so this is the
|
|
161
|
+
authoritative round count -- unlike the GitHub review count it cannot be
|
|
162
|
+
thrown off by an empty-bodied review or two reviews in one cycle. Never
|
|
163
|
+
raises: absent, empty, or malformed evidence degrades to 0 (round 1)
|
|
164
|
+
rather than taking the loop down.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
data = run_json(["alissa", "task", "get", task_ref, "--json"], timeout=90)
|
|
168
|
+
except CommandError as exc:
|
|
169
|
+
log.warning("could not read verdict evidence for %s: %s", task_ref, exc)
|
|
170
|
+
return 0
|
|
171
|
+
except Exception: # pragma: no cover - defence in depth
|
|
172
|
+
log.exception("unexpected failure reading verdict evidence for %s", task_ref)
|
|
173
|
+
return 0
|
|
174
|
+
try:
|
|
175
|
+
return self._count_verdicts(data)
|
|
176
|
+
except Exception: # pragma: no cover - defence in depth
|
|
177
|
+
log.exception("could not count verdict evidence for %s", task_ref)
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _count_verdicts(payload: object) -> int:
|
|
182
|
+
"""Count evidence items carrying a verdict envelope. Mirrors
|
|
183
|
+
`_newest_verdict`'s tolerant parsing -- one match per item at most."""
|
|
184
|
+
if not isinstance(payload, dict):
|
|
185
|
+
return 0
|
|
186
|
+
evidence = payload.get("evidence")
|
|
187
|
+
if not isinstance(evidence, list):
|
|
188
|
+
return 0
|
|
189
|
+
count = 0
|
|
190
|
+
for item in evidence:
|
|
191
|
+
if not isinstance(item, dict):
|
|
192
|
+
continue
|
|
193
|
+
for blob in (item.get("title"), item.get("markdownContent")):
|
|
194
|
+
if isinstance(blob, str) and _VERDICT_RE.search(blob):
|
|
195
|
+
count += 1
|
|
196
|
+
break
|
|
197
|
+
return count
|
|
198
|
+
|
|
157
199
|
def enqueue_reviewer(
|
|
158
200
|
self,
|
|
159
201
|
*,
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import re
|
|
15
|
+
import secrets
|
|
15
16
|
import time
|
|
16
17
|
from dataclasses import dataclass
|
|
17
18
|
from enum import Enum
|
|
@@ -105,9 +106,17 @@ class Decision:
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
def session_name(pr: PullRequest, round_: int) -> str:
|
|
108
|
-
"""tmux-safe, unique per
|
|
109
|
+
"""A tmux-safe reviewer session name, unique per spawn.
|
|
110
|
+
|
|
111
|
+
The `review-<repo>-pr<n>-r<round>` prefix stays human-readable, but a short
|
|
112
|
+
random nonce is appended so a re-used or miscounted round number can never
|
|
113
|
+
collide with a still-live session (a collision wedges the worker -- the
|
|
114
|
+
original 'stuck' failure). Safe to be non-deterministic: the generated name
|
|
115
|
+
is recorded in the spawn ledger and is what gets reaped / self-killed, so the
|
|
116
|
+
daemon never re-derives it.
|
|
117
|
+
"""
|
|
109
118
|
repo = re.sub(r"[^A-Za-z0-9-]", "-", pr.repo).strip("-").lower()
|
|
110
|
-
return f"review-{repo}-pr{pr.number}-r{round_}"
|
|
119
|
+
return f"review-{repo}-pr{pr.number}-r{round_}-{secrets.token_hex(3)}"
|
|
111
120
|
|
|
112
121
|
|
|
113
122
|
class ReviewWatcher:
|
|
@@ -143,18 +152,27 @@ class ReviewWatcher:
|
|
|
143
152
|
)
|
|
144
153
|
|
|
145
154
|
my_reviews = self.github.my_reviews(owner, repo, number)
|
|
146
|
-
|
|
155
|
+
|
|
156
|
+
# The review task (CR2) is the round record: one verdict envelope per
|
|
157
|
+
# round (CR7), so counting envelopes is the authoritative "rounds
|
|
158
|
+
# completed" -- immune to the GitHub heuristics that drift. A round whose
|
|
159
|
+
# review has an empty body undercounts (round_ repeats -> the session name
|
|
160
|
+
# collides -> the worker wedges); two reviews in one cycle overcount.
|
|
161
|
+
# Fall back to the substantive-review count only before the review task
|
|
162
|
+
# exists (round 1). Looked up here (not in _spawn) because both the count
|
|
163
|
+
# and convergence need it.
|
|
164
|
+
task = self.alissa.find_review_task(owner, repo, number)
|
|
165
|
+
completed = (
|
|
166
|
+
self.alissa.count_verdicts(task.ref) if task is not None
|
|
167
|
+
else len(my_reviews)
|
|
168
|
+
)
|
|
147
169
|
|
|
148
170
|
# Backstop: reap the sessions of rounds that have already landed a review
|
|
149
171
|
# (the fast path is the reviewer self-killing, but it may forget). A round
|
|
150
172
|
# <= completed is done; its session, if we still have it, can be killed.
|
|
151
173
|
self._reap_finished(pr.full_name, number, completed)
|
|
152
174
|
|
|
153
|
-
|
|
154
|
-
# too. _spawn still handles `task is None` exactly as before.
|
|
155
|
-
task = self.alissa.find_review_task(owner, repo, number)
|
|
156
|
-
|
|
157
|
-
converged = self._convergence_reason(my_reviews, task)
|
|
175
|
+
converged = self._convergence_reason(my_reviews, task, pr.head_sha)
|
|
158
176
|
if converged is not None:
|
|
159
177
|
return Decision(Action.CONVERGED, converged, completed)
|
|
160
178
|
|
|
@@ -181,7 +199,9 @@ class ReviewWatcher:
|
|
|
181
199
|
|
|
182
200
|
return self._spawn(pr, round_, task)
|
|
183
201
|
|
|
184
|
-
def _convergence_reason(
|
|
202
|
+
def _convergence_reason(
|
|
203
|
+
self, my_reviews: list[Review], task: Task | None, head_sha: str
|
|
204
|
+
) -> str | None:
|
|
185
205
|
"""Why the loop is done, or None if it is not.
|
|
186
206
|
|
|
187
207
|
Two independent signals, because neither alone is sufficient:
|
|
@@ -195,8 +215,26 @@ class ReviewWatcher:
|
|
|
195
215
|
declares this the verdict of record, and unlike the GitHub state it
|
|
196
216
|
can actually express approval, so it is the signal that closes the
|
|
197
217
|
loop in practice.
|
|
218
|
+
|
|
219
|
+
BOTH are bound to the current head. An approval means "this code is
|
|
220
|
+
good", so once the implementer pushes past the reviewed commit it is
|
|
221
|
+
about old code and the next round is owed. Without this bind a stale
|
|
222
|
+
approve latches the loop shut forever -- #227: round 1 approved
|
|
223
|
+
`fa304de`, the implementer pushed `fd500fc` and re-requested (and even
|
|
224
|
+
dismissed the approve), yet the envelope still read approve and no round
|
|
225
|
+
2 was ever queued.
|
|
198
226
|
"""
|
|
199
|
-
if
|
|
227
|
+
if not my_reviews:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# New commits since the newest review -> its verdict is about old code.
|
|
231
|
+
# A falsy commit_id (older records lack one) can't be checked, so it
|
|
232
|
+
# falls through rather than blocking convergence.
|
|
233
|
+
newest = my_reviews[-1]
|
|
234
|
+
if newest.commit_id and newest.commit_id != head_sha:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
if newest.state == "APPROVED":
|
|
200
238
|
return "last GitHub review state is APPROVED"
|
|
201
239
|
|
|
202
240
|
# Only checkable once a review task exists; before that there is
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.7.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.5.0
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/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
|