alissa-tools-github-reviewloop 0.4.0__tar.gz → 0.5.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.4.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.5.0}/PKG-INFO +1 -1
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/alissa.py +21 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/loop.py +46 -4
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/state.py +18 -0
- alissa_tools_github_reviewloop-0.5.0/src/main/alissa/tools/github/reviewloop/version +1 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_reviewloop-0.4.0/src/main/alissa/tools/github/reviewloop/version +0 -1
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/MANIFEST.in +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/README.md +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/requirements.txt +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/setup.cfg +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/setup.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.0}/src/main/alissa_tools_github_reviewloop.egg-info/top_level.txt +0 -0
|
@@ -185,6 +185,27 @@ class Alissa:
|
|
|
185
185
|
|
|
186
186
|
run(argv, timeout=60)
|
|
187
187
|
|
|
188
|
+
# Reviewers are one-shot per round (CR3): once the session finishes and is
|
|
189
|
+
# reaped, it must never be respawned. Make that explicit so a self-kill or
|
|
190
|
+
# a daemon reap can't trigger a respawn loop. Best-effort — an older CLI
|
|
191
|
+
# without `queue set` should not fail the enqueue.
|
|
192
|
+
try:
|
|
193
|
+
run(["alissa", "tmux", "queue", "set", session, "respawn", "off"],
|
|
194
|
+
timeout=30, check=False)
|
|
195
|
+
except CommandError: # pragma: no cover - defence in depth
|
|
196
|
+
log.warning("could not set respawn off for %s", session)
|
|
197
|
+
|
|
198
|
+
def kill_session(self, session: str, *, dry_run: bool = False) -> None:
|
|
199
|
+
"""Kill a finished reviewer's managed session to free its worker slot.
|
|
200
|
+
|
|
201
|
+
Best-effort and idempotent-friendly: the session may already be gone (the
|
|
202
|
+
reviewer self-killed), so a non-zero exit is not an error here.
|
|
203
|
+
"""
|
|
204
|
+
if dry_run:
|
|
205
|
+
log.info("[dry-run] would kill session %s", session)
|
|
206
|
+
return
|
|
207
|
+
run(["alissa", "tmux", "kill", session], timeout=30, check=False)
|
|
208
|
+
|
|
188
209
|
def add_repo_to_workspace(
|
|
189
210
|
self, owner: str, repo: str, workspace_root: Path, *, dry_run: bool = False
|
|
190
211
|
) -> None:
|
|
@@ -43,6 +43,16 @@ _CLOSE_THE_ROUND = (
|
|
|
43
43
|
"request_changes. "
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
+
# Reviewers are one-shot per round (CR3), so a finished session should not linger
|
|
47
|
+
# holding a worker slot. The daemon reaps it as a backstop, but the fast path is
|
|
48
|
+
# the reviewer releasing its own slot as its very last action. {session} is the
|
|
49
|
+
# reviewer's own managed session name, injected at spawn.
|
|
50
|
+
_RELEASE_SLOT = (
|
|
51
|
+
"FINALLY, and only once the round is fully closed above (review registered "
|
|
52
|
+
"AND verdict recorded), release your worker slot as your last action: run "
|
|
53
|
+
"`alissa tmux kill {session}`. Do nothing after it."
|
|
54
|
+
)
|
|
55
|
+
|
|
46
56
|
ROUND_1_DIRECTIVE = (
|
|
47
57
|
"You are a PR REVIEWER, not an implementer. {assignment} "
|
|
48
58
|
"Load the alissa-code-review skill and follow procedures/review-a-pr.md: "
|
|
@@ -51,7 +61,8 @@ ROUND_1_DIRECTIVE = (
|
|
|
51
61
|
"move the task to pending_validation. "
|
|
52
62
|
+ _CLOSE_THE_ROUND +
|
|
53
63
|
"NEVER push commits, merge, or change PR state. "
|
|
54
|
-
"Do NOT create further ali-* sessions."
|
|
64
|
+
"Do NOT create further ali-* sessions. "
|
|
65
|
+
+ _RELEASE_SLOT
|
|
55
66
|
)
|
|
56
67
|
|
|
57
68
|
ROUND_K_DIRECTIVE = (
|
|
@@ -63,7 +74,8 @@ ROUND_K_DIRECTIVE = (
|
|
|
63
74
|
"round-{round} verdict envelope, move the task to pending_validation. "
|
|
64
75
|
+ _CLOSE_THE_ROUND +
|
|
65
76
|
"NEVER push commits, merge, or change PR state. "
|
|
66
|
-
"Do NOT create further ali-* sessions."
|
|
77
|
+
"Do NOT create further ali-* sessions. "
|
|
78
|
+
+ _RELEASE_SLOT
|
|
67
79
|
)
|
|
68
80
|
|
|
69
81
|
ESCALATION_COMMENT = (
|
|
@@ -133,6 +145,11 @@ class ReviewWatcher:
|
|
|
133
145
|
my_reviews = self.github.my_reviews(owner, repo, number)
|
|
134
146
|
completed = len(my_reviews)
|
|
135
147
|
|
|
148
|
+
# Backstop: reap the sessions of rounds that have already landed a review
|
|
149
|
+
# (the fast path is the reviewer self-killing, but it may forget). A round
|
|
150
|
+
# <= completed is done; its session, if we still have it, can be killed.
|
|
151
|
+
self._reap_finished(pr.full_name, number, completed)
|
|
152
|
+
|
|
136
153
|
# Looked up here rather than inside _spawn: convergence needs the ref
|
|
137
154
|
# too. _spawn still handles `task is None` exactly as before.
|
|
138
155
|
task = self.alissa.find_review_task(owner, repo, number)
|
|
@@ -189,6 +206,31 @@ class ReviewWatcher:
|
|
|
189
206
|
|
|
190
207
|
return None
|
|
191
208
|
|
|
209
|
+
def _reap_finished(self, repo: str, number: int, completed: int) -> None:
|
|
210
|
+
"""Kill the managed session of every round whose review has landed.
|
|
211
|
+
|
|
212
|
+
A round `<= completed` is done (its review was submitted), so its session
|
|
213
|
+
is finished and should not hold a worker slot. The reviewer self-kills as
|
|
214
|
+
the fast path; this is the backstop for when it forgets. Idempotent — each
|
|
215
|
+
session is killed at most once (recorded in the ledger) — never runs in
|
|
216
|
+
dry-run, and best-effort: a reap failure (or an already-gone session) must
|
|
217
|
+
not take the poll down.
|
|
218
|
+
"""
|
|
219
|
+
if self.config.dry_run or completed < 1:
|
|
220
|
+
return
|
|
221
|
+
for round_ in range(1, completed + 1):
|
|
222
|
+
row = self.state.get_spawn(repo, number, round_)
|
|
223
|
+
if row is None or self.state.is_reaped(row["session"]):
|
|
224
|
+
continue
|
|
225
|
+
session = row["session"]
|
|
226
|
+
try:
|
|
227
|
+
self.alissa.kill_session(session)
|
|
228
|
+
except Exception: # pragma: no cover - defence in depth
|
|
229
|
+
log.exception("failed to reap session %s", session)
|
|
230
|
+
continue
|
|
231
|
+
self.state.record_reap(session)
|
|
232
|
+
log.info("reaped finished reviewer session %s (round %d done)", session, round_)
|
|
233
|
+
|
|
192
234
|
# -- actions -----------------------------------------------------------
|
|
193
235
|
|
|
194
236
|
def _spawn(self, pr: PullRequest, round_: int, task: Task | None) -> Decision:
|
|
@@ -210,16 +252,16 @@ class ReviewWatcher:
|
|
|
210
252
|
else:
|
|
211
253
|
assignment = f"You've been assigned Alissa review task {task.ref}."
|
|
212
254
|
|
|
255
|
+
name = session_name(pr, round_)
|
|
213
256
|
template = ROUND_1_DIRECTIVE if round_ == 1 else ROUND_K_DIRECTIVE
|
|
214
257
|
directive = template.format(
|
|
215
|
-
assignment=assignment, round=round_, cap=self.config.round_cap
|
|
258
|
+
assignment=assignment, round=round_, cap=self.config.round_cap, session=name
|
|
216
259
|
)
|
|
217
260
|
|
|
218
261
|
hub, problem = self._ensure_hub(pr)
|
|
219
262
|
if problem is not None:
|
|
220
263
|
return Decision(Action.SKIPPED, problem, round_)
|
|
221
264
|
|
|
222
|
-
name = session_name(pr, round_)
|
|
223
265
|
self.alissa.enqueue_reviewer(
|
|
224
266
|
session=name,
|
|
225
267
|
directive=directive,
|
|
@@ -31,6 +31,11 @@ CREATE TABLE IF NOT EXISTS escalations (
|
|
|
31
31
|
escalated_at INTEGER NOT NULL,
|
|
32
32
|
PRIMARY KEY (repo, number, head_sha)
|
|
33
33
|
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS reaps (
|
|
36
|
+
session TEXT NOT NULL PRIMARY KEY,
|
|
37
|
+
reaped_at INTEGER NOT NULL
|
|
38
|
+
);
|
|
34
39
|
"""
|
|
35
40
|
|
|
36
41
|
|
|
@@ -81,6 +86,19 @@ class State:
|
|
|
81
86
|
)
|
|
82
87
|
self._db.commit()
|
|
83
88
|
|
|
89
|
+
def is_reaped(self, session: str) -> bool:
|
|
90
|
+
row = self._db.execute(
|
|
91
|
+
"SELECT 1 FROM reaps WHERE session=?", (session,)
|
|
92
|
+
).fetchone()
|
|
93
|
+
return row is not None
|
|
94
|
+
|
|
95
|
+
def record_reap(self, session: str) -> None:
|
|
96
|
+
self._db.execute(
|
|
97
|
+
"INSERT OR REPLACE INTO reaps (session, reaped_at) VALUES (?,?)",
|
|
98
|
+
(session, int(time.time())),
|
|
99
|
+
)
|
|
100
|
+
self._db.commit()
|
|
101
|
+
|
|
84
102
|
def escalated(self, repo: str, number: int, head_sha: str) -> bool:
|
|
85
103
|
row = self._db.execute(
|
|
86
104
|
"SELECT 1 FROM escalations WHERE repo=? AND number=? AND head_sha=?",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.4.0
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_reviewloop-0.4.0 → alissa_tools_github_reviewloop-0.5.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
|