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
13 changes: 8 additions & 5 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.19` source commit directly from GitHub:
Install the reviewed `v0.2.20` source commit directly from GitHub:

```bash
python3 -m pip install \
'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@1b6379a258a0b6f743ce77c2d108dfd7e83d582b'
'deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@3bf238140fba3d4d5fbd2d739b9f5422e99567dd'
deploybot init
```

Expand Down Expand Up @@ -95,7 +95,7 @@ worker can dispatch deployment when GitHub suppresses the `workflow_run` event
for token-dispatched CI. Pin the Action to the full reviewed release commit:

```yaml
- uses: Forward-Future/DeployBot@1b6379a258a0b6f743ce77c2d108dfd7e83d582b
- uses: Forward-Future/DeployBot@3bf238140fba3d4d5fbd2d739b9f5422e99567dd
```

The Action uses GitHub's built-in workflow token. GitHub intentionally does not
Expand Down Expand Up @@ -160,9 +160,12 @@ they cannot create the original per-pull-request deploy intent.
`deploybot status` reports active metadata-only agent threads, pending native
notifications, every PR stage, deploy requests and their exact authorized heads,
queue order, queued and pre-queue intent overlaps, exact-`main` CI, deployment,
and pipeline pause state. It alerts when a deploy request exceeds the configured
pipeline pause state, and every open PR that has not been bound to its native
opening thread. It alerts when a deploy request exceeds the configured
ready-to-merge target and names the current gate. It never stores prompts,
transcripts, source, or credentials.
transcripts, source, or credentials. A failed PR rollup is reconciled against
the exact commit's check runs before DeployBot creates a repair block, so a
cancelled superseded run cannot hide its replacement.

`deploybot react` promotes ready intent, skips blockers, drains independent
work, and creates integration PRs when configured. New batches contain at most
Expand Down
2 changes: 1 addition & 1 deletion adapters/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deploybot",
"version": "0.2.19",
"version": "0.2.20",
"description": "DeployBot: a provider-neutral GitHub merge queue for coding agents",
"author": {
"name": "DeployBot contributors"
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@1b6379a258a0b6f743ce77c2d108dfd7e83d582b",
"deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@3bf238140fba3d4d5fbd2d739b9f5422e99567dd",
"deploybot-mcp"
]
}
Expand Down
5 changes: 5 additions & 0 deletions adapters/claude-code/skills/deploybot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ the destination for repair handoffs and the final deployment receipt. A later
deploy, repair, integration, or coordinator thread must never claim ownership
of an already-open PR.

Before the PR-opening thread finishes its response, call `pipeline_status` and
confirm that the exact PR appears in `pull_request_thread_owners` and not in
`unbound_pull_requests`. A missing binding means the PR-opening task is still
incomplete: publish the binding from this thread and verify it before stopping.

Require the user's exact `deploy` instruction before calling
`request_deployment` or `deploybot request` for that conversation's pull
request. DeployBot resolves the previously recorded PR-opening thread; never
Expand Down
4 changes: 4 additions & 0 deletions adapters/claude-code/skills/manage-merge-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Immediately after opening the PR, call `update_agent_thread` in `pr-review`
phase with its number. That first binding is immutable and owns repair handoffs
and the final deployment receipt.

Before the PR-opening response finishes, call `pipeline_status` and verify that
the exact PR is in `pull_request_thread_owners`, not
`unbound_pull_requests`. Bind it from this thread and recheck if missing.

Only the user's exact `deploy` instruction authorizes `request_deployment` for
this thread's PR. DeployBot uses the recorded opening thread; a coordinator
must never substitute its own ID. If review fixes change
Expand Down
2 changes: 1 addition & 1 deletion adapters/codex/agent-merge-queue/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deploybot",
"version": "0.2.19",
"version": "0.2.20",
"description": "Coordinate exact-head pull requests through verified deployment and thread notification",
"author": {
"name": "DeployBot contributors"
Expand Down
6 changes: 6 additions & 0 deletions adapters/codex/agent-merge-queue/skills/deploybot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ update` in `pr-draft`, `pr-review`, or `ready` phase with the PR number. The
first trusted binding is immutable and owns repair handoffs and the final
deployment receipt. Later deploy and coordinator threads never replace it.

Before this PR-opening thread finishes its response, run `deploybot status
--json` and confirm that the exact PR appears in
`pull_request_thread_owners` and not in `unbound_pull_requests`. A missing
binding means the PR-opening task is still incomplete: publish the binding from
this thread and verify it before stopping.

