From 13d7293b181581d2e4d59d8a605df76f7feb88a6 Mon Sep 17 00:00:00 2001 From: Matthew Berman <748450+mberman84@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:43:54 -0700 Subject: [PATCH 1/2] Follow only releases that need ownership --- pyproject.toml | 2 +- src/agent_merge_queue/__init__.py | 2 +- src/agent_merge_queue/cli.py | 29 +++++++++++- tests/test_cli.py | 76 ++++++++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87f62bd..108b19e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deploybot-merge-queue" -version = "0.2.7" +version = "0.2.8" description = "DeployBot: a provider-neutral GitHub merge queue for coding agents" readme = "README.md" license = "MIT" diff --git a/src/agent_merge_queue/__init__.py b/src/agent_merge_queue/__init__.py index 38b9e28..77ec758 100644 --- a/src/agent_merge_queue/__init__.py +++ b/src/agent_merge_queue/__init__.py @@ -1,3 +1,3 @@ """DeployBot: a provider-neutral GitHub merge queue for coding agents.""" -__version__ = "0.2.7" +__version__ = "0.2.8" diff --git a/src/agent_merge_queue/cli.py b/src/agent_merge_queue/cli.py index a34adfe..ed813ed 100755 --- a/src/agent_merge_queue/cli.py +++ b/src/agent_merge_queue/cli.py @@ -2691,6 +2691,26 @@ def command_follow( return result +def release_follow_needed(client: GitHub) -> bool: + current = release_state( + main_sha=client.base_sha(), + runs=client.workflow_runs(), + config=client.config.pipeline, + ) + if current["state"] == "testing": + # A repository with no current main CI has no release to follow. An + # active or queued run is returned as latest_ci and should keep its + # release owner. + return current.get("latest_ci") is not None + if current["state"] == "verified": + # Verification can finish just before the original worker is replaced. + # Only revisit it when merged thread records still need completion. + return any( + record.get("phase") == "merged" for record in client.thread_records() + ) + return True + + def command_react( client: GitHub, *, @@ -2757,7 +2777,14 @@ def command_react( ) raise release: dict[str, Any] | None = None - if follow: + should_follow = bool(drained.get("merged")) + if follow and not should_follow: + # The worker that performed the merge may be replaced while waiting for + # CI. Let a later event take over only when a release actually remains; + # an idle all-mode integration batch must not occupy the coordinator for + # the full follow timeout. + should_follow = release_follow_needed(client) + if follow and should_follow: release = command_follow( client, timeout_seconds=timeout_seconds, diff --git a/tests/test_cli.py b/tests/test_cli.py index f897777..2318db4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1192,11 +1192,25 @@ def test_reactor_dispatches_ci_once_after_a_merged_batch(self) -> None: self.assertEqual(result["dispatched_ci"], [{"id": 7, "name": "CI"}]) def test_reactor_follows_release_without_a_new_merge(self) -> None: + sha = "a" * 40 client = Mock() client.config = CONFIG client.pipeline_control.return_value = {"state": "running"} + client.base_sha.return_value = sha + client.workflow_runs.return_value = [ + { + "id": 42, + "name": "CI", + "head_sha": sha, + "status": "completed", + "conclusion": "success", + "event": "workflow_dispatch", + "created_at": "2026-06-20T00:00:00Z", + "updated_at": "2026-06-20T00:01:00Z", + } + ] empty = FreezeResult(None, [], [], [], []) - release = {"state": "verified", "main_sha": "a" * 40} + release = {"state": "verified", "main_sha": sha} with ( patch("agent_merge_queue.cli.promote_integrations", return_value=[]), patch("agent_merge_queue.cli.command_promote", return_value={}), @@ -1222,6 +1236,66 @@ def test_reactor_follows_release_without_a_new_merge(self) -> None: ) self.assertEqual(result["release"], release) + def test_reactor_does_not_follow_idle_all_mode_integration_batch(self) -> None: + config = parse_config( + { + "queue": { + "required_checks": ["CI"], + "trusted_actors": ["trusted"], + "coordinator_actors": ["coordinator"], + }, + "integration": {"mode": "all"}, + } + ) + sha = "a" * 40 + first = entry(1, "a.py") + second = entry(2, "b.py") + batch = new_batch([first, second], frozen_at="2026-06-20T00:00:00Z") + frozen = FreezeResult(batch, [first, second], [], [], []) + client = Mock() + client.config = config + client.coordinator_logins = {"coordinator"} + client.pipeline_control.return_value = {"state": "running"} + client.create_integration_pull_request.return_value = {"number": 99} + client.base_sha.return_value = sha + client.workflow_runs.return_value = [ + { + "id": 42, + "name": "CI", + "head_sha": sha, + "status": "completed", + "conclusion": "success", + "event": "push", + "created_at": "2026-06-20T00:00:00Z", + "updated_at": "2026-06-20T00:01:00Z", + }, + { + "id": 43, + "name": "Deploy", + "head_sha": sha, + "status": "completed", + "conclusion": "success", + "event": "workflow_run", + "created_at": "2026-06-20T00:01:00Z", + "updated_at": "2026-06-20T00:02:00Z", + }, + ] + client.thread_records.return_value = [] + with ( + patch("agent_merge_queue.cli.promote_integrations", return_value=[]), + patch("agent_merge_queue.cli.command_promote", return_value={}), + patch("agent_merge_queue.cli.freeze_queue", return_value=frozen), + patch("agent_merge_queue.cli.command_drain") as drain, + patch("agent_merge_queue.cli.command_follow") as follow_release, + redirect_stdout(io.StringIO()), + ): + result = command_react(client, follow=True, timeout_seconds=10) + + drain.assert_not_called() + follow_release.assert_not_called() + self.assertIsNone(result["release"]) + self.assertEqual(result["integrations"], [{"number": 99}]) + def test_reactor_pauses_when_post_merge_ci_dispatch_fails(self) -> None: client = Mock() client.config = CONFIG From ca9c382b34525ca4756d692debdcc9ea25d3c173 Mon Sep 17 00:00:00 2001 From: Matthew Berman <748450+mberman84@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:44:14 -0700 Subject: [PATCH 2/2] Pin DeployBot v0.2.8 clients to implementation --- README.md | 6 +++--- adapters/claude-code/.mcp.json | 2 +- adapters/codex/agent-merge-queue/.mcp.json | 2 +- adapters/cursor/.cursor/mcp.json | 2 +- examples/github-workflow.yml | 4 ++-- tests/test_skill.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7c45500..8832c27 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ integration PRs, follows `main` through production, and pauses after failures. ## Install -Install the reviewed `v0.2.7` source commit directly from GitHub: +Install the reviewed `v0.2.8` source commit directly from GitHub: ```bash python3 -m pip install \ - 'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@b768fd0d77bc6def505f1cd0c101d37db6edba4d' + 'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@13d7293b181581d2e4d59d8a605df76f7feb88a6' deploybot init ``` @@ -86,7 +86,7 @@ aligned with `pipeline.ci_workflows`. The privileged worker never checks out or executes pull-request code. Pin the Action to the full reviewed release commit: ```yaml -- uses: Forward-Future/DeployBot@b768fd0d77bc6def505f1cd0c101d37db6edba4d +- uses: Forward-Future/DeployBot@13d7293b181581d2e4d59d8a605df76f7feb88a6 ``` The Action uses GitHub's built-in workflow token. GitHub intentionally does not diff --git a/adapters/claude-code/.mcp.json b/adapters/claude-code/.mcp.json index cb1c297..b77f489 100644 --- a/adapters/claude-code/.mcp.json +++ b/adapters/claude-code/.mcp.json @@ -4,7 +4,7 @@ "command": "uvx", "args": [ "--from", - "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@b768fd0d77bc6def505f1cd0c101d37db6edba4d", + "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@13d7293b181581d2e4d59d8a605df76f7feb88a6", "deploybot-mcp" ] } diff --git a/adapters/codex/agent-merge-queue/.mcp.json b/adapters/codex/agent-merge-queue/.mcp.json index cb1c297..b77f489 100644 --- a/adapters/codex/agent-merge-queue/.mcp.json +++ b/adapters/codex/agent-merge-queue/.mcp.json @@ -4,7 +4,7 @@ "command": "uvx", "args": [ "--from", - "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@b768fd0d77bc6def505f1cd0c101d37db6edba4d", + "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@13d7293b181581d2e4d59d8a605df76f7feb88a6", "deploybot-mcp" ] } diff --git a/adapters/cursor/.cursor/mcp.json b/adapters/cursor/.cursor/mcp.json index cb1c297..b77f489 100644 --- a/adapters/cursor/.cursor/mcp.json +++ b/adapters/cursor/.cursor/mcp.json @@ -4,7 +4,7 @@ "command": "uvx", "args": [ "--from", - "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@b768fd0d77bc6def505f1cd0c101d37db6edba4d", + "deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@13d7293b181581d2e4d59d8a605df76f7feb88a6", "deploybot-mcp" ] } diff --git a/examples/github-workflow.yml b/examples/github-workflow.yml index af2fc85..827533d 100644 --- a/examples/github-workflow.yml +++ b/examples/github-workflow.yml @@ -55,5 +55,5 @@ jobs: with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false - # v0.2.7 implementation; keep the full commit for privileged workflows. - - uses: Forward-Future/DeployBot@b768fd0d77bc6def505f1cd0c101d37db6edba4d + # v0.2.8 implementation; keep the full commit for privileged workflows. + - uses: Forward-Future/DeployBot@13d7293b181581d2e4d59d8a605df76f7feb88a6 diff --git a/tests/test_skill.py b/tests/test_skill.py index 1af1ec0..32dba4a 100644 --- a/tests/test_skill.py +++ b/tests/test_skill.py @@ -6,7 +6,7 @@ ROOT = Path(__file__).resolve().parents[1] CANONICAL = ROOT / "skills" / "deploybot" / "SKILL.md" -RELEASE_COMMIT = "b768fd0d77bc6def505f1cd0c101d37db6edba4d" +RELEASE_COMMIT = "13d7293b181581d2e4d59d8a605df76f7feb88a6" CHECKOUT_COMMIT = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"