alissa-tools-github-revloop 0.16.7__tar.gz → 0.16.8__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.16.7/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.16.8}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/loop.py +36 -15
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/state.py +81 -21
- alissa_tools_github_revloop-0.16.8/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.16.7/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/README.md +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/setup.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.16.7 → alissa_tools_github_revloop-0.16.8}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -181,8 +181,9 @@ CHECKS_UNSETTLED_LEAD = (
|
|
|
181
181
|
"concluded.**\n\n"
|
|
182
182
|
"{detail}\n\n"
|
|
183
183
|
"This round's verdict was held for {waited} min waiting for the head's "
|
|
184
|
-
"checks to settle ({bound} min bound) and they did not, so it is
|
|
185
|
-
"as a comment: an approve would claim a head this loop never saw go
|
|
184
|
+
"checks to settle ({bound} min bound){total_note} and they did not, so it is "
|
|
185
|
+
"recorded as a comment: an approve would claim a head this loop never saw go "
|
|
186
|
+
"green. "
|
|
186
187
|
"Nothing about the review itself changed — the verdict below is the round's "
|
|
187
188
|
"own.\n\n"
|
|
188
189
|
"Submitting this review consumes the pending review request, so the daemon "
|
|
@@ -221,6 +222,13 @@ CHECKS_UNSETTLED_PAGE = (
|
|
|
221
222
|
"No label was touched and no further round is queued."
|
|
222
223
|
)
|
|
223
224
|
|
|
225
|
+
# Appended to `{bound}` above only when the hold was PROMOTED -- an unreadable
|
|
226
|
+
# wait that became a genuine pending one restarts the clock, so the bound the
|
|
227
|
+
# operator configured applies per condition and the round can be held up to
|
|
228
|
+
# twice it. Saying "held 30 min (30 min bound)" after 60 real minutes is the
|
|
229
|
+
# report being wrong about the one number an operator tunes.
|
|
230
|
+
CHECKS_TOTAL_HELD = ", {total} min in total across both waits,"
|
|
231
|
+
|
|
224
232
|
# The `{detail}` above, per reason the rollup did not settle.
|
|
225
233
|
CHECKS_STILL_RUNNING = "Still running at the bound: {names}."
|
|
226
234
|
CHECKS_UNREADABLE = (
|
|
@@ -1165,22 +1173,29 @@ class ReviewWatcher:
|
|
|
1165
1173
|
# never "restart whenever the state changes" -- a reader flapping between
|
|
1166
1174
|
# the two would then push the bound out forever, which is precisely the
|
|
1167
1175
|
# unbounded hold this bound exists to prevent.
|
|
1168
|
-
|
|
1169
|
-
promoted =
|
|
1170
|
-
if
|
|
1171
|
-
|
|
1176
|
+
hold = self.state.checks_hold(pr.full_name, pr.number, round_)
|
|
1177
|
+
promoted = hold.condition == CHECKS_UNKNOWN and rollup.state == CHECKS_PENDING
|
|
1178
|
+
if hold.since is None or promoted:
|
|
1179
|
+
self.state.record_checks_hold(
|
|
1172
1180
|
pr.full_name, pr.number, round_, rollup.state
|
|
1173
1181
|
)
|
|
1174
|
-
|
|
1182
|
+
hold = self.state.checks_hold(pr.full_name, pr.number, round_)
|
|
1183
|
+
# Two numbers, both reported: `waited` is the wait THIS condition has had
|
|
1184
|
+
# and is what the bound applies to; `held` is how long the round has been
|
|
1185
|
+
# held at all. They differ by up to a full bound once a hold has been
|
|
1186
|
+
# promoted, so a report that shows only the first tells an operator who
|
|
1187
|
+
# set 30 minutes that a 60-minute hold waited 30.
|
|
1188
|
+
waited = max(time.time() - (hold.since or time.time()), 0)
|
|
1189
|
+
held = max(time.time() - (hold.first_at or time.time()), 0)
|
|
1175
1190
|
bound = self.config.checks_wait_seconds
|
|
1176
1191
|
if waited < bound:
|
|
1177
1192
|
# One line per poll, one activity note per held round; see
|
|
1178
1193
|
# checks_hold_kind.
|
|
1179
1194
|
log.info(
|
|
1180
|
-
"%s round %d: holding its %s — CI rollup at %s is %s (%dm
|
|
1181
|
-
"%dm bound
|
|
1195
|
+
"%s round %d: holding its %s — CI rollup at %s is %s (%dm on this "
|
|
1196
|
+
"condition of the %dm bound; %dm held in total)",
|
|
1182
1197
|
pr.slug, round_, VERDICT_APPROVE, judged[:8], rollup.summary,
|
|
1183
|
-
waited // 60, bound // 60,
|
|
1198
|
+
waited // 60, bound // 60, held // 60,
|
|
1184
1199
|
)
|
|
1185
1200
|
self._note_checks_hold(pr, round_, judged, rollup)
|
|
1186
1201
|
return ChecksGate(
|
|
@@ -1188,7 +1203,8 @@ class ReviewWatcher:
|
|
|
1188
1203
|
Action.AWAITING_POST,
|
|
1189
1204
|
f"round {round_} holds its {VERDICT_APPROVE} — the CI rollup "
|
|
1190
1205
|
f"at {judged[:8]} is {rollup.summary}; "
|
|
1191
|
-
f"{int((bound - waited) // 60)}m of the wait bound left"
|
|
1206
|
+
f"{int((bound - waited) // 60)}m of the wait bound left "
|
|
1207
|
+
f"({int(held // 60)}m held in total)",
|
|
1192
1208
|
round_,
|
|
1193
1209
|
task_ref=task.ref,
|
|
1194
1210
|
),
|
|
@@ -1196,10 +1212,10 @@ class ReviewWatcher:
|
|
|
1196
1212
|
)
|
|
1197
1213
|
|
|
1198
1214
|
log.warning(
|
|
1199
|
-
"%s round %d: the CI rollup at %s is still %s after %dm
|
|
1200
|
-
"the %s envelope as a %s
|
|
1201
|
-
"head",
|
|
1202
|
-
pr.slug, round_, judged[:8], rollup.summary, waited // 60,
|
|
1215
|
+
"%s round %d: the CI rollup at %s is still %s after %dm on this "
|
|
1216
|
+
"condition (%dm held in total) — recording the %s envelope as a %s "
|
|
1217
|
+
"review, never an APPROVE on an unverified head",
|
|
1218
|
+
pr.slug, round_, judged[:8], rollup.summary, waited // 60, held // 60,
|
|
1203
1219
|
VERDICT_APPROVE, EVENT_COMMENT,
|
|
1204
1220
|
)
|
|
1205
1221
|
detail = (
|
|
@@ -1216,6 +1232,11 @@ class ReviewWatcher:
|
|
|
1216
1232
|
detail=detail,
|
|
1217
1233
|
waited=int(waited // 60),
|
|
1218
1234
|
bound=int(bound // 60),
|
|
1235
|
+
total_note=(
|
|
1236
|
+
CHECKS_TOTAL_HELD.format(total=int(held // 60))
|
|
1237
|
+
if hold.promoted
|
|
1238
|
+
else ""
|
|
1239
|
+
),
|
|
1219
1240
|
),
|
|
1220
1241
|
state=rollup.state,
|
|
1221
1242
|
detail=detail,
|
|
@@ -29,6 +29,7 @@ import json
|
|
|
29
29
|
import sqlite3
|
|
30
30
|
import time
|
|
31
31
|
from collections.abc import Iterable
|
|
32
|
+
from dataclasses import dataclass
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
|
|
34
35
|
# Poll-snapshot retention: the newest N rows are kept, older ones pruned on
|
|
@@ -108,10 +109,9 @@ CREATE TABLE IF NOT EXISTS verdict_posts (
|
|
|
108
109
|
-- then released rather than held open forever; see loop._abandon_verdict.
|
|
109
110
|
abandoned_at INTEGER,
|
|
110
111
|
-- When this round's APPROVE was FIRST held back because the judged head's
|
|
111
|
-
-- CI rollup had not settled
|
|
112
|
-
--
|
|
113
|
-
--
|
|
114
|
-
-- poll happened to land. NULL means the gate never held this round.
|
|
112
|
+
-- CI rollup had not settled -- and it is never overwritten afterwards, so
|
|
113
|
+
-- "how long has this round really been held?" always has an answer. NULL
|
|
114
|
+
-- means the gate never held this round.
|
|
115
115
|
checks_held_at INTEGER,
|
|
116
116
|
-- WHICH unsettled condition that stamp belongs to ('pending' -- checks are
|
|
117
117
|
-- genuinely running -- or 'unknown' -- the rollup could not be read). The
|
|
@@ -121,6 +121,13 @@ CREATE TABLE IF NOT EXISTS verdict_posts (
|
|
|
121
121
|
-- 'pending' one. The policy lives in loop._gate_on_checks; this column is
|
|
122
122
|
-- what lets it be decided from the ledger instead of from memory.
|
|
123
123
|
checks_held_state TEXT,
|
|
124
|
+
-- When that promotion happened: the stamp the bound is measured from once
|
|
125
|
+
-- the wait is on checks that are genuinely running. Separate from
|
|
126
|
+
-- `checks_held_at` rather than replacing it, because the two answer
|
|
127
|
+
-- different questions and a report that conflates them says a promoted hold
|
|
128
|
+
-- waited one bound when it waited two. NULL until (and unless) the promotion
|
|
129
|
+
-- happens; the bound then reads `checks_pending_at or checks_held_at`.
|
|
130
|
+
checks_pending_at INTEGER,
|
|
124
131
|
review_url TEXT,
|
|
125
132
|
last_error TEXT,
|
|
126
133
|
PRIMARY KEY (repo, number, round)
|
|
@@ -163,10 +170,42 @@ _ADDED_COLUMNS = {
|
|
|
163
170
|
"verdict_posts": (
|
|
164
171
|
("checks_held_at", "INTEGER"),
|
|
165
172
|
("checks_held_state", "TEXT"),
|
|
173
|
+
("checks_pending_at", "INTEGER"),
|
|
166
174
|
),
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class ChecksHold:
|
|
180
|
+
"""One round's CI hold, as the ledger remembers it.
|
|
181
|
+
|
|
182
|
+
Two stamps, because the gate has two honest numbers to report and they can
|
|
183
|
+
differ by a whole wait bound:
|
|
184
|
+
|
|
185
|
+
* `first_at` -- when the round was first held at all, on whatever condition;
|
|
186
|
+
* `pending_at` -- when an unreadable hold was promoted to a genuinely
|
|
187
|
+
pending one, which restarts the clock the bound is measured from (see
|
|
188
|
+
loop._gate_on_checks for why exactly once).
|
|
189
|
+
|
|
190
|
+
`since` is the one the bound uses. `first_at` is the one an operator means by
|
|
191
|
+
"how long has this been held?", and reporting only `since` after a promotion
|
|
192
|
+
understates it by up to the full bound.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
first_at: int | None = None
|
|
196
|
+
condition: str | None = None
|
|
197
|
+
pending_at: int | None = None
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def since(self) -> int | None:
|
|
201
|
+
"""The stamp the wait bound is measured from."""
|
|
202
|
+
return self.pending_at or self.first_at
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def promoted(self) -> bool:
|
|
206
|
+
return self.pending_at is not None
|
|
207
|
+
|
|
208
|
+
|
|
170
209
|
class State:
|
|
171
210
|
def __init__(self, path: Path, *, read_only: bool = False):
|
|
172
211
|
"""Open the ledger. The daemon opens it read-write (creating the file,
|
|
@@ -509,38 +548,59 @@ class State:
|
|
|
509
548
|
row = self.get_verdict_post(repo, number, round_)
|
|
510
549
|
return int(row["attempts"]) if row else 0
|
|
511
550
|
|
|
512
|
-
def checks_hold(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
condition -- (None, None) if it has never been held.
|
|
551
|
+
def checks_hold(self, repo: str, number: int, round_: int) -> "ChecksHold":
|
|
552
|
+
"""This round's CI hold: when it began, what it is waiting on, and when
|
|
553
|
+
that became a genuine `pending` -- an all-None ChecksHold if it has never
|
|
554
|
+
been held.
|
|
517
555
|
|
|
518
556
|
A read, deliberately: whether an existing stamp still applies is a
|
|
519
557
|
policy question about CI (see loop._gate_on_checks), and this table's job
|
|
520
|
-
is to remember the answer, not to make it.
|
|
558
|
+
is to remember the answer, not to make it. It remembers BOTH stamps
|
|
559
|
+
because the two answer different questions -- `since` bounds the wait,
|
|
560
|
+
`first_at` is how long the round has really been held -- and a report
|
|
561
|
+
that conflates them tells an operator a promoted hold waited 30 minutes
|
|
562
|
+
when it waited 60.
|
|
521
563
|
"""
|
|
522
564
|
row = self.get_verdict_post(repo, number, round_)
|
|
523
565
|
if row is None or not row["checks_held_at"]:
|
|
524
|
-
return
|
|
525
|
-
|
|
566
|
+
return ChecksHold()
|
|
567
|
+
pending_at = row["checks_pending_at"]
|
|
568
|
+
return ChecksHold(
|
|
569
|
+
first_at=int(row["checks_held_at"]),
|
|
570
|
+
condition=(row["checks_held_state"] or None),
|
|
571
|
+
pending_at=int(pending_at) if pending_at else None,
|
|
572
|
+
)
|
|
526
573
|
|
|
527
574
|
def record_checks_hold(
|
|
528
575
|
self, repo: str, number: int, round_: int, condition: str
|
|
529
576
|
) -> int:
|
|
530
|
-
"""
|
|
577
|
+
"""Record that this round is held on `condition`; return the stamp the
|
|
578
|
+
bound is measured from.
|
|
531
579
|
|
|
532
580
|
The caller decides WHEN to call this -- once when the hold begins, and at
|
|
533
581
|
most once more when an unreadable hold is promoted to a genuinely pending
|
|
534
582
|
one, because the bound is defined against the first observation of the
|
|
535
|
-
condition actually being waited on.
|
|
536
|
-
|
|
583
|
+
condition actually being waited on (loop._gate_on_checks owns that rule).
|
|
584
|
+
|
|
585
|
+
The promotion NO LONGER overwrites `checks_held_at`: it fills
|
|
586
|
+
`checks_pending_at` instead, so the ledger keeps when the round was first
|
|
587
|
+
held as well as when its current wait started. Nothing about the bound
|
|
588
|
+
changes; what changes is that the daemon can now say both numbers out
|
|
589
|
+
loud, which the operator-facing report needs.
|
|
537
590
|
"""
|
|
538
591
|
now = int(time.time())
|
|
539
|
-
self.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
592
|
+
if self.checks_hold(repo, number, round_).first_at is None:
|
|
593
|
+
self._db.execute(
|
|
594
|
+
"UPDATE verdict_posts SET checks_held_at = ?, checks_held_state = ? "
|
|
595
|
+
"WHERE repo=? AND number=? AND round=?",
|
|
596
|
+
(now, condition, repo, number, round_),
|
|
597
|
+
)
|
|
598
|
+
else:
|
|
599
|
+
self._db.execute(
|
|
600
|
+
"UPDATE verdict_posts SET checks_pending_at = ?, "
|
|
601
|
+
"checks_held_state = ? WHERE repo=? AND number=? AND round=?",
|
|
602
|
+
(now, condition, repo, number, round_),
|
|
603
|
+
)
|
|
544
604
|
self._db.commit()
|
|
545
605
|
return now
|
|
546
606
|
|
|
@@ -603,7 +663,7 @@ class State:
|
|
|
603
663
|
return self._read_rows(
|
|
604
664
|
"SELECT repo, number, round, first_seen_at, head_sha, attempts, "
|
|
605
665
|
"last_attempt_at, posted_at, abandoned_at, checks_held_at, "
|
|
606
|
-
"checks_held_state, review_url, last_error "
|
|
666
|
+
"checks_held_state, checks_pending_at, review_url, last_error "
|
|
607
667
|
"FROM verdict_posts "
|
|
608
668
|
"ORDER BY first_seen_at DESC, number DESC",
|
|
609
669
|
limit,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.16.8
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.16.7
|
|
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
|