Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions scripts/linear-ticket/tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,32 @@ class FakeGitHub:
def __init__(self, protected):
self.protected = protected
self.current_base = "release/next"
self.pr_state = "open"
self.pr_merged = False
self.pr_head = "abc123"
self.association_paginated = None
self.fail_pull_numbers = set()
self.statuses = []
self.deleted_comments = []

def get(self, path, *, paginate=False):
if path.endswith("/commits/abc123/pulls"):
self.association_paginated = paginate
return [{
"number": 17,
"state": self.pr_state,
"head": {"sha": self.pr_head},
"base": {"repo": {"full_name": self.repo}},
}]
if path.endswith("/pulls/17"):
if 17 in self.fail_pull_numbers:
return None
return {
"number": 17,
"state": "open",
"state": self.pr_state,
"merged": self.pr_merged,
"html_url": "https://github.com/Comfy-Org/example/pull/17",
"head": {"sha": "abc123", "ref": "feature/be-123"},
"head": {"sha": self.pr_head, "ref": "feature/be-123"},
"base": {"ref": "release/next"},
"title": "Change something",
"body": "",
Expand Down Expand Up @@ -97,6 +113,61 @@ def test_unknown_protection_state_fails_closed_without_querying_linear(self):
self.assertEqual(validator.run(event()), 1)
self.assertEqual(github.statuses, [])

def test_signal_that_finishes_after_pr_merge_is_a_noop(self):
github = FakeGitHub(protected=True)
github.pr_state = "closed"
github.pr_merged = True
validator = self.validator(github)
validator._query_attachments = lambda _url: self.fail("Linear must not be queried")
stale_event = event()
stale_event["workflow_run"]["pull_requests"] = []

self.assertEqual(validator.run(stale_event), 0)
self.assertEqual(github.statuses, [])
self.assertEqual(github.deleted_comments, [])

def test_closed_unmerged_pr_fails_closed(self):
github = FakeGitHub(protected=True)
github.pr_state = "closed"
validator = self.validator(github)
stale_event = event()
stale_event["workflow_run"]["pull_requests"] = []

self.assertEqual(validator.run(stale_event), 1)
self.assertEqual(github.statuses, [])

def test_merged_pr_with_a_newer_head_is_a_noop(self):
github = FakeGitHub(protected=True)
github.pr_state = "closed"
github.pr_merged = True
github.pr_head = "newer-head"
validator = self.validator(github)
validator._query_attachments = lambda _url: self.fail("Linear must not be queried")
stale_event = event()
stale_event["workflow_run"]["pull_requests"] = []

self.assertEqual(validator.run(stale_event), 0)
self.assertEqual(github.statuses, [])

def test_pr_fetch_failure_does_not_become_a_completed_noop(self):
github = FakeGitHub(protected=True)
github.fail_pull_numbers.add(17)
validator = self.validator(github)
stale_event = event()
stale_event["workflow_run"]["pull_requests"] = []

self.assertEqual(validator.run(stale_event), 1)
self.assertEqual(github.statuses, [])

def test_commit_associations_are_paginated(self):
github = FakeGitHub(protected=True)
validator = self.validator(github)
stale_event = event()
stale_event["workflow_run"]["pull_requests"] = []

self.assertEqual(validator._resolve_pr(stale_event, "abc123"), (17, False))
self.assertTrue(github.association_paginated)

def test_retargeted_pr_does_not_publish_stale_terminal_status(self):
github = FakeGitHub(protected=False)
github.current_base = "main"
Expand Down
59 changes: 44 additions & 15 deletions scripts/linear-ticket/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,11 @@ def run(self, event: dict) -> int:
"signal workflow must run on pull_request; refusing to validate.")
return 1

self.pr_number = self._resolve_pr(event, head_sha)
self.pr_number, completed = self._resolve_pr(event, head_sha)
if completed:
log(f"The PR at {head_sha} merged before its signal run completed — nothing to "
"validate.")
return 0
if self.pr_number is None:
return 1 # error already reported

Expand Down Expand Up @@ -433,31 +437,56 @@ def run(self, event: dict) -> int:

return self._diagnose_and_fail(nodes, infra_error, branch, title, body)

def _resolve_pr(self, event: dict, head_sha: str) -> int | None:
"""Exactly one open PR. Same-repo runs carry workflow_run.pull_requests; fork runs do
not, so fall back to the commit->PR association (GitHub-owned data either way)."""
def _resolve_pr(self, event: dict, head_sha: str) -> tuple[int | None, bool]:
"""Resolve one open PR, or identify a signal whose exact-head PR already closed.

Same-repo runs normally carry ``workflow_run.pull_requests``. GitHub can empty that
list when a fast merge or close beats the signal run, and fork runs omit it, so fall
back to the commit->PR association (GitHub-owned data either way). The boolean return
is true only for an unambiguous completed exact-head PR; callers may safely no-op it.
"""
wr = event.get("workflow_run") or {}
candidates = [pr.get("number") for pr in (wr.get("pull_requests") or []) if pr.get("number")]
if not candidates:
Comment thread
christian-byrne marked this conversation as resolved.
assoc = self.gh.get(f"/repos/{self.gh.repo}/commits/{head_sha}/pulls") or []
assoc = self.gh.get(
f"/repos/{self.gh.repo}/commits/{head_sha}/pulls", paginate=True)
if assoc is None:
error(f"Could not fetch PRs associated with {head_sha}; failing closed.")
return None, False
candidates = [
pr.get("number") for pr in assoc
Comment thread
christian-byrne marked this conversation as resolved.
if pr.get("state") == "open"
and (pr.get("base") or {}).get("repo", {}).get("full_name") == self.gh.repo
if ((pr.get("base") or {}).get("repo") or {}).get("full_name") == self.gh.repo
]

open_prs: list[int] = []
completed_prs: list[int] = []
other_prs: list[int] = []
unreadable_prs: list[int] = []
for number in dict.fromkeys(candidates): # de-dup, preserve order
data = self.gh.get(f"/repos/{self.gh.repo}/pulls/{number}")
if data and data.get("state") == "open":
if data is None:
unreadable_prs.append(number)
elif data.get("state") == "open":
open_prs.append(number)
Comment thread
christian-byrne marked this conversation as resolved.

if len(open_prs) != 1:
error(f"Expected exactly one open PR associated with {head_sha}, found "
f"{len(open_prs)} (event={wr.get('event')}). Refusing to publish an "
"ambiguous result.")
return None
return open_prs[0]
elif data.get("merged") is True or data.get("merged_at"):
completed_prs.append(number)
else:
other_prs.append(number)

if unreadable_prs:
error(f"Could not fetch associated PR(s) {unreadable_prs}; failing closed.")
return None, False

if len(open_prs) == 1:
return open_prs[0], False
if not open_prs and len(completed_prs) == 1 and not other_prs:
return None, True

Comment thread
christian-byrne marked this conversation as resolved.
error(f"Expected exactly one open PR associated with {head_sha}, found "
f"{len(open_prs)} (and {len(completed_prs)} merged, "
f"{len(other_prs)} non-merged closed/unknown; event={wr.get('event')}). "
"Refusing to publish an ambiguous result.")
return None, False

def _query_attachments(self, html_url: str):
"""attachmentsForURL(this PR) with bounded retry for the async-link race (design §5
Expand Down
Loading