Skip to content
Merged
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
6 changes: 2 additions & 4 deletions skills/nextwork/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,8 @@ like production dispatch: first whitespace token of the first comment line.
itself is older than `--stale-hours` (default 6h); raw age still uses
`--triage-stale-hours` (default 72h).
- `waiting_ci` and `waiting_merge_queue` are not flipped by `--stale-hours`.
- Merge-queue membership is only checked for PRs labeled `ready-for-merge`
(to avoid an extra API call per PR); the check uses the PR's `baseRefName`
when available (not only the repo default branch). Other PRs never report
`in_merge_queue`.
- Merge-queue membership is checked for all open PRs; the check uses the
PR's `baseRefName` when available (not only the repo default branch).
- Linked-PR detection scans open PRs only when an issue reaches that check
(after blockers / assignment / sub-issues). The scan is capped at five
GraphQL pages (~500 PRs) per repo; beyond that, some links may be missed.
Expand Down
22 changes: 12 additions & 10 deletions skills/nextwork/scripts/nextwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,14 @@ def classify_pr(
eliminated=True,
)

# Merge-queue membership is authoritative regardless of labels.
if item.get("in_merge_queue"):
return Classification(
status="waiting_merge_queue",
reason="Already enqueued in the merge queue",
eliminated=True,
)

inflight = classify_inflight_agent(comments, stale_hours, now)
if inflight:
return inflight
Expand Down Expand Up @@ -980,12 +988,6 @@ def classify_pr(
)

if "ready-for-merge" in labels:
if item.get("in_merge_queue"):
return Classification(
status="waiting_merge_queue",
reason="Already enqueued in the merge queue",
eliminated=True,
)
if checks_pending:
return Classification(
status="waiting_ci",
Expand Down Expand Up @@ -1927,9 +1929,9 @@ def _resolve_linked(r=repo, n=number, fn=get_linked) -> list[int]:


def maybe_check_merge_queue(items: list[dict[str, Any]], fetcher: MergeQueueChecker) -> None:
"""Second pass: only hits the merge-queue API for PRs labeled ready-for-merge."""
"""Second pass: hits the merge-queue API for all open PRs."""
for item in items:
if item["kind"] == "pull" and "ready-for-merge" in item.get("labels", []):
if item["kind"] == "pull":
item["in_merge_queue"] = fetcher.is_in_merge_queue(
item["repo"],
item["number"],
Expand Down Expand Up @@ -2552,9 +2554,9 @@ def main(argv: list[str] | None = None) -> None:
quiet=args.quiet,
)
maybe_check_merge_queue(items, fetcher)
# Merge-queue membership can change ready_to_merge -> waiting_merge_queue; reclassify.
# Merge-queue membership can change any status -> waiting_merge_queue; reclassify.
for item in items:
if item["kind"] == "pull" and "ready-for-merge" in item.get("labels", []):
if item["kind"] == "pull" and item.get("in_merge_queue"):
classification = classify_item(
item,
user,
Expand Down
35 changes: 28 additions & 7 deletions skills/nextwork/scripts/nextwork_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,20 @@ def test_waiting_merge_queue(self):
self.assertEqual(result.status, "waiting_merge_queue")
self.assertTrue(result.eliminated)

def test_waiting_merge_queue_without_ready_for_merge_label(self):
"""Merge-queue detection works regardless of which labels are present."""
item = make_pr(labels=["requires-manual-review"], in_merge_queue=True)
result = classify_pr(item, "alice", 6, NOW)
self.assertEqual(result.status, "waiting_merge_queue")
self.assertTrue(result.eliminated)

def test_waiting_merge_queue_no_labels(self):
"""Merge-queue detection works even with no control labels at all."""
item = make_pr(labels=[], in_merge_queue=True)
result = classify_pr(item, "alice", 6, NOW)
self.assertEqual(result.status, "waiting_merge_queue")
self.assertTrue(result.eliminated)

def test_waiting_fix_bot_author(self):
item = make_pr(
author="fullsend-ai-coder[bot]",
Expand Down Expand Up @@ -2212,15 +2226,18 @@ def test_apply_with_confirmed_ok(self):


class TestMaybeCheckMergeQueue(unittest.TestCase):
def test_sets_flag_only_for_ready_for_merge_prs(self):
def test_checks_all_prs_regardless_of_labels(self):
class StubFetcher:
def __init__(self):
self.calls = []

def is_in_merge_queue(self, repo, number, *, base_branch=None):
self.seen = (repo, number, base_branch)
return number == 2
self.calls.append((repo, number, base_branch))
return number == 1

stub = StubFetcher()
items = [
{"kind": "pull", "repo": "a/b", "number": 1, "labels": ["ready-for-review"]},
{"kind": "pull", "repo": "a/b", "number": 1, "labels": ["requires-manual-review"]},
{
"kind": "pull",
"repo": "a/b",
Expand All @@ -2231,10 +2248,14 @@ def is_in_merge_queue(self, repo, number, *, base_branch=None):
{"kind": "issue", "repo": "a/b", "number": 3, "labels": ["ready-for-merge"]},
]
maybe_check_merge_queue(items, stub)
self.assertNotIn("in_merge_queue", items[0])
self.assertTrue(items[1]["in_merge_queue"])
self.assertEqual(stub.seen, ("a/b", 2, "release"))
self.assertTrue(items[0]["in_merge_queue"])
self.assertFalse(items[1]["in_merge_queue"])
# Issues are skipped.
self.assertNotIn("in_merge_queue", items[2])
# Both PRs checked (regardless of labels), issue skipped.
self.assertEqual(len(stub.calls), 2)
self.assertIn(("a/b", 1, None), stub.calls)
self.assertIn(("a/b", 2, "release"), stub.calls)


class TestFetchErrors(unittest.TestCase):
Expand Down
Loading