alissa-tools-github-revloop 0.31.13__tar.gz → 0.31.14__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.13/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.14}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/loop.py +5 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/loop_events.py +79 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/state.py +24 -5
- alissa_tools_github_revloop-0.31.14/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.31.13/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -102,10 +102,12 @@ from .loop_events import (
|
|
|
102
102
|
auth_rejected_prefix,
|
|
103
103
|
build_emitter,
|
|
104
104
|
credential_identity,
|
|
105
|
+
pr_closing_issue,
|
|
105
106
|
recorded_round_verdicts,
|
|
106
107
|
round_verdict_kind,
|
|
107
108
|
round_verdict_prefix,
|
|
108
109
|
ROUND_VERDICT_PREFIX,
|
|
110
|
+
task_number_of,
|
|
109
111
|
)
|
|
110
112
|
from .proc import CommandError
|
|
111
113
|
from .state import State
|
|
@@ -6319,6 +6321,9 @@ class ReviewWatcher:
|
|
|
6319
6321
|
head_sha=pr.head_sha,
|
|
6320
6322
|
session=name,
|
|
6321
6323
|
task_ref=task.ref if task else None,
|
|
6324
|
+
# What the PR serves (issue #165), for the round events.
|
|
6325
|
+
issue_number=pr_closing_issue(pr.body, pr.full_name),
|
|
6326
|
+
task_number=task_number_of(pr_origin_task_ref(pr.body)),
|
|
6322
6327
|
)
|
|
6323
6328
|
# Recorded only once the round is actually enqueued, and beside
|
|
6324
6329
|
# the spawn row for the same reason: a notice the reviewer never
|
|
@@ -91,6 +91,17 @@ the envelope's `Contract: v<n>` and the review task's ref. Every
|
|
|
91
91
|
the ledger does not know one, and is keyed on `(repo, pr, round, head)`, so a
|
|
92
92
|
native post's `verdict_posts` row and its ping derive ONE event (the row's,
|
|
93
93
|
with the ping folded in), never two.
|
|
94
|
+
|
|
95
|
+
Every ROUND event (`ROUND_EVENT_KINDS`) also says what the PR serves (issue
|
|
96
|
+
#165), so Studio can link a review to its task: `issueNumber` when the PR
|
|
97
|
+
body CLOSES one issue of the PR's own repo (`pr_closing_issue` -- a mention,
|
|
98
|
+
a cross-repo close or two different closes is no field, never a guess) and
|
|
99
|
+
`taskNumber` when it names its origin task (`Origin task: TASK-<n>`, read by
|
|
100
|
+
loop's `pr_origin_task_ref`). Both are read once, at spawn, onto the round's
|
|
101
|
+
`spawns` row; an event takes them from its own round's row, else from the
|
|
102
|
+
PR's newest spawn row (a pre-spawn `checks.held`, a `round.capped` no spawn
|
|
103
|
+
explains). Each field is omitted when unknown -- never null -- so a PR whose
|
|
104
|
+
body names neither emits the same payload as before, byte for byte.
|
|
94
105
|
"""
|
|
95
106
|
|
|
96
107
|
from __future__ import annotations
|
|
@@ -148,6 +159,16 @@ ROUND_EVENT_KINDS = frozenset({
|
|
|
148
159
|
"checks.held",
|
|
149
160
|
})
|
|
150
161
|
|
|
162
|
+
# A GitHub closing claim (issue #165): one of the nine keywords, an optional
|
|
163
|
+
# colon, then `#<n>`, `<owner>/<repo>#<n>` or an issue URL. The repo parts
|
|
164
|
+
# are matched so a cross-repo close can be told apart -- and refused.
|
|
165
|
+
_CLOSING_REF_RE = re.compile(
|
|
166
|
+
r"(?<![\w-])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)(?:\s*:\s*|\s+)"
|
|
167
|
+
r"(?:https?://github\.com/(?P<url_repo>[\w.-]+/[\w.-]+)/issues/(?P<url_n>\d+)"
|
|
168
|
+
r"|(?:(?P<repo>[\w.-]+/[\w.-]+))?#(?P<n>\d+))(?![\w-])",
|
|
169
|
+
re.IGNORECASE,
|
|
170
|
+
)
|
|
171
|
+
|
|
151
172
|
# The verdict-of-record row (issue #155): one per (PR, round),
|
|
152
173
|
# `round-verdict:<round>@<head>:<verdict>:<readiness>:<class>:<contract>:<taskRef>`.
|
|
153
174
|
# The round leads so "already recorded for this round" is one prefix read
|
|
@@ -340,6 +361,30 @@ def _verdict_data(
|
|
|
340
361
|
}
|
|
341
362
|
|
|
342
363
|
|
|
364
|
+
def pr_closing_issue(body: str, repo: str) -> "int | None":
|
|
365
|
+
"""The issue number a PR body CLOSES in the PR's own `repo`, or None.
|
|
366
|
+
|
|
367
|
+
Only a closing claim counts (`Closes #6`, `fixes: owner/repo#6`, a
|
|
368
|
+
closing keyword before the issue's URL); a bare mention (`see #6`) is not
|
|
369
|
+
one. A claim on another repo's issue is dropped, and two DIFFERENT
|
|
370
|
+
same-repo issues are ambiguous -- the event carries one `issueNumber`,
|
|
371
|
+
and picking between them would be a guess -- so they read as None too.
|
|
372
|
+
"""
|
|
373
|
+
found: "set[int]" = set()
|
|
374
|
+
for match in _CLOSING_REF_RE.finditer(body or ""):
|
|
375
|
+
target = match.group("url_repo") or match.group("repo")
|
|
376
|
+
if target is not None and target.lower() != repo.lower():
|
|
377
|
+
continue
|
|
378
|
+
found.add(int(match.group("url_n") or match.group("n")))
|
|
379
|
+
return found.pop() if len(found) == 1 else None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def task_number_of(ref: "str | None") -> "int | None":
|
|
383
|
+
"""`TASK-<n>` as the integer `taskNumber` the ingest takes, or None."""
|
|
384
|
+
match = re.fullmatch(r"TASK-(\d+)", ref or "", re.IGNORECASE)
|
|
385
|
+
return int(match.group(1)) if match else None
|
|
386
|
+
|
|
387
|
+
|
|
343
388
|
def _ms(seconds: "int | float") -> int:
|
|
344
389
|
"""A ledger stamp (epoch seconds) as the API's epoch-ms `at`."""
|
|
345
390
|
return int(seconds) * 1000
|
|
@@ -748,6 +793,39 @@ def _mark_plan_rounds(
|
|
|
748
793
|
event["data"] = {**event.get("data", {}), "kind": ROUND_KIND_PLAN}
|
|
749
794
|
|
|
750
795
|
|
|
796
|
+
def _mark_links(
|
|
797
|
+
stamped: "list[tuple[int, dict]]", spawns: "list[dict]"
|
|
798
|
+
) -> None:
|
|
799
|
+
"""Stamp `issueNumber` / `taskNumber` on round events, in place (issue
|
|
800
|
+
#165). The event's own round's spawn row wins; an event whose round has
|
|
801
|
+
no spawn row (or no round at all) reads the PR's newest one. A field the
|
|
802
|
+
row does not know is left off, so a PR with neither keeps its payload."""
|
|
803
|
+
by_round: "dict[tuple[str, int, int], dict]" = {}
|
|
804
|
+
by_pr: "dict[tuple[str, int], dict]" = {}
|
|
805
|
+
# Oldest first, so the newest spawn of a round / PR is the one kept --
|
|
806
|
+
# what the body said LAST, even when that is nothing. `read_spawns` is
|
|
807
|
+
# newest first, so reversing it first keeps a same-second tie in order.
|
|
808
|
+
for spawn in sorted(reversed(spawns), key=lambda r: int(r["spawned_at"])):
|
|
809
|
+
repo, number = spawn["repo"], int(spawn["number"])
|
|
810
|
+
by_round[(repo, number, int(spawn["round"]))] = spawn
|
|
811
|
+
by_pr[(repo, number)] = spawn
|
|
812
|
+
for _, event in stamped:
|
|
813
|
+
if event["kind"] not in ROUND_EVENT_KINDS or "prNumber" not in event:
|
|
814
|
+
continue
|
|
815
|
+
repo, number = str(event.get("repo") or ""), int(event["prNumber"])
|
|
816
|
+
row: "dict | None" = None
|
|
817
|
+
if "round" in event:
|
|
818
|
+
row = by_round.get((repo, number, int(event["round"])))
|
|
819
|
+
if row is None:
|
|
820
|
+
row = by_pr.get((repo, number))
|
|
821
|
+
if row is None:
|
|
822
|
+
continue
|
|
823
|
+
if row.get("issue_number") is not None:
|
|
824
|
+
event["issueNumber"] = int(row["issue_number"])
|
|
825
|
+
if row.get("task_number") is not None:
|
|
826
|
+
event["taskNumber"] = int(row["task_number"])
|
|
827
|
+
|
|
828
|
+
|
|
751
829
|
def derive_events(state: State, *, since: int = 0) -> "list[dict]":
|
|
752
830
|
"""Every loop event the ledger implies whose stamp is >= `since`,
|
|
753
831
|
oldest first.
|
|
@@ -772,6 +850,7 @@ def derive_events(state: State, *, since: int = 0) -> "list[dict]":
|
|
|
772
850
|
stamped += _grant_events(state.read_grants())
|
|
773
851
|
stamped += _reap_events(state.read_reaps())
|
|
774
852
|
_mark_plan_rounds(stamped, _plan_rounds(pings))
|
|
853
|
+
_mark_links(stamped, spawns)
|
|
775
854
|
|
|
776
855
|
stamped = [(at, event) for at, event in stamped if at >= since]
|
|
777
856
|
stamped.sort(key=lambda pair: pair[0])
|
|
@@ -64,7 +64,9 @@ CREATE TABLE IF NOT EXISTS spawns (
|
|
|
64
64
|
head_sha TEXT NOT NULL,
|
|
65
65
|
session TEXT NOT NULL PRIMARY KEY,
|
|
66
66
|
task_ref TEXT,
|
|
67
|
-
spawned_at INTEGER NOT NULL
|
|
67
|
+
spawned_at INTEGER NOT NULL,
|
|
68
|
+
issue_number INTEGER,
|
|
69
|
+
task_number INTEGER
|
|
68
70
|
)"""
|
|
69
71
|
|
|
70
72
|
SCHEMA = f"""
|
|
@@ -405,6 +407,14 @@ _ADDED_COLUMNS = {
|
|
|
405
407
|
"verdicts": (
|
|
406
408
|
("readiness", "TEXT"),
|
|
407
409
|
),
|
|
410
|
+
# What the PR serves, read off its body at spawn (issue #165): the
|
|
411
|
+
# same-repo issue it closes and the origin task it names. NULL is "the
|
|
412
|
+
# body named none" -- and is the true value for every historical row,
|
|
413
|
+
# whose PR body was never read for it.
|
|
414
|
+
"spawns": (
|
|
415
|
+
("issue_number", "INTEGER"),
|
|
416
|
+
("task_number", "INTEGER"),
|
|
417
|
+
),
|
|
408
418
|
}
|
|
409
419
|
|
|
410
420
|
|
|
@@ -755,12 +765,20 @@ class State:
|
|
|
755
765
|
head_sha: str,
|
|
756
766
|
session: str,
|
|
757
767
|
task_ref: str | None,
|
|
768
|
+
issue_number: int | None = None,
|
|
769
|
+
task_number: int | None = None,
|
|
758
770
|
) -> None:
|
|
771
|
+
"""`issue_number` / `task_number` are what the PR serves (issue
|
|
772
|
+
#165): the same-repo issue its body closes and the origin task it
|
|
773
|
+
names, None when it names none. The loop-event emitter stamps them
|
|
774
|
+
on every round event of the PR."""
|
|
759
775
|
self._db.execute(
|
|
760
776
|
"INSERT OR REPLACE INTO spawns "
|
|
761
|
-
"(repo, number, round, head_sha, session, task_ref, spawned_at
|
|
762
|
-
"
|
|
763
|
-
|
|
777
|
+
"(repo, number, round, head_sha, session, task_ref, spawned_at, "
|
|
778
|
+
"issue_number, task_number) "
|
|
779
|
+
"VALUES (?,?,?,?,?,?,?,?,?)",
|
|
780
|
+
(repo, number, round_, head_sha, session, task_ref, int(time.time()),
|
|
781
|
+
issue_number, task_number),
|
|
764
782
|
)
|
|
765
783
|
self._db.commit()
|
|
766
784
|
|
|
@@ -1937,7 +1955,8 @@ class State:
|
|
|
1937
1955
|
at any row age. An empty collection reads nothing.
|
|
1938
1956
|
"""
|
|
1939
1957
|
sql = (
|
|
1940
|
-
"SELECT repo, number, round, head_sha, session, task_ref, spawned_at "
|
|
1958
|
+
"SELECT repo, number, round, head_sha, session, task_ref, spawned_at, "
|
|
1959
|
+
"issue_number, task_number "
|
|
1941
1960
|
"FROM spawns"
|
|
1942
1961
|
)
|
|
1943
1962
|
params: tuple = ()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.14
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.13
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/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
|