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: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion adapters/claude-code/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Expand Down
2 changes: 1 addition & 1 deletion adapters/codex/agent-merge-queue/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Expand Down
2 changes: 1 addition & 1 deletion adapters/cursor/.cursor/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Expand Down
4 changes: 2 additions & 2 deletions examples/github-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/agent_merge_queue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""DeployBot: a provider-neutral GitHub merge queue for coding agents."""

__version__ = "0.2.7"
__version__ = "0.2.8"
29 changes: 28 additions & 1 deletion src/agent_merge_queue/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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,
Expand Down
76 changes: 75 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={}),
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down