Skip to content

Commit 02ad0c3

Browse files
committed
Update
[ghstack-poisoned]
1 parent f72a01b commit 02ad0c3

4 files changed

Lines changed: 147 additions & 12 deletions

File tree

greenlight/src/greenlight/github_client.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
_PRComment,
3333
_PRReview,
3434
_RepoClient,
35+
_VerdictReview,
3536
)
3637

3738

@@ -364,20 +365,31 @@ def upsert_issue_comment(
364365
pr.create_issue_comment(body)
365366

366367

367-
def dismiss_prior_greenlight_approvals(pr: VerdictPR, *, bot_login: str, message: str) -> list[int]:
368-
"""Dismiss every prior APPROVED review authored by ``bot_login``.
368+
def _iter_greenlight_approvals(pr: VerdictPR, bot_login: str) -> Iterator[_VerdictReview]:
369+
"""Yield each live APPROVED review authored by ``bot_login`` -- greenlight's own approvals.
369370
370-
The login is passed in (the greenlight GitHub App's ``<slug>[bot]`` account) rather
371-
than read via ``get_user``, which is not available on an App installation token; only
372-
that account's own approvals are ever dismissed.
371+
The login is passed in (the greenlight GitHub App's ``<slug>[bot]`` account) rather than read
372+
via ``get_user``, which an App installation token cannot call. Null-user, empty-login, and
373+
non-APPROVED reviews are skipped; the login match is case-insensitive.
373374
"""
374375
target = bot_login.lower()
375-
dismissed: list[int] = []
376376
for review in pr.get_reviews():
377377
user = review.user
378378
if user is None or not user.login:
379379
continue
380380
if user.login.lower() == target and review.state == _REVIEW_STATE_APPROVED:
381-
review.dismiss(message)
382-
dismissed.append(review.id)
381+
yield review
382+
383+
384+
def has_live_greenlight_approval(pr: VerdictPR, *, bot_login: str) -> bool:
385+
"""Return True iff ``bot_login`` has at least one live (undismissed) APPROVED review on ``pr``."""
386+
return next(_iter_greenlight_approvals(pr, bot_login), None) is not None
387+
388+
389+
def dismiss_prior_greenlight_approvals(pr: VerdictPR, *, bot_login: str, message: str) -> list[int]:
390+
"""Dismiss every prior APPROVED review authored by ``bot_login`` (see ``_iter_greenlight_approvals``)."""
391+
dismissed: list[int] = []
392+
for review in _iter_greenlight_approvals(pr, bot_login):
393+
review.dismiss(message)
394+
dismissed.append(review.id)
383395
return dismissed

greenlight/src/greenlight/verdict.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
updates GitHub with a defanged copy of the message. Both LAND and NO_LAND upsert one
1313
canonical verdict comment -- edited in place across runs, found by a hidden marker and
1414
restricted to greenlight's own account (``bot_login``); LAND additionally posts an approving
15-
review, and NO_LAND additionally dismisses greenlight's own prior approval (matched by
16-
``bot_login``). The row is authoritative, so the comment upsert is best-effort on every path (a
15+
review unless greenlight already holds a live approval on the PR, and NO_LAND additionally
16+
dismisses greenlight's own prior approval (both matched by ``bot_login``). The row is
17+
authoritative, so the comment upsert is best-effort on every path (a
1718
failed write is logged and swallowed); the LAND approving review and the NO_LAND dismissal are the
1819
merge gate and stay load-bearing -- they raise on failure. MARKER statuses (CANCELLED / FAILED /
1920
AI_REVIEW_STARTED) always emit the row and, when a ``bot_login`` and token are both present,
@@ -303,8 +304,11 @@ def _run_full(
303304
job_url = request.agent_job_url or request.eval_job_url
304305
body = comment_format.verdict_body(status, reason, message, job_url, request.run_id)
305306
if status == STATUS_LAND:
306-
github_client.post_review(pr, event=github_client.REVIEW_EVENT_APPROVE, body=_LAND_REVIEW_BODY)
307-
logger.info("approved %s#%d", request.repo, request.pr_number)
307+
if github_client.has_live_greenlight_approval(pr, bot_login=request.bot_login):
308+
logger.info("already approved %s#%d; skipping re-approval", request.repo, request.pr_number)
309+
else:
310+
github_client.post_review(pr, event=github_client.REVIEW_EVENT_APPROVE, body=_LAND_REVIEW_BODY)
311+
logger.info("approved %s#%d", request.repo, request.pr_number)
308312
_best_effort_upsert(request, config, body, build_github=build_github, pr=pr)
309313
return
310314
dismissed = github_client.dismiss_prior_greenlight_approvals(

greenlight/tests/test_github_client.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,3 +1323,54 @@ def test_dismiss_prior_greenlight_approvals_when_none_match():
13231323

13241324
assert dismissed == []
13251325
assert reviews[0].dismissed_with is None
1326+
1327+
1328+
def test_has_live_greenlight_approval_true_for_own_approved():
1329+
pr = _FakeVerdictPR(reviews=[_FakeVerdictReview(1, _FakeActor("greenlight-app[bot]"), "APPROVED")])
1330+
1331+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is True
1332+
1333+
1334+
def test_has_live_greenlight_approval_false_when_no_reviews():
1335+
pr = _FakeVerdictPR(reviews=[])
1336+
1337+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is False
1338+
1339+
1340+
def test_has_live_greenlight_approval_matches_login_case_insensitively():
1341+
pr = _FakeVerdictPR(reviews=[_FakeVerdictReview(1, _FakeActor("GreenLight-App[Bot]"), "APPROVED")])
1342+
1343+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is True
1344+
1345+
1346+
def test_has_live_greenlight_approval_skips_null_user_and_empty_login():
1347+
reviews = [
1348+
_FakeVerdictReview(1, None, "APPROVED"),
1349+
_FakeVerdictReview(2, _FakeActor(""), "APPROVED"),
1350+
]
1351+
pr = _FakeVerdictPR(reviews=reviews)
1352+
1353+
# A null-user or empty-login APPROVED review cannot be attributed to greenlight.
1354+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is False
1355+
1356+
1357+
def test_has_live_greenlight_approval_false_for_commented_and_dismissed():
1358+
reviews = [
1359+
_FakeVerdictReview(1, _FakeActor("greenlight-app[bot]"), "COMMENTED"),
1360+
_FakeVerdictReview(2, _FakeActor("greenlight-app[bot]"), "DISMISSED"),
1361+
]
1362+
pr = _FakeVerdictPR(reviews=reviews)
1363+
1364+
# Only APPROVED is a live approval; a dismissed review is no longer live.
1365+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is False
1366+
1367+
1368+
def test_has_live_greenlight_approval_true_when_any_own_approved_among_many():
1369+
reviews = [
1370+
_FakeVerdictReview(1, _FakeActor("alice"), "APPROVED"),
1371+
_FakeVerdictReview(2, _FakeActor("greenlight-app[bot]"), "COMMENTED"),
1372+
_FakeVerdictReview(3, _FakeActor("greenlight-app[bot]"), "APPROVED"),
1373+
]
1374+
pr = _FakeVerdictPR(reviews=reviews)
1375+
1376+
assert github_client.has_live_greenlight_approval(pr, bot_login="greenlight-app[bot]") is True

greenlight/tests/test_verdict.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,74 @@ def test_full_land_approves_with_empty_body(make_config, tmp_path):
697697
assert pr.created_reviews == [("APPROVE", "")]
698698

699699

700+
def test_full_land_skips_reapproval_when_already_approved(make_config, tmp_path, caplog):
701+
rec = _Recorder()
702+
emit = _FakeEmit(rec)
703+
reviews = [_FakeReview(1, _BOT, "APPROVED", rec)]
704+
pr = _FakePR("h", rec, reviews=reviews)
705+
gh = _FakeGithub(_FakeRepo(pr))
706+
vf = _write_verdict(tmp_path, status="LAND", reason="clean", message="LGTM")
707+
req = VerdictRequest(repo="r", pr_number=40, head_sha="h", eval_hash=_HASH, verdict_file=vf, bot_login=_BOT)
708+
709+
with caplog.at_level(logging.INFO, logger="greenlight"):
710+
verdict.run(req, make_config(github_token="tok"), build_github=lambda t: gh, emit=emit, now=lambda: _FIXED)
711+
712+
# A live greenlight approval already exists: no second APPROVE review is posted (the prior one is
713+
# left intact), yet the canonical comment is still upserted and the skip is logged.
714+
assert pr.created_reviews == []
715+
assert reviews[0].dismissed_with is None
716+
assert rec.events == ["emit", "comment"]
717+
assert pr.comments[0].startswith(comment_format.COMMENT_MARKER)
718+
assert f"**{comment_format.LAND_HEADLINE}**" in pr.comments[0]
719+
assert "LGTM" in pr.comments[0]
720+
assert any("skipping re-approval" in record.getMessage() for record in caplog.records)
721+
722+
723+
def test_full_land_reapproves_when_prior_bot_approval_dismissed(make_config, tmp_path):
724+
rec = _Recorder()
725+
emit = _FakeEmit(rec)
726+
pr = _FakePR("h", rec, reviews=[_FakeReview(1, _BOT, "DISMISSED", rec)])
727+
gh = _FakeGithub(_FakeRepo(pr))
728+
vf = _write_verdict(tmp_path, status="LAND", reason="clean", message="LGTM")
729+
req = VerdictRequest(repo="r", pr_number=41, head_sha="h", eval_hash=_HASH, verdict_file=vf, bot_login=_BOT)
730+
731+
verdict.run(req, make_config(github_token="tok"), build_github=lambda t: gh, emit=emit, now=lambda: _FIXED)
732+
733+
# A prior greenlight approval that was dismissed is not live, so LAND re-approves.
734+
assert pr.created_reviews == [("APPROVE", "")]
735+
assert rec.events == ["emit", "review:APPROVE", "comment"]
736+
737+
738+
def test_full_land_approves_when_only_other_author_approved(make_config, tmp_path):
739+
rec = _Recorder()
740+
emit = _FakeEmit(rec)
741+
pr = _FakePR("h", rec, reviews=[_FakeReview(1, "alice", "APPROVED", rec)])
742+
gh = _FakeGithub(_FakeRepo(pr))
743+
vf = _write_verdict(tmp_path, status="LAND", reason="clean", message="LGTM")
744+
req = VerdictRequest(repo="r", pr_number=42, head_sha="h", eval_hash=_HASH, verdict_file=vf, bot_login=_BOT)
745+
746+
verdict.run(req, make_config(github_token="tok"), build_github=lambda t: gh, emit=emit, now=lambda: _FIXED)
747+
748+
# A human's approval is not greenlight's own, so LAND still posts greenlight's approval.
749+
assert pr.created_reviews == [("APPROVE", "")]
750+
assert rec.events == ["emit", "review:APPROVE", "comment"]
751+
752+
753+
def test_full_land_approves_when_only_bot_commented_review(make_config, tmp_path):
754+
rec = _Recorder()
755+
emit = _FakeEmit(rec)
756+
pr = _FakePR("h", rec, reviews=[_FakeReview(1, _BOT, "COMMENTED", rec)])
757+
gh = _FakeGithub(_FakeRepo(pr))
758+
vf = _write_verdict(tmp_path, status="LAND", reason="clean", message="LGTM")
759+
req = VerdictRequest(repo="r", pr_number=43, head_sha="h", eval_hash=_HASH, verdict_file=vf, bot_login=_BOT)
760+
761+
verdict.run(req, make_config(github_token="tok"), build_github=lambda t: gh, emit=emit, now=lambda: _FIXED)
762+
763+
# A COMMENTED review from greenlight is not an approval, so LAND posts one.
764+
assert pr.created_reviews == [("APPROVE", "")]
765+
assert rec.events == ["emit", "review:APPROVE", "comment"]
766+
767+
700768
class _CommentBoomPR(_FakePR):
701769
"""A PR whose issue-comment create always fails, to prove the cosmetic write is best-effort."""
702770

0 commit comments

Comments
 (0)