Require the user's exact `deploy` instruction before running `deploybot request`
for that conversation's pull request. DeployBot resolves the recorded
PR-opening thread; never substitute the current coordinator's thread ID. If an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Immediately after opening the PR, run `deploybot thread update --provider codex
--thread-id <stable-id> --phase pr-review --pr <number>`. That first binding is
immutable and owns repair handoffs and the final deployment receipt.

Before this PR-opening response finishes, run `deploybot status --json` and
verify that the exact PR is in `pull_request_thread_owners`, not
`unbound_pull_requests`. Bind it from this thread and recheck if missing.

Only the user's exact `deploy` instruction authorizes `deploybot request` for
this thread's PR. DeployBot uses the recorded opening thread; a coordinator
must never substitute its own ID. If review fixes change the
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@1b6379a258a0b6f743ce77c2d108dfd7e83d582b",
"deploybot-merge-queue[mcp] @ git+https://github.com/Forward-Future/DeployBot.git@3bf238140fba3d4d5fbd2d739b9f5422e99567dd",
"deploybot-mcp"
]
}
Expand Down
4 changes: 2 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DeployBot reference

This reference describes the CLI, MCP server, policy file, and GitHub Action in
DeployBot v0.2.19. GitHub labels and authenticated comments are the durable state;
DeployBot v0.2.20. GitHub labels and authenticated comments are the durable state;
the CLI and MCP tools are two interfaces to the same operations.

## CLI
Expand All @@ -23,7 +23,7 @@ DeployBot resolves the pull request for the current branch.
| `deploybot init [--force]` | Write a safe starter policy. Existing files are preserved unless `--force` is supplied. |
| `deploybot ensure-labels` | Create or refresh the configured queue, blocked, intent, pause, and registry labels. |
| `deploybot doctor [--json]` | Check authentication, policy, labels, actors, checks, workflows, and branch protection without changing repository state. |
| `deploybot status [--json]` | Read active thread metadata, pending native notifications, PR stages, exact-head deploy intent, pre-queue intent overlaps, request-stage timing alerts, queue state, exact-main CI, deployment, and pipeline control state. |
| `deploybot status [--json]` | Read active thread metadata, unbound open PRs, pending native notifications, PR stages, exact-head deploy intent, pre-queue intent overlaps, request-stage timing alerts, queue state, exact-main CI, deployment, and pipeline control state. |
| `deploybot plan [--json]` | Read the ordered queue, dependencies, blockers, and source-overlap groups. |
| `deploybot inspect [PR] [--json]` | Evaluate one exact PR head without granting merge authority. |
| `deploybot metrics [--limit N] [--json]` | Summarize p50, p95, and maximum delivery timings for recent merged PRs. The default limit is 25. |
Expand Down
4 changes: 2 additions & 2 deletions examples/github-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,5 @@ jobs:
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
# v0.2.19 implementation; keep the full commit for privileged workflows.
- uses: Forward-Future/DeployBot@1b6379a258a0b6f743ce77c2d108dfd7e83d582b
# v0.2.20 implementation; keep the full commit for privileged workflows.
- uses: Forward-Future/DeployBot@3bf238140fba3d4d5fbd2d739b9f5422e99567dd
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.19"
version = "0.2.20"
description = "DeployBot: a provider-neutral GitHub merge queue for coding agents"
readme = "README.md"
license = "MIT"
Expand Down
5 changes: 5 additions & 0 deletions skills/deploybot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ the destination for repair handoffs and the final deployment receipt. A later
deploy, repair, integration, or coordinator thread must never claim ownership
of an already-open PR.

Before the PR-opening thread finishes its response, call `pipeline_status` and
confirm that the exact PR appears in `pull_request_thread_owners` and not in
`unbound_pull_requests`. A missing binding means the PR-opening task is still
incomplete: publish the binding from this thread and verify it before stopping.

Require the user's exact `deploy` instruction before calling
`request_deployment` or `deploybot request` for that conversation's pull
request. DeployBot resolves the previously recorded PR-opening thread; never
Expand Down
3 changes: 3 additions & 0 deletions skills/manage-merge-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ the repository's policy; never assume a particular review vendor.
6. Immediately after opening the PR, call `update_agent_thread` in `pr-draft`,
`pr-review`, or `ready` phase with its number so this opening thread becomes
the immutable repair and deployment-receipt destination.
7. Before finishing the PR-opening response, call `pipeline_status` and verify
that the exact PR is in `pull_request_thread_owners`, not
`unbound_pull_requests`. Bind and recheck it if missing.

Do not merge merely because review is complete.

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.19"
__version__ = "0.2.20"
32 changes: 29 additions & 3 deletions src/agent_merge_queue/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2344,9 +2344,22 @@ def snapshot(
INTEGRATION_MARKER,
coordinator_logins(self),
)
if integration and any(
checks.get(name) != "passed" for name in self.config.required_checks
):
# GitHub's PR rollup can briefly retain a cancelled or failed run after
# a replacement run for the same exact head has started. Reconcile a
# reported failure with the commit check-runs endpoint before creating
# a repair block. Integration PRs keep the broader fallback because
# their controller-dispatched checks may be absent from the rollup.
rollup_has_required_failure = any(
checks.get(name) == "failed" for name in self.config.required_checks
)
integration_needs_exact_checks = bool(
integration
and any(
checks.get(name) != "passed"
for name in self.config.required_checks
)
)
if rollup_has_required_failure or integration_needs_exact_checks:
checks = merge_known_check_states(
check_states(check_rollup + self.commit_check_runs(head_sha)),
known_checks,
Expand Down Expand Up @@ -2910,6 +2923,18 @@ def inspect(
for _, value in sorted(thread_owners.items())
if value.pull_request in open_number_set
],
"unbound_pull_requests": [
{
"pull_request": value["number"],
"pipeline_stage": stage,
"head_sha": value["head_sha"],
"title": value["title"],
"url": value["url"],
}
for stage, values in stages.items()
for value in values
if value["number"] not in thread_owners
],
"notifications": client.deployment_notifications(),
"pull_requests": stages,
"queue": [
Expand All @@ -2934,6 +2959,7 @@ def print_pipeline_status(value: dict[str, Any], *, json_output: bool) -> None:
"threads: "
f"{len(value['threads'])} active; "
f"notifications: {len(value.get('notifications') or [])} pending; "
f"unbound PRs: {len(value.get('unbound_pull_requests') or [])}; "
"deploy requests: "
f"{sum(1 for entries in stages.values() for entry in entries if entry.get('deploy_intent'))}; "
f"queue: {len(value['queue'])}; "
Expand Down
65 changes: 65 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,13 @@ def test_status_exposes_intent_head_overlap_and_request_delay(self) -> None:
result["pull_request_thread_owners"][0]["thread_id"],
"opening-thread",
)
self.assertEqual(
{
(value["pull_request"], value["pipeline_stage"])
for value in result["unbound_pull_requests"]
},
{(2, "blocked"), (3, "ready")},
)

def test_overlap_mode_holds_only_ready_members_of_near_ready_components(
self,
Expand Down Expand Up @@ -1597,6 +1604,64 @@ def test_integration_snapshot_uses_exact_commit_check_fallback(self) -> None:
self.assertEqual(value.checks["CI"], "passed")
client.commit_check_runs.assert_called_once_with(head_sha)

def test_failed_rollup_uses_exact_head_replacement_check(self) -> None:
head_sha = "a" * 40
client = object.__new__(GitHub)
client.config = CONFIG
client.repository = "example/repo"
client.trusted_logins = {"trusted"}
client.coordinator_logins = {"trusted"}
client.comments = Mock(return_value=[])
client.changed_paths = Mock(return_value=([], []))
client.commit_check_runs = Mock(
return_value=[
{
"name": "CI",
"conclusion": "cancelled",
"started_at": "2026-06-20T00:00:00Z",
},
{
"name": "CI",
"status": "in_progress",
"started_at": "2026-06-20T00:01:00Z",
},
]
)
client._json = Mock(
return_value={
"baseRefName": "main",
"body": "",
"headRefOid": head_sha,
"isDraft": False,
"labels": [],
"mergeStateStatus": "UNSTABLE",
"mergeable": "MERGEABLE",
"number": 39,
"state": "OPEN",
"statusCheckRollup": [
{
"__typename": "CheckRun",
"name": "CI",
"conclusion": "CANCELLED",
"startedAt": "2026-06-20T00:00:00Z",
}
],
"title": "Replacement check is running",
"url": "https://example.test/39",
}
)

value = client.snapshot(
39,
require_marker=False,
allow_blocked_label=True,
)

self.assertEqual(value.state, "waiting")
self.assertEqual(value.checks["CI"], "pending")
self.assertNotIn("CI failed", value.reasons)
client.commit_check_runs.assert_called_once_with(head_sha)

def test_required_checks_accept_passing_terminal_conclusions(self) -> None:
states = check_states(
[
Expand Down
4 changes: 3 additions & 1 deletion tests/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

ROOT = Path(__file__).resolve().parents[1]
CANONICAL = ROOT / "skills" / "deploybot" / "SKILL.md"
RELEASE_COMMIT = "1b6379a258a0b6f743ce77c2d108dfd7e83d582b"
RELEASE_COMMIT = "3bf238140fba3d4d5fbd2d739b9f5422e99567dd"
CHECKOUT_COMMIT = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"


Expand Down Expand Up @@ -81,6 +81,8 @@ def test_status_guidance_is_read_only(self) -> None:
self.assertIn("acknowledge_thread_deployment", skill)
self.assertIn("heartbeat automation", skill)
self.assertIn("notification_handoff.required_action", skill)
self.assertIn("unbound_pull_requests", skill)
self.assertIn("pull_request_thread_owners", skill)
self.assertIn("human-facing release receipt", skill)
self.assertIn("acknowledge silently", skill)
self.assertIn("untrusted display-only", skill)
Expand Down