alissa-tools-github-revloop 0.19.0__tar.gz → 0.21.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_revloop-0.19.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.21.0}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/__main__.py +10 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/config.py +39 -2
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/ghclient.py +43 -15
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/loop.py +78 -3
- alissa_tools_github_revloop-0.21.0/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.19.0/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/README.md +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/setup.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.19.0 → alissa_tools_github_revloop-0.21.0}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -80,6 +80,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
80
80
|
metavar="OWNER/REPO",
|
|
81
81
|
help="only watch this repo; repeatable. Replaces the config list entirely.",
|
|
82
82
|
)
|
|
83
|
+
over.add_argument(
|
|
84
|
+
"--author",
|
|
85
|
+
dest="authors",
|
|
86
|
+
action="append",
|
|
87
|
+
metavar="LOGIN",
|
|
88
|
+
help="only review PRs opened by this GitHub login; repeatable. "
|
|
89
|
+
"Replaces the config list entirely. Empty = every author is served "
|
|
90
|
+
"(a scope filter, not a grant); the self-review skip still applies.",
|
|
91
|
+
)
|
|
83
92
|
over.add_argument(
|
|
84
93
|
"--operator",
|
|
85
94
|
dest="operators",
|
|
@@ -194,6 +203,7 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
194
203
|
through. `repos` becomes a tuple so it matches the config-file form."""
|
|
195
204
|
return {
|
|
196
205
|
"repos": tuple(args.repos) if args.repos else None,
|
|
206
|
+
"authors": tuple(args.authors) if args.authors else None,
|
|
197
207
|
"operators": tuple(args.operators) if args.operators else None,
|
|
198
208
|
"poll_interval": args.poll_interval,
|
|
199
209
|
"round_cap": args.round_cap,
|
|
@@ -120,6 +120,7 @@ CONFIG_KEYS = (
|
|
|
120
120
|
"poll_interval",
|
|
121
121
|
"round_cap",
|
|
122
122
|
"repos",
|
|
123
|
+
"authors",
|
|
123
124
|
"operators",
|
|
124
125
|
"agent_profile",
|
|
125
126
|
"reviewer_login",
|
|
@@ -262,6 +263,21 @@ class Config:
|
|
|
262
263
|
# Empty tuple means "every repo that requests a review from me".
|
|
263
264
|
repos: tuple[str, ...] = ()
|
|
264
265
|
|
|
266
|
+
# GitHub logins whose PRs this loop will spend rounds on. A SCOPE FILTER,
|
|
267
|
+
# not a capability grant -- so, like `repos` and unlike `operators`, empty
|
|
268
|
+
# (the default) means EVERY author, and an existing deployment with no
|
|
269
|
+
# `authors` key behaves exactly as it did before the key existed.
|
|
270
|
+
#
|
|
271
|
+
# The distinction is worth keeping straight: `operators` empty means NOBODY
|
|
272
|
+
# because honouring an ack is a power being handed out, and a grant that
|
|
273
|
+
# defaults to everyone is a hole. Serving a review request is not a power
|
|
274
|
+
# the daemon hands to the author -- summoning the loop already costs repo
|
|
275
|
+
# write access (to request the reviewer) plus a place on the `repos`
|
|
276
|
+
# allowlist. This key only narrows which of those already-authorised PRs are
|
|
277
|
+
# worth the rounds: skip dependabot/renovate, or a teammate who does not
|
|
278
|
+
# want agent reviews.
|
|
279
|
+
authors: tuple[str, ...] = ()
|
|
280
|
+
|
|
265
281
|
# GitHub logins whose re-entry ack may raise a capped PR's effective cap
|
|
266
282
|
# (loop.parse_reentry_ack). Empty -- the default -- means NO ack is ever
|
|
267
283
|
# honoured: the lever fails closed, because anyone who can comment on a PR
|
|
@@ -356,6 +372,23 @@ class Config:
|
|
|
356
372
|
def watches(self, full_name: str) -> bool:
|
|
357
373
|
return not self.repos or full_name in self.repos
|
|
358
374
|
|
|
375
|
+
def serves_author(self, login: str) -> bool:
|
|
376
|
+
"""Whether a PR by this login is in scope for the loop.
|
|
377
|
+
|
|
378
|
+
Empty list = every author (the filter pattern, see the field). Otherwise
|
|
379
|
+
membership case-insensitively, because GitHub logins are: a config
|
|
380
|
+
saying `Alissa-App` must match the `alissa-app` the API returns, or the
|
|
381
|
+
allowlist silently serves nobody -- the same failure mode `_string_list`
|
|
382
|
+
exists to prevent. Mirrors `loop.ReviewWatcher._is_operator`.
|
|
383
|
+
|
|
384
|
+
This is not the self-review guard and cannot stand in for it: an author
|
|
385
|
+
listed here is still refused if it is the reviewer identity, because
|
|
386
|
+
that check runs first and independently (see `loop.evaluate`).
|
|
387
|
+
"""
|
|
388
|
+
if not self.authors:
|
|
389
|
+
return True
|
|
390
|
+
return login.lower() in {a.lower() for a in self.authors}
|
|
391
|
+
|
|
359
392
|
@classmethod
|
|
360
393
|
def build(
|
|
361
394
|
cls,
|
|
@@ -399,6 +432,9 @@ class Config:
|
|
|
399
432
|
)
|
|
400
433
|
|
|
401
434
|
repos = _string_list(raw.get("repos", ()), "repos", "owner/repo entries")
|
|
435
|
+
authors = _string_list(
|
|
436
|
+
raw.get("authors", ()), "authors", "GitHub logins"
|
|
437
|
+
)
|
|
402
438
|
operators = _string_list(
|
|
403
439
|
raw.get("operators", ()), "operators", "GitHub logins"
|
|
404
440
|
)
|
|
@@ -529,6 +565,7 @@ class Config:
|
|
|
529
565
|
poll_interval=interval,
|
|
530
566
|
round_cap=cap,
|
|
531
567
|
repos=repos,
|
|
568
|
+
authors=authors,
|
|
532
569
|
operators=operators,
|
|
533
570
|
agent_profile=raw.get("agent_profile", "claude"),
|
|
534
571
|
reviewer_login=raw.get("reviewer_login"),
|
|
@@ -553,8 +590,8 @@ def _string_list(value: Any, key: str, what: str) -> tuple[str, ...]:
|
|
|
553
590
|
The guard is the point: JSON makes `"repos": "org/repo"` an easy typo, and
|
|
554
591
|
Python would iterate it into single CHARACTERS -- an allowlist of 30-odd
|
|
555
592
|
one-character names, which `watches()` then matches against nothing and the
|
|
556
|
-
daemon quietly reviews no PR at all.
|
|
557
|
-
|
|
593
|
+
daemon quietly reviews no PR at all. Every list key goes through here so
|
|
594
|
+
none of them can grow the footgun back.
|
|
558
595
|
"""
|
|
559
596
|
if isinstance(value, str):
|
|
560
597
|
raise ValueError(
|
|
@@ -899,12 +899,28 @@ class GitHub:
|
|
|
899
899
|
app; see ACTIONS_FALLBACK_NOTE for why that is accepted here and how the
|
|
900
900
|
answer says so.
|
|
901
901
|
|
|
902
|
-
COST
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
902
|
+
COST, stated exactly, because it is the sentence an operator would size
|
|
903
|
+
rate-limit headroom from: this path costs one call for the run listing
|
|
904
|
+
plus one per KEPT workflow, where the read it replaces cost one.
|
|
905
|
+
|
|
906
|
+
Nothing caches that on the production path. `loop._dry_run_rollups` is
|
|
907
|
+
the only rollup memo and only the dry-run branch reads it, so what
|
|
908
|
+
bounds how often the cost is paid is the POLL INTERVAL -- both while
|
|
909
|
+
`_checks_at_spawn` holds a round on `checks_spawn_wait_seconds` and
|
|
910
|
+
while `_gate_on_checks` holds a finished verdict on
|
|
911
|
+
`checks_wait_seconds`, each of which re-reads the rollup every pass.
|
|
912
|
+
|
|
913
|
+
Nor is the fan-out bounded. CHECK_RUN_PAGE_LIMIT bounds the run
|
|
914
|
+
LISTING; the number of kept workflows is whatever the sha carries, and
|
|
915
|
+
each one costs a `jobs` call with `_api`'s 60s timeout, so a
|
|
916
|
+
many-workflow sha turns one rollup read into a long synchronous stall
|
|
917
|
+
inside a poll pass that has other PRs to decide. On this fleet's repos
|
|
918
|
+
(one or two workflows per commit) it is two or three calls; that is a
|
|
919
|
+
fact about these repos, not a bound this code enforces.
|
|
920
|
+
|
|
921
|
+
The trade is still worth taking where it applies: the alternative there
|
|
922
|
+
is a permanent CHECKS_UNKNOWN, which costs one call and can never
|
|
923
|
+
approve.
|
|
908
924
|
"""
|
|
909
925
|
runs = self._rollup_listing(
|
|
910
926
|
f"repos/{owner}/{repo}/actions/runs",
|
|
@@ -919,14 +935,26 @@ class GitHub:
|
|
|
919
935
|
|
|
920
936
|
@staticmethod
|
|
921
937
|
def _latest_run_per_workflow(runs: list[dict]) -> list[dict]:
|
|
922
|
-
"""One run per workflow: the most recent.
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
run
|
|
938
|
+
"""One run per workflow AND trigger event: the most recent.
|
|
939
|
+
|
|
940
|
+
Two different facts are being separated here, and collapsing them was a
|
|
941
|
+
fail-open bug.
|
|
942
|
+
|
|
943
|
+
SUPERSEDED ATTEMPTS collapse. A sha carries more than one run of the
|
|
944
|
+
same workflow for the same event whenever it was triggered again -- a
|
|
945
|
+
re-run shares its run id, a re-dispatch does not. Reading all of them
|
|
946
|
+
would judge the commit on a superseded attempt: an earlier failed run
|
|
947
|
+
whose re-trigger passed would hold the head red forever.
|
|
948
|
+
|
|
949
|
+
DISTINCT EVENTS DO NOT. A workflow declaring `on: [push, pull_request]`
|
|
950
|
+
produces a `push` run and a `pull_request` run for the same sha, and
|
|
951
|
+
GitHub publishes a check run per job for BOTH -- so the read this
|
|
952
|
+
stands in for reports both. Keying on `workflow_id` alone dropped the
|
|
953
|
+
lower `run_number` of the pair unread, and when the dropped one was the
|
|
954
|
+
red one the fallback answered GREEN for a commit whose real rollup is
|
|
955
|
+
red. That is the fail-open direction this whole component exists to
|
|
956
|
+
avoid, so the key carries `event` as well. A re-run preserves its run's
|
|
957
|
+
`event`, so the superseded-attempt collapse above is unaffected.
|
|
930
958
|
|
|
931
959
|
Recency is `run_number` then `id`, both monotonic per workflow and both
|
|
932
960
|
integers -- deliberately not a timestamp string, which is the field a
|
|
@@ -938,7 +966,7 @@ class GitHub:
|
|
|
938
966
|
for index, run_ in enumerate(runs):
|
|
939
967
|
workflow_id = run_.get("workflow_id")
|
|
940
968
|
key: object = (
|
|
941
|
-
("workflow", workflow_id)
|
|
969
|
+
("workflow", workflow_id, str(run_.get("event") or ""))
|
|
942
970
|
if workflow_id is not None
|
|
943
971
|
else ("unidentified", index)
|
|
944
972
|
)
|
|
@@ -407,6 +407,22 @@ CHECKS_FAILING_LINE = "- `{name}` — {conclusion}{url}"
|
|
|
407
407
|
|
|
408
408
|
# Prepended when the gate held the approve for the whole wait bound and the
|
|
409
409
|
# rollup still had not settled, so the verdict lands as a COMMENT.
|
|
410
|
+
# Prepended to an APPROVE whose green came from the Actions-API fallback rather
|
|
411
|
+
# than the check-runs rollup. Not a caveat on the review -- the code verdict is
|
|
412
|
+
# unchanged -- but on the EVIDENCE behind the approve: the fallback cannot see a
|
|
413
|
+
# check run posted by a third-party check app, so "green" here means "every
|
|
414
|
+
# Actions-produced context passed", which is the same thing only on an
|
|
415
|
+
# Actions-only repo.
|
|
416
|
+
CHECKS_FALLBACK_LEAD = (
|
|
417
|
+
"**CI rollup at `{sha}` read through the Actions API.** This deployment's "
|
|
418
|
+
"credential cannot read check runs (GitHub does not offer the `Checks` "
|
|
419
|
+
"permission on fine-grained PATs), so the head was confirmed green from its "
|
|
420
|
+
"workflow runs and jobs instead. That read sees every GitHub-Actions "
|
|
421
|
+
"context and no third-party check app's, so on an Actions-only repo it is "
|
|
422
|
+
"the same answer — flagging the path because an approve is the operator's "
|
|
423
|
+
"merge cue.\n\n"
|
|
424
|
+
)
|
|
425
|
+
|
|
410
426
|
CHECKS_UNSETTLED_LEAD = (
|
|
411
427
|
"**Recorded as a comment, not an approve — the CI rollup at `{sha}` never "
|
|
412
428
|
"concluded.**\n\n"
|
|
@@ -466,10 +482,18 @@ CHECKS_STILL_RUNNING = "Still running at the bound: {names}."
|
|
|
466
482
|
# which waited on nothing and so cannot describe anything as being "at the
|
|
467
483
|
# bound" (see CHECKS_AT_SPAWN_GATE_OFF).
|
|
468
484
|
CHECKS_GATE_OFF_DETAIL = "Still running when the round was queued: {names}."
|
|
485
|
+
# Deliberately does NOT tell the operator to grant `checks: read`, which is what
|
|
486
|
+
# it said before the Actions fallback existed. GitHub does not offer the `Checks`
|
|
487
|
+
# permission on fine-grained PATs at all, and a rollup that reaches UNKNOWN on
|
|
488
|
+
# such a deployment has ALREADY tried the Actions read and failed -- so that
|
|
489
|
+
# advice pointed at the one action that provably cannot work, at the moment the
|
|
490
|
+
# operator is already blocked. The remaining causes are the ones named here.
|
|
469
491
|
CHECKS_UNREADABLE = (
|
|
470
492
|
"The rollup could not be read: `{why}`. An unreadable rollup is not a green "
|
|
471
|
-
"one
|
|
472
|
-
"
|
|
493
|
+
"one. On a credential that cannot read check runs the loop already fell "
|
|
494
|
+
"back to the Actions API, so reaching this point means that read failed "
|
|
495
|
+
"too — check the credential's `Actions: Read`, or whether the listing was "
|
|
496
|
+
"truncated (more contexts than the page bound)."
|
|
473
497
|
)
|
|
474
498
|
|
|
475
499
|
# The operator page for a native verdict post that keeps failing. Loud on
|
|
@@ -1671,6 +1695,13 @@ class ReviewWatcher:
|
|
|
1671
1695
|
if pr.author == self.github.login:
|
|
1672
1696
|
# GitHub rejects a self review-request, so this should be
|
|
1673
1697
|
# unreachable -- but a shared bot identity would land here.
|
|
1698
|
+
#
|
|
1699
|
+
# FIRST, and deliberately above the authors allowlist (further
|
|
1700
|
+
# down): the allowlist is a scope filter an operator writes, and no
|
|
1701
|
+
# entry in it may buy back a review GitHub itself forbids. Listing
|
|
1702
|
+
# the reviewer login in `authors` therefore narrows the loop to a PR
|
|
1703
|
+
# it will then refuse -- which is the correct reading of "only serve
|
|
1704
|
+
# this author" for an author that cannot be served.
|
|
1674
1705
|
return Decision(
|
|
1675
1706
|
Action.SKIPPED,
|
|
1676
1707
|
f"PR author is the reviewer identity ({pr.author}); "
|
|
@@ -1731,6 +1762,36 @@ class ReviewWatcher:
|
|
|
1731
1762
|
self._clear_own_review_request(pr, my_reviews, converged)
|
|
1732
1763
|
return Decision(Action.CONVERGED, converged, completed)
|
|
1733
1764
|
|
|
1765
|
+
# THE AUTHORS SCOPE FILTER (issue #93), and its position is the whole
|
|
1766
|
+
# of its contract: it gates STARTING a round and nothing else.
|
|
1767
|
+
#
|
|
1768
|
+
# Client-side, because the discovery search is deliberately untouched --
|
|
1769
|
+
# the author is only in hand once the PR detail is fetched. Below
|
|
1770
|
+
# convergence rather than up beside the self-review skip, because every
|
|
1771
|
+
# branch above finishes a round that was already started: a verdict
|
|
1772
|
+
# envelope ahead of its native review still becomes a review of record
|
|
1773
|
+
# (_close_round_natively), and a converged PR still gets its dangling
|
|
1774
|
+
# review request withdrawn (_clear_own_review_request). An operator who
|
|
1775
|
+
# narrows `authors` mid-round must not strand the round that is already
|
|
1776
|
+
# running, and stopping one step short of its verdict of record is the
|
|
1777
|
+
# studio #298 failure this module refuses everywhere else (PR #94 round
|
|
1778
|
+
# 1). It costs a filtered PR one my_reviews fetch and the review-task
|
|
1779
|
+
# lookup per poll -- the negative cache bounds the corpus search -- and
|
|
1780
|
+
# that is the price of not stranding a round.
|
|
1781
|
+
#
|
|
1782
|
+
# Above THIS line, therefore, and everything the issue's checklist asks
|
|
1783
|
+
# for follows from where it sits: the effective cap and its ack
|
|
1784
|
+
# announcement, the cap-out escalation (the only branch below that would
|
|
1785
|
+
# COMMENT on the PR), `round_ = completed + 1`, the stale-round probe and
|
|
1786
|
+
# the spawn are all downstream. A filtered PR burns no round number,
|
|
1787
|
+
# records no attempt, holds no stale-round slot and never says a word on
|
|
1788
|
+
# the PR -- it gets silence, exactly as an unwatched repo does.
|
|
1789
|
+
if not self.config.serves_author(pr.author):
|
|
1790
|
+
return Decision(
|
|
1791
|
+
Action.SKIPPED,
|
|
1792
|
+
f"PR author {pr.author} is not in the authors allowlist",
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1734
1795
|
# The effective cap is the configured one plus every re-entry an
|
|
1735
1796
|
# operator has explicitly acked on THIS PR (issue #42). The sum is a
|
|
1736
1797
|
# local read, so it costs nothing on the common path; the GitHub scan
|
|
@@ -2438,10 +2499,24 @@ class ReviewWatcher:
|
|
|
2438
2499
|
rollup = self.github.check_rollup(pr.owner, pr.repo, judged)
|
|
2439
2500
|
|
|
2440
2501
|
if rollup.state == CHECKS_GREEN:
|
|
2441
|
-
|
|
2502
|
+
# `info`, not `debug`: this is the line an operator reads to find
|
|
2503
|
+
# out WHICH read path called the head green, and on the deployment
|
|
2504
|
+
# that needs the answer the fallback is every read.
|
|
2505
|
+
log.info(
|
|
2442
2506
|
"%s round %d: CI rollup at %s is %s — approving as usual",
|
|
2443
2507
|
pr.slug, round_, judged[:8], rollup.summary,
|
|
2444
2508
|
)
|
|
2509
|
+
if rollup.via_actions_fallback:
|
|
2510
|
+
# The one state where the marker had nowhere PR-visible to go.
|
|
2511
|
+
# Every other path that surfaces `summary` (the hold activity
|
|
2512
|
+
# comment, the degraded-verdict lead) is reachable only from
|
|
2513
|
+
# PENDING or UNKNOWN, so "approved on green" -- the claim an
|
|
2514
|
+
# operator acts on -- was the single verdict that did not say
|
|
2515
|
+
# it came from the narrower read.
|
|
2516
|
+
return ChecksGate(
|
|
2517
|
+
lead=CHECKS_FALLBACK_LEAD.format(sha=judged[:8]),
|
|
2518
|
+
state=CHECKS_GREEN,
|
|
2519
|
+
)
|
|
2445
2520
|
return ChecksGate()
|
|
2446
2521
|
|
|
2447
2522
|
if rollup.state == CHECKS_RED:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.21.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.19.0
|
|
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
|