Skip to content
Closed
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
25 changes: 24 additions & 1 deletion src/agent_merge_queue/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,12 +1672,21 @@ def create_integration_pull_request(
except QueueError as error:
if "Reference already exists" not in str(error):
raise
ref = self._json("api", f"repos/{self.repository}/git/ref/heads/{branch}")
branch_head = str((ref.get("object") or {}).get("sha") or "")
if not branch_head:
raise QueueError(f"integration branch {branch} has no readable head")
else:
branch_head = base_sha

merged_heads: list[str] = []
conflict: dict[str, Any] | None = None
for entry in entries:
if self.is_ancestor(entry.head_sha, branch_head):
merged_heads.append(entry.head_sha)
continue
try:
self._json(
merged = self._json(
"api",
"--method",
"POST",
Expand All @@ -1689,8 +1698,22 @@ def create_integration_pull_request(
"-f",
f"commit_message=DeployBot batch {batch_id}: PR #{entry.number}",
)
branch_head = str((merged or {}).get("sha") or branch_head)
merged_heads.append(entry.head_sha)
except QueueError as error:
# GitHub returns 204 with an empty body when a concurrent
# update makes this merge a no-op. _json cannot decode that
# response, so re-read the authoritative branch head and
# accept it only when the exact frozen source is now present.
if str(error) == "GitHub returned invalid JSON":
ref = self._json(
"api", f"repos/{self.repository}/git/ref/heads/{branch}"
)
current_head = str((ref.get("object") or {}).get("sha") or "")
if current_head and self.is_ancestor(entry.head_sha, current_head):
branch_head = current_head
merged_heads.append(entry.head_sha)
continue
conflict = {
"number": entry.number,
"head_sha": entry.head_sha,
Expand Down
95 changes: 95 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2426,6 +2426,7 @@ def test_integration_can_require_non_actions_author(self) -> None:
client.owner = "example"
client.coordinator_logins = {"trusted"}
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(return_value=False)
client._json = Mock(
side_effect=[
{},
Expand Down Expand Up @@ -2460,6 +2461,7 @@ def test_integration_requires_app_author_to_be_a_coordinator(self) -> None:
client.owner = "example"
client.coordinator_logins = {"trusted"}
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(return_value=False)
client._json = Mock(
side_effect=[
{},
Expand Down Expand Up @@ -3619,6 +3621,7 @@ def test_integration_pr_contains_every_frozen_head(self) -> None:
client.owner = "example"
client.name = "repo"
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(return_value=False)
client._json = Mock(
side_effect=[
{},
Expand All @@ -3644,6 +3647,13 @@ def test_integration_pr_contains_every_frozen_head(self) -> None:
if "repos/example/repo/merges" in call.args
]
self.assertEqual(len(merge_calls), 2)
self.assertEqual(
[call.args for call in client.is_ancestor.call_args_list],
[
(first.head_sha, "b" * 40),
(second.head_sha, "1" * 40),
],
)
self.assertEqual(
[call.args for call in client.remove_label.call_args_list],
[
Expand All @@ -3670,6 +3680,7 @@ def test_conflicted_integration_keeps_source_intents_discoverable(self) -> None:
client.owner = "example"
client.name = "repo"
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(return_value=False)
client._json = Mock(
side_effect=[
{},
Expand Down Expand Up @@ -3830,6 +3841,89 @@ def test_resumed_integration_finishes_source_label_delegation(self) -> None:
],
)

def test_integration_skips_source_already_contained_transitively(self) -> None:
cumulative = entry(1, "shared.py")
contained = entry(2, "shared.py")
batch = new_batch([cumulative, contained], frozen_at="2026-06-20T00:00:00Z")
client = object.__new__(GitHub)
client.config = parse_config(
{
"queue": {
"required_checks": ["CI"],
"trusted_actors": ["trusted"],
},
"integration": {"mode": "all"},
}
)
client.repository = "example/repo"
client.owner = "example"
client.name = "repo"
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(side_effect=[False, True])
client._json = Mock(
side_effect=[
{},
{"sha": "m" * 40},
[],
{"number": 99, "html_url": "https://example.test/99"},
]
)
client.comment = Mock()
client.labels = Mock(return_value={"merge-queue", "deploy-requested"})
client.remove_label = Mock()

result = client.create_integration_pull_request(
batch=batch, entries=[cumulative, contained]
)

self.assertIsNone(result["conflict"])
merge_calls = [
call
for call in client._json.call_args_list
if "repos/example/repo/merges" in call.args
]
self.assertEqual(len(merge_calls), 1)
marker = client.comment.call_args.args[1]
self.assertIn(cumulative.head_sha, marker)
self.assertIn(contained.head_sha, marker)

def test_integration_recovers_empty_merge_response_after_race(self) -> None:
source = entry(1, "shared.py")
batch = new_batch([source], frozen_at="2026-06-20T00:00:00Z")
client = object.__new__(GitHub)
client.config = parse_config(
{
"queue": {
"required_checks": ["CI"],
"trusted_actors": ["trusted"],
},
"integration": {"mode": "all"},
}
)
client.repository = "example/repo"
client.owner = "example"
client.name = "repo"
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(side_effect=[False, True])
client._json = Mock(
side_effect=[
{},
QueueError("GitHub returned invalid JSON"),
{"object": {"sha": "m" * 40}},
[],
{"number": 99, "html_url": "https://example.test/99"},
]
)
client.comment = Mock()
client.labels = Mock(return_value={"merge-queue", "deploy-requested"})
client.remove_label = Mock()

result = client.create_integration_pull_request(batch=batch, entries=[source])

self.assertIsNone(result["conflict"])
marker = client.comment.call_args.args[1]
self.assertIn(source.head_sha, marker)

def test_failed_integration_pr_creation_removes_its_orphan_branch(self) -> None:
first = entry(1, "shared.py")
second = entry(2, "shared.py")
Expand All @@ -3848,6 +3942,7 @@ def test_failed_integration_pr_creation_removes_its_orphan_branch(self) -> None:
client.owner = "example"
client.name = "repo"
client.base_sha = Mock(return_value="b" * 40)
client.is_ancestor = Mock(return_value=False)
client._json = Mock(
side_effect=[
{},
Expand Down